- 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
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:
- Deploy backend con nuevo endpoint (backward compatible — no rompe nada existente)
- Deploy frontend nuevo (no existía antes)
- 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_Createlo 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)