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);