trading-platform/orchestration/analisis/OQI-001-CONTRATOS-API.md
Adrian Flores Cortes 76b0ced338 [TASK-002] docs: Auditoria comprehensiva frontend trading-platform
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>
2026-01-25 12:57:14 -06:00

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)*