using FluentValidation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SIGCM2.Api.Authorization;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Common;
using SIGCM2.Application.Usuarios.Create;
using SIGCM2.Application.Usuarios.Deactivate;
using SIGCM2.Application.Usuarios.GetById;
using SIGCM2.Application.Usuarios.List;
using SIGCM2.Application.Usuarios.Reactivate;
using SIGCM2.Application.Usuarios.Update;
using SIGCM2.Application.Usuarios.ChangeMyPassword;
using SIGCM2.Application.Usuarios.ResetPassword;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace SIGCM2.Api.Controllers;
///
/// UDT-001/UDT-008: Usuario management endpoints.
/// RequirePermission moved to method level to allow /me/password with [Authorize] only.
///
[ApiController]
[Route("api/v1/users")]
public sealed class UsuariosController : ControllerBase
{
private readonly IDispatcher _dispatcher;
private readonly IValidator _createValidator;
public UsuariosController(
IDispatcher dispatcher,
IValidator createValidator)
{
_dispatcher = dispatcher;
_createValidator = createValidator;
}
/// Creates a new user. Requires administracion:usuarios:gestionar.
[HttpPost]
[RequirePermission("administracion:usuarios:gestionar")]
[ProducesResponseType(typeof(UsuarioCreatedDto), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task CreateUsuario([FromBody] CreateUsuarioRequest request)
{
var command = new CreateUsuarioCommand(
Username: request.Username ?? string.Empty,
Password: request.Password ?? string.Empty,
Nombre: request.Nombre ?? string.Empty,
Apellido: request.Apellido ?? string.Empty,
Email: request.Email,
Rol: request.Rol ?? string.Empty);
var validation = await _createValidator.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 CreatedAtAction(nameof(CreateUsuario), new { id = result.Id }, result);
}
/// Lists usuarios with optional filters and pagination.
[HttpGet]
[RequirePermission("administracion:usuarios:gestionar")]
[ProducesResponseType(typeof(PagedResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task ListUsuarios(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? rol = null,
[FromQuery] bool? activo = null,
[FromQuery] string? search = null)
{
if (page < 1) return BadRequest(new { error = "page must be >= 1" });
if (pageSize < 1) return BadRequest(new { error = "pageSize must be >= 1" });
var query = new ListUsuariosQuery(page, pageSize, rol, activo, search);
var result = await _dispatcher.Send>(query);
return Ok(result);
}
/// Gets a single usuario by id.
[HttpGet("{id:int}")]
[RequirePermission("administracion:usuarios:gestionar")]
[ProducesResponseType(typeof(UsuarioDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetUsuarioById([FromRoute] int id)
{
var query = new GetUsuarioByIdQuery(id);
var result = await _dispatcher.Send(query);
return Ok(result);
}
/// Updates a usuario's editable fields.
[HttpPut("{id:int}")]
[RequirePermission("administracion:usuarios:gestionar")]
[ProducesResponseType(typeof(UsuarioDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task UpdateUsuario([FromRoute] int id, [FromBody] UpdateUsuarioRequest request)
{
var command = new UpdateUsuarioCommand(
Id: id,
Nombre: request.Nombre ?? string.Empty,
Apellido: request.Apellido ?? string.Empty,
Email: request.Email,
Rol: request.Rol ?? string.Empty,
Activo: request.Activo ?? true);
var result = await _dispatcher.Send(command);
return Ok(result);
}
/// Deactivates a usuario (idempotent).
[HttpPatch("{id:int}/deactivate")]
[RequirePermission("administracion:usuarios:gestionar")]
[ProducesResponseType(typeof(UsuarioDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task DeactivateUsuario([FromRoute] int id)
{
var command = new DeactivateUsuarioCommand(id);
var result = await _dispatcher.Send(command);
return Ok(result);
}
/// Reactivates a usuario (idempotent).
[HttpPatch("{id:int}/reactivate")]
[RequirePermission("administracion:usuarios:gestionar")]
[ProducesResponseType(typeof(UsuarioDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task ReactivateUsuario([FromRoute] int id)
{
var command = new ReactivateUsuarioCommand(id);
var result = await _dispatcher.Send(command);
return Ok(result);
}
///
/// Changes the authenticated user's own password.
/// Declared BEFORE /{id:int} route to avoid routing ambiguity (though :int constraint handles it).
/// Requires only authentication (no specific permission).
///
[HttpPut("me/password")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task ChangeMyPassword([FromBody] ChangeMyPasswordRequest request)
{
var sub = User.FindFirstValue(JwtRegisteredClaimNames.Sub)
?? User.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new UnauthorizedAccessException();
var command = new ChangeMyPasswordCommand(
UsuarioId: int.Parse(sub),
OldPassword: request.OldPassword ?? string.Empty,
NewPassword: request.NewPassword ?? string.Empty);
await _dispatcher.Send(command);
return NoContent();
}
/// Resets a usuario's password (admin only). Returns a one-time temp password.
[HttpPost("{id:int}/password/reset")]
[RequirePermission("administracion:usuarios:gestionar")]
[ProducesResponseType(typeof(ResetUsuarioPasswordResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task ResetUsuarioPassword([FromRoute] int id)
{
var sub = User.FindFirstValue(JwtRegisteredClaimNames.Sub)
?? User.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new UnauthorizedAccessException();
var command = new ResetUsuarioPasswordCommand(
TargetId: id,
CallerId: int.Parse(sub));
var result = await _dispatcher.Send(command);
return Ok(result);
}
}
// ── request body records ──────────────────────────────────────────────────────
/// Create user request body — nullable to catch missing field scenarios.
public sealed record CreateUsuarioRequest(
string? Username,
string? Password,
string? Nombre,
string? Apellido,
string? Email,
string? Rol);
public sealed record UpdateUsuarioRequest(
string? Nombre,
string? Apellido,
string? Email,
string? Rol,
bool? Activo);
public sealed record ChangeMyPasswordRequest(
string? OldPassword,
string? NewPassword);