feat(api): add /refresh [AllowAnonymous] and /logout [Authorize] endpoints to AuthController
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using SIGCM2.Application.Abstractions;
|
using SIGCM2.Application.Abstractions;
|
||||||
using SIGCM2.Application.Auth.Login;
|
using SIGCM2.Application.Auth.Login;
|
||||||
|
using SIGCM2.Application.Auth.Logout;
|
||||||
|
using SIGCM2.Application.Auth.Refresh;
|
||||||
|
|
||||||
namespace SIGCM2.Api.Controllers;
|
namespace SIGCM2.Api.Controllers;
|
||||||
|
|
||||||
@@ -10,19 +14,22 @@ namespace SIGCM2.Api.Controllers;
|
|||||||
public sealed class AuthController : ControllerBase
|
public sealed class AuthController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IDispatcher _dispatcher;
|
private readonly IDispatcher _dispatcher;
|
||||||
private readonly IValidator<LoginCommand> _validator;
|
private readonly IValidator<LoginCommand> _loginValidator;
|
||||||
|
private readonly IValidator<RefreshCommand> _refreshValidator;
|
||||||
|
|
||||||
public AuthController(IDispatcher dispatcher, IValidator<LoginCommand> validator)
|
public AuthController(
|
||||||
|
IDispatcher dispatcher,
|
||||||
|
IValidator<LoginCommand> loginValidator,
|
||||||
|
IValidator<RefreshCommand> refreshValidator)
|
||||||
{
|
{
|
||||||
_dispatcher = dispatcher;
|
_dispatcher = dispatcher;
|
||||||
_validator = validator;
|
_loginValidator = loginValidator;
|
||||||
|
_refreshValidator = refreshValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Authenticates a user and returns a JWT access token.</summary>
|
/// <summary>Authenticates a user and returns a JWT access token + refresh token.</summary>
|
||||||
/// <response code="200">Returns access token and refresh token.</response>
|
|
||||||
/// <response code="400">Validation error — missing or empty fields.</response>
|
|
||||||
/// <response code="401">Invalid credentials.</response>
|
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
|
[AllowAnonymous]
|
||||||
[ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
@@ -30,7 +37,7 @@ public sealed class AuthController : ControllerBase
|
|||||||
{
|
{
|
||||||
var command = new LoginCommand(request.Username ?? string.Empty, request.Password ?? string.Empty);
|
var command = new LoginCommand(request.Username ?? string.Empty, request.Password ?? string.Empty);
|
||||||
|
|
||||||
var validation = await _validator.ValidateAsync(command);
|
var validation = await _loginValidator.ValidateAsync(command);
|
||||||
if (!validation.IsValid)
|
if (!validation.IsValid)
|
||||||
{
|
{
|
||||||
var errors = validation.Errors
|
var errors = validation.Errors
|
||||||
@@ -42,7 +49,56 @@ public sealed class AuthController : ControllerBase
|
|||||||
var result = await _dispatcher.Send<LoginCommand, LoginResponseDto>(command);
|
var result = await _dispatcher.Send<LoginCommand, LoginResponseDto>(command);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("refresh")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(RefreshResponseDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
public async Task<IActionResult> 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<RefreshCommand, RefreshResponseDto>(command);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Revokes all active refresh tokens for the authenticated user.
|
||||||
|
/// Requires a valid Bearer access token. Client must discard local tokens after this call.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("logout")]
|
||||||
|
[Authorize]
|
||||||
|
[ProducesResponseType(typeof(LogoutResponseDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
public async Task<IActionResult> Logout()
|
||||||
|
{
|
||||||
|
var sub = User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
|
||||||
|
if (!int.TryParse(sub, out var userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var result = await _dispatcher.Send<LogoutCommand, LogoutResponseDto>(new LogoutCommand(userId));
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Login request body — nullable to catch missing field scenarios.</summary>
|
/// <summary>Login request body — nullable to catch missing field scenarios.</summary>
|
||||||
public sealed record LoginRequest(string? Username, string? Password);
|
public sealed record LoginRequest(string? Username, string? Password);
|
||||||
|
|
||||||
|
/// <summary>Refresh request body.</summary>
|
||||||
|
public sealed record RefreshRequest(string? AccessToken, string? RefreshToken);
|
||||||
|
|||||||
Reference in New Issue
Block a user