Files
PruebaGentle/design.md
dmolinari 7b9a7192c1 feat(authentication): implement frontend authentication system
- Created auth and user types (T10)
- Implemented API client with token handling (T11)
- Built AuthContext with JWT decoding (T12)
- Added ProtectedRoute component (T13)
- Created LoginPage, RegisterPage, DashboardPage (T14-T16)
- Updated App.tsx with routing and auth provider (T17)
- Added Dockerfile, nginx.conf for frontend deployment (T18-T19)
- Updated docker-compose.yml to include frontend service (T20)
- Updated .gitignore to exclude frontend build artifacts (T21)
- Removed unused App.css (T22)

Refs #2
2026-04-01 13:37:37 -03:00

9.7 KiB

Design: Autenticación Frontend + Endpoint Register

Technical Approach

Implementar un frontend React completo para autenticación (Login + Register + Dashboard protegido) desde cero, y agregar un endpoint público de registro al backend existente. El backend ya tiene JWT auth y CRUD completo — se reutilizan IUserRepository, IPasswordHasher, y el patrón de generación de JWT del AuthController existente.

El frontend sigue el patrón establecido en AGENT.md: Functional Components + Hooks, TypeScript estricto (sin any), Tailwind CSS exclusivo, API client centralizado.

Architecture Decisions

Decisión: Context API para Auth (no Redux)

Opción elegida: React Context + useAuth hook Alternativas: Redux Toolkit, Zustand, Jotai Rationale: Estado de auth es un único objeto (token + user) con baja frecuencia de cambio. Redux sería overkill para un estado tan simple. Context + hook es el patrón nativo de React, sin dependencias extra, y suficiente para auth state.

Decisión: localStorage para persistencia de token

Opción elegida: localStorage.getItem/setItem Alternativas: httpOnly cookies, sessionStorage, memory only Rationale: Simple y funcional para MVP. httpOnly cookies requerirían cambios en el backend (SameSite, CORS credentials). sessionStorage se pierde al cerrar la pestaña. localStorage sobrevive al refresh del navegador. El riesgo de XSS se acepta por simplicidad — en producción se migraría a httpOnly cookies.

Decisión: fetch nativo (no axios)

Opción elegida: fetch API wrapper con apiClient<T> Alternativas: axios, ky Rationale: fetch es nativo del browser, sin dependencias. El wrapper centralizado maneja headers, token injection, y error handling (incluyendo 401 redirect). Para el scope actual (2 endpoints de auth), no se necesita interceptor de axios.

Decisión: SP con THROW para duplicados (no validación en C#)

Opción elegida: Validar duplicados en sp_User_Register con THROW Alternativas: Validar en C# con queries previas (como hace UsersController.Create) Rationale: El UsersController actual hace 2 queries previas (GetByUsername + GetAll) para validar duplicados. Para el register, el SP puede hacerlo en una sola operación atómica. Esto es más eficiente y elimina race conditions. El SP lanza error 50001 (username) o 50002 (email), que el controller captura y mapea a 409.

Decisión: Multi-stage Docker con nginx

Opción elegida: Build stage (node) + Serve stage (nginx:alpine) Alternativas: Vite dev server en producción, serve package Rationale: nginx es el estándar para servir SPAs en producción. Multi-stage mantiene la imagen final ligera (~25MB vs ~500MB con node). nginx también maneja SPA routing (try_files), compresión gzip, y cache de assets estáticos.

Data Flow

Flujo de Login

LoginPage → authApi.login(dto) → fetch POST /api/auth/login
                                       │
                              ← { token, expiresAt }
                                       │
                           authContext.setToken()
                           localStorage.setItem('token')
                           JWT decode → setUser()
                           navigate('/dashboard')

Flujo de Register

RegisterPage → authApi.register(dto) → fetch POST /api/auth/register
                                            │
                                   ← { token, expiresAt, userId }
                                            │
                                authContext.setToken() (mismo que login)
                                navigate('/dashboard')

Flujo de ProtectedRoute

ProtectedRoute → authContext.isAuthenticated?
     │                    │
    NO ──────────────→ navigate('/login')
     │
    YES ──────→ <Outlet /> (renderiza la ruta hija)

Flujo de 401 (token expirado)

apiClient → fetch() → response.status === 401
                              │
                     localStorage.removeItem('token')
                     window.location.href = '/login'
                     throw Error('Sesión expirada')

File Changes

Backend (nuevos)

File Action Description
Backend/Sql/sp_User_Register.sql Create SP que valida duplicados y retorna usuario creado
Backend/PruebaGentle.Core/DTOs/RegisterDto.cs Create DTO: Username, Password, Email, NombreCompleto
Backend/PruebaGentle.Core/DTOs/RegisterResponseDto.cs Create DTO: Token, ExpiresAt, UserId

Backend (modificados)

File Action Description
Backend/PruebaGentle.API/Controllers/AuthController.cs Modify Agregar endpoint Register (+30 líneas)
Backend/PruebaGentle.Core/Interfaces/IUserRepository.cs Modify Agregar RegisterAsync(User user)
Backend/PruebaGentle.Infrastructure/Repositories/UserRepository.cs Modify Implementar RegisterAsync con sp_User_Register

Frontend (nuevos — todos)

File Action Description
Frontend/ (directorio completo) Create Bootstrap con Vite + React + TS
Frontend/src/types/auth.ts Create LoginRequest, RegisterRequest, AuthResponse, RegisterResponse
Frontend/src/types/user.ts Create User interface
Frontend/src/api/client.ts Create fetch wrapper con Bearer token + error handling
Frontend/src/hooks/useAuth.tsx Create AuthContext + useAuth hook
Frontend/src/components/ProtectedRoute.tsx Create Wrapper de ruta protegida
Frontend/src/pages/LoginPage.tsx Create Formulario de login
Frontend/src/pages/RegisterPage.tsx Create Formulario de registro
Frontend/src/pages/DashboardPage.tsx Create Dashboard con info de usuario + logout
Frontend/src/App.tsx Create Router principal
Frontend/src/main.tsx Create Entry point con AuthProvider
Frontend/Dockerfile Create Multi-stage build (node → nginx)
Frontend/nginx.conf Create Config nginx para SPA
Frontend/vite.config.ts Create Config Vite + Tailwind plugin

Configuración (modificados)

File Action Description
docker-compose.yml Modify Agregar servicio frontend
.gitignore Modify Agregar sección Node.js (node_modules/, dist/)

Interfaces / Contracts

Backend — DTOs

// RegisterDto.cs
public class RegisterDto
{
    public string Username { get; set; } = string.Empty;     // [Required], [MinLength(3)], [MaxLength(50)]
    public string Password { get; set; } = string.Empty;     // [Required], [MinLength(6)]
    public string Email { get; set; } = string.Empty;        // [Required], [EmailAddress]
    public string NombreCompleto { get; set; } = string.Empty; // [Required], [MaxLength(100)]
}

// RegisterResponseDto.cs
public class RegisterResponseDto
{
    public string Token { get; set; } = string.Empty;
    public DateTime ExpiresAt { get; set; }
    public int UserId { get; set; }
}

Frontend — Types

// src/types/auth.ts
export interface LoginRequest {
  username: string;
  password: string;
}

export interface RegisterRequest {
  username: string;
  password: string;
  email: string;
  nombreCompleto: string;
}

export interface AuthResponse {
  token: string;
  expiresAt: string;
}

export interface RegisterResponse extends AuthResponse {
  userId: number;
}

// src/types/user.ts
export interface User {
  id: number;
  username: string;
  email: string;
  nombreCompleto: string;
}

API Contract

POST /api/auth/register
  Request:  { username, password, email, nombreCompleto }
  Success:  200 { token, expiresAt, userId }
  Error:    409 { error: "Username already exists" }
  Error:    409 { error: "Email already exists" }
  Error:    400 { error: "..." }  // validation errors

Auth Context Shape

interface AuthContextType {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (credentials: LoginRequest) => Promise<void>;
  register: (data: RegisterRequest) => Promise<void>;
  logout: () => void;
}

Testing Strategy

Layer Qué testear Approach
Backend Register endpoint — duplicados username/email retornan 409 Manual con curl/Postman por ahora
Backend Register endpoint — datos válidos retornan token + userId Manual con curl/Postman
Frontend Flujo completo: Register → auto-login → Dashboard Manual (docker-compose up)
Frontend Login con credenciales inválidas muestra error Manual
Frontend ProtectedRoute redirige a /login sin token Manual
Frontend Logout limpia token y redirige a /login Manual

Nota: Tests automatizados fuera de alcance — framework no configurado aún en frontend.

Migration / Rollout

No se requiere migración de datos. El SP sp_User_Register es un nuevo objeto que no afecta tablas existentes. El rollout es:

  1. Deploy backend con nuevo endpoint (backward compatible — no rompe nada existente)
  2. Deploy frontend nuevo (no existía antes)
  3. docker-compose up levanta los 3 servicios (sqlserver, backend, frontend)

Rollback: eliminar servicio frontend de docker-compose.yml. Backend register endpoint puede dejarse sin efecto secundario.

Open Questions

  • ¿El SP debe retornar el PasswordHash en OUTPUT? (Actualmente sp_User_Create lo incluye — mejor excluirlo en register)
  • ¿Necesitamos validación de email único en el SP además de username? (Sí, según propuesta)
  • ¿El puerto del frontend en docker-compose debe ser 3000 o 80? (Propuesto: 3000:80)