using Api.SeaHavenIndustries.DTOs; using Api.SeaHavenIndustries.Filters; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; namespace Api.SeaHavenIndustries.Controllers; /// /// Anonymous invite registration. The invite token travels only in request bodies, /// and every failure maps to a fixed public message. /// [AllowAnonymous] [ApiController] [Route("api/team-member-invites")] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] [TypeFilter(typeof(InviteRegistrationExceptionFilter))] public sealed class TeamMemberInviteController : ControllerBase { public const string InvalidInviteMessage = "This invite link is invalid or has expired. Ask your admin to send a new invite."; private readonly ITeamMemberRegistrationService _registrationService; public TeamMemberInviteController(ITeamMemberRegistrationService registrationService) { _registrationService = registrationService; } [HttpPost("resolve")] public async Task Resolve(TeamMemberInviteTokenRequestDTO request, CancellationToken cancellationToken) { var outcome = await _registrationService.ResolveAsync(request.Token, cancellationToken); return outcome.Status == TeamMemberRegistrationStatus.Ok ? Ok(outcome.Details) : Failure(outcome); } [HttpPost("send-code")] public async Task SendCode(TeamMemberInviteTokenRequestDTO request, CancellationToken cancellationToken) { var outcome = await _registrationService.SendCodeAsync(request.Token, cancellationToken); return outcome.Status == TeamMemberRegistrationStatus.Ok ? Ok(new { message = "Code sent" }) : Failure(outcome); } [HttpPost("verify-code")] public async Task VerifyCode(VerifyTeamMemberInviteCodeRequestDTO request, CancellationToken cancellationToken) { var outcome = await _registrationService.VerifyCodeAsync(request.Token, request.Code, cancellationToken); return outcome.Status == TeamMemberRegistrationStatus.Ok ? Ok(new { message = "Email confirmed" }) : Failure(outcome); } [HttpPost("complete")] public async Task Complete(CompleteTeamMemberRegistrationRequestDTO request, CancellationToken cancellationToken) { var outcome = await _registrationService.CompleteAsync(request, cancellationToken); return outcome.Status == TeamMemberRegistrationStatus.Ok && outcome.Session is not null ? Ok(LoginPayload.From(outcome.Session)) : Failure(outcome); } private IActionResult Failure(TeamMemberRegistrationOutcomeDTO outcome) { var (statusCode, code, message) = outcome.Status switch { TeamMemberRegistrationStatus.CodeIncorrect => (StatusCodes.Status400BadRequest, "code_incorrect", "Incorrect code — check your email and try again"), TeamMemberRegistrationStatus.CodeExpired => (StatusCodes.Status400BadRequest, "code_expired", "This code has expired — request a new code"), TeamMemberRegistrationStatus.CodeLocked => (StatusCodes.Status400BadRequest, "code_locked", "Too many incorrect attempts — request a new code"), TeamMemberRegistrationStatus.ResendTooSoon => (StatusCodes.Status429TooManyRequests, "resend_too_soon", "Please wait a moment before requesting another code"), TeamMemberRegistrationStatus.ResendLimitReached => (StatusCodes.Status429TooManyRequests, "resend_limit_reached", "Too many codes were requested. Ask your admin to send a new invite."), TeamMemberRegistrationStatus.CodeDeliveryFailed => (StatusCodes.Status503ServiceUnavailable, "code_delivery_failed", "We couldn't send the code. Try again in a minute."), TeamMemberRegistrationStatus.EmailNotConfirmed => (StatusCodes.Status400BadRequest, "email_not_confirmed", "Confirm your email before finishing registration"), TeamMemberRegistrationStatus.PasswordRejected => (StatusCodes.Status400BadRequest, "password_rejected", "Password must be at least 6 characters and include one uppercase letter, one number, and one special character."), TeamMemberRegistrationStatus.InvalidPhone => (StatusCodes.Status400BadRequest, "invalid_phone", "Enter a valid phone number"), _ => (StatusCodes.Status400BadRequest, "invalid_invite", InvalidInviteMessage) }; if (outcome.RetryAfterSeconds is int retryAfter) Response.Headers.RetryAfter = retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture); return StatusCode(statusCode, new { code, message, retryAfterSeconds = outcome.RetryAfterSeconds }); } }