using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Meezi.API.Models.Auth; using Meezi.API.Services; using Meezi.API.Services; using Meezi.Core.Constants; using Meezi.Shared; namespace Meezi.API.Controllers; [ApiController] [Route("api/auth")] public class AuthController : ControllerBase { private readonly IAuthService _authService; private readonly IValidator _sendOtpValidator; private readonly IValidator _verifyOtpValidator; private readonly IValidator _refreshValidator; private readonly IValidator _registerValidator; private readonly IValidator _verifyRegisterValidator; public AuthController( IAuthService authService, IValidator sendOtpValidator, IValidator verifyOtpValidator, IValidator refreshValidator, IValidator registerValidator, IValidator verifyRegisterValidator) { _authService = authService; _sendOtpValidator = sendOtpValidator; _verifyOtpValidator = verifyOtpValidator; _refreshValidator = refreshValidator; _registerValidator = registerValidator; _verifyRegisterValidator = verifyRegisterValidator; } [HttpPost("send-otp")] [EnableRateLimiting("auth-otp")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task SendOtp([FromBody] SendOtpRequest request, CancellationToken cancellationToken) { var validation = await _sendOtpValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); var (success, data, code, message) = await _authService.SendOtpAsync(request, cancellationToken); if (!success) return ErrorResult(code!, message!); return Ok(new ApiResponse(true, data)); } [HttpPost("verify-otp")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task VerifyOtp([FromBody] VerifyOtpRequest request, CancellationToken cancellationToken) { var validation = await _verifyOtpValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); var (success, data, code, message, choices) = await _authService.VerifyOtpAsync(request, cancellationToken); if (!success && code == "CHOOSE_CAFE") return Ok(new ApiResponse(false, choices, new ApiError("CHOOSE_CAFE", "Please select a café to continue."))); if (!success) return ErrorResult(code!, message!); return Ok(new ApiResponse(true, data)); } [HttpPost("switch-cafe")] [Authorize] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task SwitchCafe([FromBody] SwitchCafeRequest request, CancellationToken cancellationToken) { var userId = User.FindFirstValue(JwtRegisteredClaimNames.Sub) ?? User.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) return Unauthorized(); var (success, data, code, message) = await _authService.SwitchCafeAsync(userId, request.CafeId, cancellationToken); if (!success) return ErrorResult(code!, message!); return Ok(new ApiResponse(true, data)); } [HttpPost("refresh")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task Refresh([FromBody] RefreshTokenRequest request, CancellationToken cancellationToken) { var validation = await _refreshValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); var (success, data, code, message) = await _authService.RefreshAsync(request, cancellationToken); if (!success) return ErrorResult(code!, message!); return Ok(new ApiResponse(true, data)); } [HttpPost("register")] [EnableRateLimiting("auth-otp")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task Register([FromBody] RegisterRequest request, CancellationToken cancellationToken) { var validation = await _registerValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); var (success, data, code, message) = await _authService.RegisterAsync(request, cancellationToken); if (!success) return ErrorResult(code!, message!); return Ok(new ApiResponse(true, data)); } [HttpPost("verify-register")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task VerifyRegister([FromBody] VerifyRegisterRequest request, CancellationToken cancellationToken) { var validation = await _verifyRegisterValidator.ValidateAsync(request, cancellationToken); if (!validation.IsValid) return BadRequest(ValidationError(validation)); var (success, data, code, message) = await _authService.VerifyRegisterAsync(request, cancellationToken); if (!success) return ErrorResult(code!, message!); return Ok(new ApiResponse(true, data)); } [HttpGet("me")] [Authorize] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public IActionResult GetMe() { var userId = User.FindFirstValue(JwtRegisteredClaimNames.Sub) ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty; var expClaim = User.FindFirstValue(JwtRegisteredClaimNames.Exp); var expiresAt = expClaim != null && long.TryParse(expClaim, out var exp) ? DateTimeOffset.FromUnixTimeSeconds(exp).UtcDateTime : DateTime.UtcNow; var data = new AuthTokenResponse( AccessToken: string.Empty, RefreshToken: string.Empty, ExpiresAt: expiresAt, UserId: userId, CafeId: User.FindFirstValue(MeeziClaimTypes.CafeId) ?? string.Empty, Role: User.FindFirstValue(MeeziClaimTypes.Role) ?? string.Empty, PlanTier: User.FindFirstValue(MeeziClaimTypes.PlanTier) ?? string.Empty, Language: User.FindFirstValue(MeeziClaimTypes.Language) ?? string.Empty, Actor: User.FindFirstValue(MeeziClaimTypes.Actor) ?? MeeziActorKinds.Merchant, BranchId: User.FindFirstValue(MeeziClaimTypes.BranchId)); return Ok(new ApiResponse(true, data)); } private static ApiResponse ValidationError(FluentValidation.Results.ValidationResult validation) { var first = validation.Errors.First(); return new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName)); } private IActionResult ErrorResult(string code, string message) => code switch { "RATE_LIMITED" => StatusCode(StatusCodes.Status429TooManyRequests, new ApiResponse(false, null, new ApiError(code, message))), "NOT_FOUND" => NotFound(new ApiResponse(false, null, new ApiError(code, message))), "INVALID_OTP" or "INVALID_TOKEN" => Unauthorized(new ApiResponse(false, null, new ApiError(code, message))), "ALREADY_REGISTERED" => Conflict(new ApiResponse(false, null, new ApiError(code, message))), _ => BadRequest(new ApiResponse(false, null, new ApiError(code, message))) }; }