Analisis exhaustivo CAPVED de 9 epics (OQI-001 a OQI-009) con: - 48 documentos generados (~19,000 lineas) - 122+ componentes analizados - 113 endpoints API mapeados - 30 gaps criticos identificados - Roadmap de implementacion (2,457h esfuerzo) - 9 subagentes en paralelo (2.5-3h vs 20h) Hallazgos principales: - 38% completitud promedio - 10 gaps bloqueantes (P0) - OQI-009 (MT4) 0% funcional - OQI-005 (Pagos) PCI-DSS non-compliant - Test coverage <10% Entregables: - EXECUTIVE-SUMMARY.md (reporte ejecutivo) - 02-ANALISIS.md (consolidado 9 epics) - 48 docs tecnicos por epic (componentes, APIs, gaps) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
871 lines
20 KiB
Markdown
871 lines
20 KiB
Markdown
# OQI-001: Contratos de API - Fundamentos-Auth
|
|
|
|
**Módulo:** OQI-001 (fundamentos-auth)
|
|
**Ruta:** `apps/backend/src/modules/auth/`
|
|
**Endpoints Analizados:** 14
|
|
**Fecha:** 2026-01-25
|
|
**Status:** ANÁLISIS COMPLETO
|
|
|
|
---
|
|
|
|
## ENDPOINTS CONSUMIDOS POR EL FRONTEND
|
|
|
|
### AUTENTICACIÓN BASE (4 Endpoints)
|
|
|
|
#### 1. POST /api/v1/auth/register
|
|
**Componente Consumidor:** Register.tsx (línea 46)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Registro de nuevo usuario
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
email: string // Requerido, formato email válido
|
|
password: string // Requerido, ≥8 chars, mayúscula, minúscula, número, especial
|
|
firstName: string // Requerido
|
|
lastName: string // Requerido
|
|
acceptTerms: boolean // Requerido (true)
|
|
}
|
|
```
|
|
|
|
**Response Schema (Success):**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
user: {
|
|
id: string
|
|
email: string
|
|
firstName: string
|
|
lastName: string
|
|
status: 'pending_verification' | 'active'
|
|
emailVerified: boolean
|
|
createdAt: string
|
|
}
|
|
tokens?: {
|
|
accessToken: string
|
|
refreshToken: string
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response Schema (Error):**
|
|
```typescript
|
|
{
|
|
success: false
|
|
error: string // Ej: "Email already registered"
|
|
errors?: [
|
|
{
|
|
field: string
|
|
message: string
|
|
}
|
|
]
|
|
statusCode: number // 400, 409, 422, etc
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
if (!response.ok) {
|
|
throw new Error(data.error || data.errors?.[0]?.message || 'Error al registrar')
|
|
}
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `201 Created` - Registro exitoso
|
|
- `400 Bad Request` - Validación fallida
|
|
- `409 Conflict` - Email ya registrado
|
|
- `422 Unprocessable Entity` - Datos inválidos
|
|
|
|
---
|
|
|
|
#### 2. POST /api/v1/auth/login
|
|
**Componente Consumidor:** Login.tsx (línea 31)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Inicio de sesión con email/contraseña
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
email: string // Requerido
|
|
password: string // Requerido
|
|
totpCode?: string // Opcional, 6 dígitos si 2FA está habilitado
|
|
rememberMe?: boolean // Opcional, default false
|
|
}
|
|
```
|
|
|
|
**Response Schema (Sin 2FA):**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
user: {
|
|
id: string
|
|
email: string
|
|
displayName: string
|
|
mfaEnabled: boolean
|
|
status: 'active'
|
|
lastLoginAt: string
|
|
}
|
|
tokens: {
|
|
accessToken: string
|
|
refreshToken: string
|
|
expiresIn: number // Segundos
|
|
tokenType: 'Bearer'
|
|
}
|
|
session: {
|
|
id: string
|
|
ipAddress: string
|
|
userAgent: string
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response Schema (Requiere 2FA):**
|
|
```typescript
|
|
{
|
|
success: true
|
|
requiresTwoFactor: true
|
|
data: {
|
|
tempToken: string // Token temporal para verificar TOTP
|
|
}
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Error al iniciar sesion')
|
|
}
|
|
|
|
if (data.requiresTwoFactor) {
|
|
setRequiresTwoFactor(true) // Mostrar input TOTP
|
|
return
|
|
}
|
|
|
|
// Almacenar tokens
|
|
localStorage.setItem('accessToken', data.data.tokens.accessToken)
|
|
localStorage.setItem('refreshToken', data.data.tokens.refreshToken)
|
|
navigate('/dashboard')
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Login exitoso
|
|
- `401 Unauthorized` - Credenciales incorrectas
|
|
- `403 Forbidden` - Cuenta desactivada/suspendida
|
|
- `429 Too Many Requests` - Rate limiting
|
|
|
|
---
|
|
|
|
#### 3. POST /api/v1/auth/logout
|
|
**Componente Consumidor:** SessionsList.tsx + DeviceCard.tsx (revoke sesión actual)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Cierre de sesión actual
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
// Vacío, auth via Bearer token
|
|
}
|
|
```
|
|
|
|
**Headers Requeridos:**
|
|
```
|
|
Authorization: Bearer <accessToken>
|
|
```
|
|
|
|
**Response Schema:**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
message: "Logged out successfully"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En authService.logout()
|
|
localStorage.removeItem('token')
|
|
localStorage.removeItem('refreshToken')
|
|
// Redirect a /login
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Logout exitoso
|
|
- `401 Unauthorized` - Token inválido
|
|
|
|
---
|
|
|
|
#### 4. POST /api/v1/auth/logout-all
|
|
**Componente Consumidor:** SessionsList.tsx - Botón "Sign Out All Devices"
|
|
**Método HTTP:** POST
|
|
**Descripción:** Cierre de todas las sesiones excepto la actual (se cierra actual también)
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
// Vacío, auth via Bearer token
|
|
}
|
|
```
|
|
|
|
**Headers Requeridos:**
|
|
```
|
|
Authorization: Bearer <accessToken>
|
|
```
|
|
|
|
**Response Schema:**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
message: "Signed out from 3 devices" // Count de sesiones cerradas
|
|
count: number
|
|
}
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En authService.revokeAllSessions()
|
|
try {
|
|
const response = await api.post('/auth/logout-all')
|
|
const count = parseInt(response.data.message?.match(/\d+/)?.[0] || '0')
|
|
} catch (error) {
|
|
throw new Error('Failed to revoke all sessions')
|
|
}
|
|
// Después: localStorage.removeItem, redirect /login
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Logout exitoso
|
|
- `401 Unauthorized` - No autenticado
|
|
|
|
---
|
|
|
|
### EMAIL & CONTRASEÑA (3 Endpoints)
|
|
|
|
#### 5. POST /api/v1/auth/verify-email
|
|
**Componente Consumidor:** VerifyEmail.tsx (línea 23)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Verificar email con token de confirmación
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
token: string // Requerido, token de verificación (query param)
|
|
}
|
|
```
|
|
|
|
**Response Schema (Success):**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
message: "Email verified successfully"
|
|
user: {
|
|
id: string
|
|
email: string
|
|
emailVerified: true
|
|
status: 'active'
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response Schema (Error):**
|
|
```typescript
|
|
{
|
|
success: false
|
|
error: string // "Invalid or expired token"
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En VerifyEmail.tsx useEffect()
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Error al verificar email')
|
|
}
|
|
setStatus('success') // Mostrar CheckCircle
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Verificación exitosa
|
|
- `400 Bad Request` - Token inválido/expirado
|
|
- `409 Conflict` - Email ya verificado
|
|
|
|
---
|
|
|
|
#### 6. POST /api/v1/auth/forgot-password
|
|
**Componente Consumidor:** ForgotPassword.tsx (línea 17)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Solicitar enlace de recuperación de contraseña
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
email: string // Requerido
|
|
}
|
|
```
|
|
|
|
**Response Schema:**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
message: "If account exists, reset link will be sent to email"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Comportamiento de Seguridad:**
|
|
- SIEMPRE retorna `200 OK`, incluso si email no existe (privacy)
|
|
- Envía email solo si cuenta existe
|
|
- Link expira en 1 hora (típico)
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En ForgotPassword.tsx
|
|
const data = await response.json()
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Error al enviar el email')
|
|
}
|
|
setSubmitted(true) // Mostrar mensaje "Revisa tu Email"
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Solicitud procesada (incluso si email no existe)
|
|
- `429 Too Many Requests` - Rate limiting
|
|
|
|
---
|
|
|
|
#### 7. POST /api/v1/auth/reset-password
|
|
**Componente Consumidor:** ResetPassword.tsx (línea 47)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Establecer nueva contraseña con token
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
token: string // Requerido (query param)
|
|
password: string // Requerido, mismas reglas que Register
|
|
}
|
|
```
|
|
|
|
**Response Schema (Success):**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
message: "Password reset successfully"
|
|
user: {
|
|
id: string
|
|
email: string
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response Schema (Error):**
|
|
```typescript
|
|
{
|
|
success: false
|
|
error: string // "Invalid or expired token" / "Password too weak"
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En ResetPassword.tsx
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Error al restablecer contrasena')
|
|
}
|
|
setStatus('success')
|
|
// Después puede iniciar sesión con nueva contraseña
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Restablecimiento exitoso
|
|
- `400 Bad Request` - Token inválido/expirado
|
|
- `422 Unprocessable Entity` - Contraseña débil
|
|
|
|
---
|
|
|
|
### TELÉFONO & OTP (2 Endpoints)
|
|
|
|
#### 8. POST /api/v1/auth/phone/send-otp
|
|
**Componente Consumidor:** PhoneLoginForm.tsx (línea 42)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Enviar código OTP por SMS/WhatsApp
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
phoneNumber: string // Requerido, sin caracteres especiales
|
|
countryCode: string // Requerido, ej: "52", "1", "57"
|
|
channel: 'sms' | 'whatsapp' // Requerido
|
|
}
|
|
```
|
|
|
|
**Response Schema (Success):**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
message: "OTP sent successfully"
|
|
expiresAt: string // ISO timestamp, típicamente +10 mins
|
|
maskedPhone: string // "55 1234 5678"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response Schema (Error):**
|
|
```typescript
|
|
{
|
|
success: false
|
|
error: string // "Invalid phone number" / "SMS provider error"
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En PhoneLoginForm.tsx handleSendOTP()
|
|
const response = await fetch('/api/v1/auth/phone/send-otp', {...})
|
|
const data = await response.json()
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Error al enviar el codigo')
|
|
}
|
|
setOtpExpiresAt(new Date(data.data.expiresAt))
|
|
setStep('otp') // Cambiar a pantalla de verificación
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - OTP enviado
|
|
- `400 Bad Request` - Número inválido
|
|
- `429 Too Many Requests` - Rate limiting
|
|
- `503 Service Unavailable` - Proveedor SMS/WhatsApp inactivo
|
|
|
|
---
|
|
|
|
#### 9. POST /api/v1/auth/phone/verify-otp
|
|
**Componente Consumidor:** PhoneLoginForm.tsx (línea 73)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Verificar código OTP y obtener tokens
|
|
|
|
**Request Schema:**
|
|
```typescript
|
|
{
|
|
phoneNumber: string // Requerido
|
|
countryCode: string // Requerido
|
|
otpCode: string // Requerido, exactamente 6 dígitos
|
|
}
|
|
```
|
|
|
|
**Response Schema (Success):**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
tokens: {
|
|
accessToken: string
|
|
refreshToken: string
|
|
expiresIn: number
|
|
tokenType: 'Bearer'
|
|
}
|
|
user: {
|
|
id: string
|
|
phone: string
|
|
phoneVerified: true
|
|
status: 'active'
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response Schema (Error):**
|
|
```typescript
|
|
{
|
|
success: false
|
|
error: string // "Invalid OTP" / "OTP expired" / "Invalid phone number"
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En PhoneLoginForm.tsx handleVerifyOTP()
|
|
const response = await fetch('/api/v1/auth/phone/verify-otp', {...})
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Codigo invalido')
|
|
}
|
|
localStorage.setItem('accessToken', data.data.tokens.accessToken)
|
|
localStorage.setItem('refreshToken', data.data.tokens.refreshToken)
|
|
navigate('/dashboard')
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Verificación exitosa
|
|
- `400 Bad Request` - OTP inválido
|
|
- `401 Unauthorized` - OTP expirado
|
|
- `429 Too Many Requests` - Demasiados intentos fallidos
|
|
|
|
---
|
|
|
|
### OAUTH (3+ Endpoints)
|
|
|
|
#### 10. GET /api/v1/auth/{provider}
|
|
**Componente Consumidor:** SocialLoginButtons.tsx (línea 65)
|
|
**Método HTTP:** GET
|
|
**Descripción:** Obtener URL de OAuth del proveedor
|
|
|
|
**Proveedores Soportados:**
|
|
- `google`
|
|
- `facebook`
|
|
- `twitter`
|
|
- `apple`
|
|
- `github`
|
|
|
|
**Query Parameters:**
|
|
```
|
|
?returnUrl=/login // URL a redirigir después del callback
|
|
```
|
|
|
|
**Response Schema:**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
authUrl: string // URL de OAuth del proveedor
|
|
state: string // State token para CSRF protection
|
|
codeChallenge?: string // PKCE
|
|
}
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En SocialLoginButtons.tsx handleSocialLogin()
|
|
const response = await fetch(
|
|
`/api/v1/auth/${provider}?returnUrl=${encodeURIComponent(window.location.pathname)}`
|
|
)
|
|
const data = await response.json()
|
|
if (data.success && data.data.authUrl) {
|
|
window.location.href = data.data.authUrl
|
|
}
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - URL generada
|
|
- `400 Bad Request` - Proveedor inválido
|
|
|
|
---
|
|
|
|
#### 11. POST /api/v1/auth/{provider}/callback
|
|
**Componente Consumidor:** AuthCallback.tsx (procesado por backend)
|
|
**Método HTTP:** POST (redirección GET desde proveedor)
|
|
**Descripción:** Callback post-OAuth, procesa code y obtiene tokens
|
|
|
|
**Query Parameters (desde proveedor OAuth):**
|
|
```
|
|
?code=authorization_code
|
|
&state=state_token
|
|
&error=access_denied (si usuario deniega)
|
|
```
|
|
|
|
**Response Schema (en AuthCallback.tsx query params):**
|
|
```
|
|
/auth/callback?
|
|
accessToken=xxx
|
|
&refreshToken=xxx
|
|
&isNewUser=true|false
|
|
&returnUrl=/dashboard
|
|
&error=error_code
|
|
```
|
|
|
|
**Error Codes Posibles:**
|
|
- `invalid_state` - CSRF detection fallido
|
|
- `oauth_failed` - Conexión con proveedor falló
|
|
- `oauth_error` - Error genérico OAuth
|
|
- `missing_code_verifier` - PKCE validation fallida
|
|
- `invalid_provider` - Proveedor no registrado
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En AuthCallback.tsx useEffect()
|
|
if (errorParam) {
|
|
setStatus('error')
|
|
setError(getErrorMessage(errorParam))
|
|
return
|
|
}
|
|
|
|
if (accessToken && refreshToken) {
|
|
localStorage.setItem('accessToken', accessToken)
|
|
localStorage.setItem('refreshToken', refreshToken)
|
|
setStatus('success')
|
|
setTimeout(() => {
|
|
if (isNewUser) navigate('/onboarding')
|
|
else navigate(returnUrl)
|
|
}, 1500)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### SESIONES (3 Endpoints)
|
|
|
|
#### 12. GET /api/v1/auth/sessions
|
|
**Componente Consumidor:** SessionsList.tsx (línea 32, en store)
|
|
**Método HTTP:** GET
|
|
**Descripción:** Obtener todas las sesiones activas del usuario
|
|
|
|
**Headers Requeridos:**
|
|
```
|
|
Authorization: Bearer <accessToken>
|
|
```
|
|
|
|
**Response Schema:**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: [
|
|
{
|
|
id: string // UUID
|
|
userAgent: string // User-Agent header
|
|
ipAddress: string // IP origen
|
|
createdAt: string // ISO timestamp
|
|
lastActiveAt: string // ISO timestamp
|
|
isCurrent: boolean // True si es la sesión actual
|
|
deviceId?: string
|
|
},
|
|
...
|
|
]
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En authService.getSessions()
|
|
try {
|
|
const response = await api.get('/auth/sessions')
|
|
return response.data.data || []
|
|
} catch (error) {
|
|
throw new Error('Failed to fetch active sessions')
|
|
}
|
|
```
|
|
|
|
**Consumido por:** SessionsList.tsx a través de Zustand store
|
|
- useSessionsStore.fetchSessions() llamado en useEffect
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Sesiones obtenidas
|
|
- `401 Unauthorized` - No autenticado
|
|
|
|
---
|
|
|
|
#### 13. DELETE /api/v1/auth/sessions/{sessionId}
|
|
**Componente Consumidor:** DeviceCard.tsx (línea 150, a través de store)
|
|
**Método HTTP:** DELETE
|
|
**Descripción:** Revocar una sesión específica
|
|
|
|
**Headers Requeridos:**
|
|
```
|
|
Authorization: Bearer <accessToken>
|
|
```
|
|
|
|
**Path Parameters:**
|
|
```
|
|
sessionId: string // UUID de la sesión
|
|
```
|
|
|
|
**Response Schema:**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
message: "Session revoked successfully"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Manejo de Error (Frontend):**
|
|
```javascript
|
|
// En authService.revokeSession()
|
|
try {
|
|
await api.delete(`/auth/sessions/${sessionId}`)
|
|
} catch (error) {
|
|
throw new Error('Failed to revoke session')
|
|
}
|
|
|
|
// En store: remueve de lista
|
|
set({
|
|
sessions: state.sessions.filter(s => s.id !== sessionId)
|
|
})
|
|
```
|
|
|
|
**Caso Especial - Sesión Actual:**
|
|
Si sessionId es la sesión actual:
|
|
- Backend cierra la sesión
|
|
- Frontend detecta en store (isCurrent === true)
|
|
- Llama a logout() + redirect /login
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Revocación exitosa
|
|
- `401 Unauthorized` - No autenticado
|
|
- `404 Not Found` - Sesión no existe
|
|
|
|
---
|
|
|
|
### 2FA / SEGURIDAD (2+ Endpoints - Parcialmente Implementados)
|
|
|
|
#### 14. POST /api/v1/auth/2fa/setup
|
|
**Componente Consumidor:** SecuritySettings.tsx - Tab "two-factor" (SIN IMPLEMENTAR)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Generar QR y secreto para 2FA
|
|
|
|
**Nota:** Botón "Setup" existe pero no tiene handler implementado
|
|
|
|
**Request Schema Esperado:**
|
|
```typescript
|
|
{
|
|
method: 'totp' | 'sms'
|
|
}
|
|
```
|
|
|
|
**Response Schema Esperado:**
|
|
```typescript
|
|
{
|
|
success: true
|
|
data: {
|
|
secret: string // Base32 para app authenticator
|
|
qrCode: string // URL de QR (data:image/png...)
|
|
backupCodes?: string[] // Códigos de backup
|
|
}
|
|
}
|
|
```
|
|
|
|
**Estados HTTP Esperados:**
|
|
- `200 OK` - Setup generado
|
|
- `401 Unauthorized` - No autenticado
|
|
|
|
---
|
|
|
|
#### 15. POST /api/v1/auth/2fa/enable
|
|
**Componente Consumidor:** SecuritySettings.tsx - Tab "two-factor" (SIN IMPLEMENTAR)
|
|
**Método HTTP:** POST
|
|
**Descripción:** Activar 2FA tras escanear QR
|
|
|
|
**Nota:** Endpoint no llamado desde frontend actual
|
|
|
|
**Request Schema Esperado:**
|
|
```typescript
|
|
{
|
|
method: 'totp' | 'sms'
|
|
verificationCode: string // Código de 6 dígitos
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## RESUMEN DE CONTRATOS
|
|
|
|
| # | Endpoint | Método | Estado | Componente | Autenticación |
|
|
|---|----------|--------|--------|-----------|---|
|
|
| 1 | /auth/register | POST | ✓ | Register.tsx | ✗ |
|
|
| 2 | /auth/login | POST | ✓ | Login.tsx | ✗ |
|
|
| 3 | /auth/logout | POST | ✓ | SessionsList/DeviceCard | ✓ |
|
|
| 4 | /auth/logout-all | POST | ✓ | SessionsList | ✓ |
|
|
| 5 | /auth/verify-email | POST | ✓ | VerifyEmail.tsx | ✗ |
|
|
| 6 | /auth/forgot-password | POST | ✓ | ForgotPassword.tsx | ✗ |
|
|
| 7 | /auth/reset-password | POST | ✓ | ResetPassword.tsx | ✗ |
|
|
| 8 | /auth/phone/send-otp | POST | ✓ | PhoneLoginForm | ✗ |
|
|
| 9 | /auth/phone/verify-otp | POST | ✓ | PhoneLoginForm | ✗ |
|
|
| 10 | /auth/{provider} | GET | ✓ | SocialLoginButtons | ✗ |
|
|
| 11 | /auth/{provider}/callback | POST | ✓ | AuthCallback | ✗ |
|
|
| 12 | /auth/sessions | GET | ✓ | SessionsList (store) | ✓ |
|
|
| 13 | /auth/sessions/{id} | DELETE | ✓ | DeviceCard (store) | ✓ |
|
|
| 14 | /auth/2fa/setup | POST | ✗ | SecuritySettings (TODO) | ✓ |
|
|
| 15 | /auth/2fa/enable | POST | ✗ | SecuritySettings (TODO) | ✓ |
|
|
|
|
**Leyenda:**
|
|
- ✓ = Implementado
|
|
- ✗ = No implementado / TODO
|
|
- Autenticación: ✓ = Requiere Bearer token
|
|
|
|
---
|
|
|
|
## PATRONES DE MANEJO DE ERRORES
|
|
|
|
### Patrón 1: Formularios (Register, Login)
|
|
```javascript
|
|
try {
|
|
const response = await fetch('/api/v1/auth/...', {...})
|
|
const data = await response.json()
|
|
if (!response.ok) {
|
|
throw new Error(data.error || data.errors?.[0]?.message || 'Error genérico')
|
|
}
|
|
// Éxito
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Error desconocido')
|
|
}
|
|
```
|
|
|
|
### Patrón 2: Store (Zustand)
|
|
```javascript
|
|
try {
|
|
await authService.action()
|
|
set({ /* updated state */ })
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : 'Mensaje por defecto'
|
|
set({ error: errorMessage })
|
|
throw error // Re-lanzar para capturador superior
|
|
}
|
|
```
|
|
|
|
### Patrón 3: Llamadas Automáticas (VerifyEmail)
|
|
```javascript
|
|
useEffect(() => {
|
|
if (!token) {
|
|
setStatus('no-token')
|
|
return
|
|
}
|
|
verifyEmail(token)
|
|
}, [token])
|
|
```
|
|
|
|
---
|
|
|
|
## CONFIGURACIÓN DE API
|
|
|
|
**Base URL:** `import.meta.env?.VITE_API_URL || '/api/v1'`
|
|
|
|
**Axios Interceptors (auth.service.ts):**
|
|
1. **Request:** Agrega `Authorization: Bearer <token>` si localStorage tiene token
|
|
2. **Response:** En error 401, limpia localStorage y redirige a /login
|
|
|
|
**Headers por Defecto:**
|
|
```javascript
|
|
'Content-Type': 'application/json'
|
|
'Authorization': 'Bearer <accessToken>' (si aplica)
|
|
```
|
|
|
|
---
|
|
|
|
## NOTAS CRÍTICAS
|
|
|
|
1. **Rate Limiting:** forgotten-password y phone/send-otp implementan rate limiting
|
|
2. **CSRF:** OAuth usa state token (generado en backend)
|
|
3. **PKCE:** OAuth puede usar PKCE (code_challenge / code_verifier)
|
|
4. **Token Refresh:** Interceptor responde a 401 pero no hay refresh endpoint consumido desde componentes
|
|
5. **Seguridad Privacidad:** forgot-password NO indica si email existe
|
|
6. **OTP Expiración:** Típicamente 10 minutos
|
|
7. **Email Verification:** Token típicamente expira en 24-48 horas
|
|
8. **Password Reset:** Token expira en 1 hora
|
|
|
|
---
|
|
|
|
*Análisis completado: 2026-01-25*
|
|
*15 endpoints documentados (13 implementados, 2 TODO)*
|