UDT-003: Registro de Usuarios (admin-only) + fix JWT claim mapping #4

Merged
dmolinari merged 3 commits from feature/UDT-003 into main 2026-04-15 14:23:53 +00:00
Owner

Summary

Implementa UDT-003: Registro de Usuarios siguiendo flujo SDD completo (explore → proposal → spec → design → tasks → apply → verify → archive) en modo Strict TDD.

  • POST /api/v1/users admin-only ([Authorize(Roles="admin")]) con BCrypt cost 12, password policy configurable, mapeo UQ 2627 + UsernameAlreadyExistsException → 409.
  • Frontend React: ruta /usuarios/nuevo con guard admin, UserForm (RHF + Zod), useCreateUser (TanStack Query), nav link admin-only.
  • Fix colateral: JwtBearerOptions.MapInboundClaims=false — desbloquea Login_Refresh_Logout_FullFlow que quedó rojo desde UDT-002 (middleware mapeaba sub → ClaimTypes.NameIdentifier y rompía User.FindFirst("sub")).

Commits

  • 3d598fa feat(api): UDT-003 registro de usuarios — backend completo (Phases 1-6)
  • dd99e5c feat(web): UDT-003 formulario de alta de usuarios (admin)
  • bce591e fix(auth): preserve JWT claim names in bearer middleware

Requirements cubiertos (5/5)

# Requirement Estado
1 Admin-only Access Control
2 Request Validation (FluentValidation + Zod)
3 Unique Username (409)
4 Password Hashing (BCrypt cost 12)
5 Successful Creation Response (201 sin PasswordHash)

Test plan

  • dotnet build src/src.sln → 0 errores, 0 warnings
  • dotnet test tests/SIGCM2.Application.Tests107/107 PASSED
  • dotnet test tests/SIGCM2.Api.Tests15/15 PASSED (incluye 7 nuevos UDT-003 + el flow completo login/refresh/logout ya en verde)
  • cd src/web && npx tsc --noEmit → clean
  • npx vitest run39/39 PASSED (12 nuevos UDT-003)
  • Smoke manual: login admin → crear usuario en /usuarios/nuevo → login con el usuario recién creado

Archivos destacados

Backend nuevos

  • src/api/SIGCM2.Application/Usuarios/Create/ — Command + Handler + Validator + DTO
  • src/api/SIGCM2.Domain/Exceptions/UsernameAlreadyExistsException.cs
  • src/api/SIGCM2.Api/Controllers/UsuariosController.cs
  • tests/SIGCM2.Api.Tests/Usuarios/CreateUsuarioEndpointTests.cs

Backend modificados

  • src/api/SIGCM2.Domain/Entities/Usuario.cs — factory ForCreation
  • src/api/SIGCM2.Application/Abstractions/Persistence/IUsuarioRepository.cs+AddAsync, +ExistsByUsernameAsync
  • src/api/SIGCM2.Infrastructure/Persistence/UsuarioRepository.cs — impl Dapper
  • src/api/SIGCM2.Infrastructure/DependencyInjection.csRoleClaimType="rol", MapInboundClaims=false, NameClaimType="name"
  • src/api/SIGCM2.Api/Filters/ExceptionFilter.cs — +409 handlers
  • tests/SIGCM2.TestSupport/TestWebAppFactory.cs — override SqlConnectionFactory singleton con ConfigureTestServices

Frontend nuevos

  • src/web/src/features/users/{api,hooks,components,pages}/
  • src/web/src/tests/features/users/

Frontend modificados

  • src/web/src/router.tsx — ruta /usuarios/nuevo
  • src/web/src/components/layout/AppSidebar.tsx — link admin-only

Decisiones arquitecturales

  1. Scope estricto: role gate nativo [Authorize(Roles="admin")]; RBAC granular se difiere a UDT-005/006.
  2. Nueva carpeta Usuarios/ separada de Auth/ (alta es gestión de usuarios, no auth).
  3. ExceptionFilter mapea SqlException 2627 + UsernameAlreadyExistsException → 409.
  4. Password policy en AuthOptions (min 8, letra + dígito por default).

Deuda / follow-ups

Registrados como issues con label followup (no dependen de este PR body para sobrevivir):

  • #5 [UDT-005/006] Crear ProtectedRoute reutilizable con rol-check en frontend
  • #6 [Auditoría] Registrar admin creador en alta de usuarios

Gap de roadmap detectado: agregado UDT-008: Gestión completa de usuarios en STATUS.md (list / edit / deactivate / password-change / reset; requiere UDT-006 cerrado).

Engram artifact trail

sdd/udt-003-registro-usuarios/{explore,proposal,spec,design,tasks,apply-progress,verify-report,archive-report} (IDs en archive-report).

## Summary Implementa **UDT-003: Registro de Usuarios** siguiendo flujo SDD completo (explore → proposal → spec → design → tasks → apply → verify → archive) en modo Strict TDD. - `POST /api/v1/users` admin-only (`[Authorize(Roles="admin")]`) con BCrypt cost 12, password policy configurable, mapeo UQ 2627 + `UsernameAlreadyExistsException` → 409. - Frontend React: ruta `/usuarios/nuevo` con guard admin, `UserForm` (RHF + Zod), `useCreateUser` (TanStack Query), nav link admin-only. - Fix colateral: `JwtBearerOptions.MapInboundClaims=false` — desbloquea `Login_Refresh_Logout_FullFlow` que quedó rojo desde UDT-002 (middleware mapeaba `sub → ClaimTypes.NameIdentifier` y rompía `User.FindFirst("sub")`). ## Commits - `3d598fa` feat(api): UDT-003 registro de usuarios — backend completo (Phases 1-6) - `dd99e5c` feat(web): UDT-003 formulario de alta de usuarios (admin) - `bce591e` fix(auth): preserve JWT claim names in bearer middleware ## Requirements cubiertos (5/5) | # | Requirement | Estado | |---|-------------|--------| | 1 | Admin-only Access Control | ✅ | | 2 | Request Validation (FluentValidation + Zod) | ✅ | | 3 | Unique Username (409) | ✅ | | 4 | Password Hashing (BCrypt cost 12) | ✅ | | 5 | Successful Creation Response (201 sin PasswordHash) | ✅ | ## Test plan - [x] `dotnet build src/src.sln` → 0 errores, 0 warnings - [x] `dotnet test tests/SIGCM2.Application.Tests` → **107/107 PASSED** - [x] `dotnet test tests/SIGCM2.Api.Tests` → **15/15 PASSED** (incluye 7 nuevos UDT-003 + el flow completo login/refresh/logout ya en verde) - [x] `cd src/web && npx tsc --noEmit` → clean - [x] `npx vitest run` → **39/39 PASSED** (12 nuevos UDT-003) - [x] Smoke manual: login admin → crear usuario en `/usuarios/nuevo` → login con el usuario recién creado ✅ ## Archivos destacados **Backend nuevos** - `src/api/SIGCM2.Application/Usuarios/Create/` — Command + Handler + Validator + DTO - `src/api/SIGCM2.Domain/Exceptions/UsernameAlreadyExistsException.cs` - `src/api/SIGCM2.Api/Controllers/UsuariosController.cs` - `tests/SIGCM2.Api.Tests/Usuarios/CreateUsuarioEndpointTests.cs` **Backend modificados** - `src/api/SIGCM2.Domain/Entities/Usuario.cs` — factory `ForCreation` - `src/api/SIGCM2.Application/Abstractions/Persistence/IUsuarioRepository.cs` — `+AddAsync`, `+ExistsByUsernameAsync` - `src/api/SIGCM2.Infrastructure/Persistence/UsuarioRepository.cs` — impl Dapper - `src/api/SIGCM2.Infrastructure/DependencyInjection.cs` — `RoleClaimType="rol"`, `MapInboundClaims=false`, `NameClaimType="name"` - `src/api/SIGCM2.Api/Filters/ExceptionFilter.cs` — +409 handlers - `tests/SIGCM2.TestSupport/TestWebAppFactory.cs` — override `SqlConnectionFactory` singleton con `ConfigureTestServices` **Frontend nuevos** - `src/web/src/features/users/{api,hooks,components,pages}/` - `src/web/src/tests/features/users/` **Frontend modificados** - `src/web/src/router.tsx` — ruta `/usuarios/nuevo` - `src/web/src/components/layout/AppSidebar.tsx` — link admin-only ## Decisiones arquitecturales 1. Scope estricto: role gate nativo `[Authorize(Roles="admin")]`; RBAC granular se difiere a UDT-005/006. 2. Nueva carpeta `Usuarios/` separada de `Auth/` (alta es gestión de usuarios, no auth). 3. `ExceptionFilter` mapea `SqlException 2627` + `UsernameAlreadyExistsException` → 409. 4. Password policy en `AuthOptions` (min 8, letra + dígito por default). ## Deuda / follow-ups Registrados como issues con label `followup` (no dependen de este PR body para sobrevivir): - #5 `[UDT-005/006] Crear ProtectedRoute reutilizable con rol-check en frontend` - #6 `[Auditoría] Registrar admin creador en alta de usuarios` Gap de roadmap detectado: agregado `UDT-008: Gestión completa de usuarios` en `STATUS.md` (list / edit / deactivate / password-change / reset; requiere UDT-006 cerrado). ## Engram artifact trail `sdd/udt-003-registro-usuarios/{explore,proposal,spec,design,tasks,apply-progress,verify-report,archive-report}` (IDs en `archive-report`).
dmolinari added 3 commits 2026-04-15 14:06:18 +00:00
- Domain: Usuario.ForCreation factory, UsernameAlreadyExistsException, IUsuarioRepository extendido
- Application: CreateUsuarioCommand/Validator/Handler, UsuarioCreatedDto, AuthOptions password policy
- Infrastructure: UsuarioRepository.ExistsByUsernameAsync + AddAsync (INSERT OUTPUT INSERTED.Id), RoleClaimType="rol" en TokenValidationParameters
- Api: UsuariosController POST api/v1/users [Authorize(Roles="admin")], ExceptionFilter mapea UsernameAlreadyExistsException + SqlException 2627 → 409
- Tests (unit): 43 tests — 33 validator + 10 handler (107 total, green)
- Tests (integration): 7 tests CreateUsuarioEndpoint — 401/403/400/201/409/race/e2e (green)
- Fix: TestWebAppFactory.ConfigureTestServices reemplaza SqlConnectionFactory singleton con CS de test correcto
Agrega CreateUserPage con UserForm (react-hook-form + Zod), hook useCreateUser
(TanStack Query mutation), ruta /users/new protegida y entrada en AppSidebar.
Incluye tests Vitest: UserForm (9 casos) y useCreateUser (3 casos).
JwtBearerOptions.MapInboundClaims defaulted to true, which mapped the
'sub' claim to ClaimTypes.NameIdentifier in HttpContext.User. Logout
endpoint read User.FindFirst("sub") and got null, returning 401 for
any authenticated caller.

Fix: set MapInboundClaims=false and pin NameClaimType="name" so the
JWT claims land in the principal with their original names, aligning
with how JwtService.GetPrincipalFromExpiredToken (used by refresh)
already consumes them.

Unblocks Login_Refresh_Logout_FullFlow integration test (15/15 green).
dmolinari merged commit 890da06f71 into main 2026-04-15 14:23:53 +00:00
dmolinari deleted branch feature/UDT-003 2026-04-15 14:23:53 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dmolinari/SIG-CM2.0#4