Files

241 lines
9.7 KiB
Markdown
Raw Permalink Normal View History

# 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
```csharp
// 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
```typescript
// 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
```typescript
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)