using System.IdentityModel.Tokens.Jwt; using FluentValidation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SIGCM2.Application.Abstractions; using SIGCM2.Application.Auth.Login; using SIGCM2.Application.Auth.Logout; using SIGCM2.Application.Auth.Refresh; namespace SIGCM2.Api.Controllers; [ApiController] [Route("api/v1/auth")] public sealed class AuthController : ControllerBase { private readonly IDispatcher _dispatcher; private readonly IValidator _loginValidator; private readonly IValidator _refreshValidator; public AuthController( IDispatcher dispatcher, IValidator loginValidator, IValidator refreshValidator) { _dispatcher = dispatcher; _loginValidator = loginValidator; _refreshValidator = refreshValidator; } /// Authenticates a user and returns a JWT access token + refresh token. [HttpPost("login")] [AllowAnonymous] [ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Login([FromBody] LoginRequest request) { var command = new LoginCommand(request.Username ?? string.Empty, request.Password ?? string.Empty); var validation = await _loginValidator.ValidateAsync(command); if (!validation.IsValid) { var errors = validation.Errors .GroupBy(e => e.PropertyName) .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); return BadRequest(new { errors }); } var result = await _dispatcher.Send(command); return Ok(result); } /// /// Rotates a refresh token pair. Accepts an expired access token to extract the user identity. /// Returns a new access + refresh token pair. Does NOT require Authorization header. /// [HttpPost("refresh")] [AllowAnonymous] [ProducesResponseType(typeof(RefreshResponseDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Refresh([FromBody] RefreshRequest request) { var command = new RefreshCommand( request.AccessToken ?? string.Empty, request.RefreshToken ?? string.Empty); var validation = await _refreshValidator.ValidateAsync(command); if (!validation.IsValid) { var errors = validation.Errors .GroupBy(e => e.PropertyName) .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()); return BadRequest(new { errors }); } var result = await _dispatcher.Send(command); return Ok(result); } /// /// Revokes all active refresh tokens for the authenticated user. /// Requires a valid Bearer access token. Client must discard local tokens after this call. /// [HttpPost("logout")] [Authorize] [ProducesResponseType(typeof(LogoutResponseDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Logout() { var sub = User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value; if (!int.TryParse(sub, out var userId)) return Unauthorized(); var result = await _dispatcher.Send(new LogoutCommand(userId)); return Ok(result); } } /// Login request body — nullable to catch missing field scenarios. public sealed record LoginRequest(string? Username, string? Password); /// Refresh request body. public sealed record RefreshRequest(string? AccessToken, string? RefreshToken);