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

20 KiB

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:

{
  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):

{
  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):

{
  success: false
  error: string              // Ej: "Email already registered"
  errors?: [
    {
      field: string
      message: string
    }
  ]
  statusCode: number         // 400, 409, 422, etc
}

Manejo de Error (Frontend):

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:

{
  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):

{
  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):

{
  success: true
  requiresTwoFactor: true
  data: {
    tempToken: string       // Token temporal para verificar TOTP
  }
}

Manejo de Error (Frontend):

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:

{
  // Vacío, auth via Bearer token
}

Headers Requeridos:

Authorization: Bearer <accessToken>

Response Schema:

{
  success: true
  data: {
    message: "Logged out successfully"
  }
}

Manejo de Error (Frontend):

// 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:

{
  // Vacío, auth via Bearer token
}

Headers Requeridos:

Authorization: Bearer <accessToken>

Response Schema:

{
  success: true
  data: {
    message: "Signed out from 3 devices"  // Count de sesiones cerradas
    count: number
  }
}

Manejo de Error (Frontend):

// 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:

{
  token: string             // Requerido, token de verificación (query param)
}

Response Schema (Success):

{
  success: true
  data: {
    message: "Email verified successfully"
    user: {
      id: string
      email: string
      emailVerified: true
      status: 'active'
    }
  }
}

Response Schema (Error):

{
  success: false
  error: string             // "Invalid or expired token"
}

Manejo de Error (Frontend):

// 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:

{
  email: string             // Requerido
}

Response Schema:

{
  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):

// 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:

{
  token: string             // Requerido (query param)
  password: string          // Requerido, mismas reglas que Register
}

Response Schema (Success):

{
  success: true
  data: {
    message: "Password reset successfully"
    user: {
      id: string
      email: string
    }
  }
}

Response Schema (Error):

{
  success: false
  error: string             // "Invalid or expired token" / "Password too weak"
}

Manejo de Error (Frontend):

// 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:

{
  phoneNumber: string       // Requerido, sin caracteres especiales
  countryCode: string       // Requerido, ej: "52", "1", "57"
  channel: 'sms' | 'whatsapp'  // Requerido
}

Response Schema (Success):

{
  success: true
  data: {
    message: "OTP sent successfully"
    expiresAt: string       // ISO timestamp, típicamente +10 mins
    maskedPhone: string     // "55 1234 5678"
  }
}

Response Schema (Error):

{
  success: false
  error: string             // "Invalid phone number" / "SMS provider error"
}

Manejo de Error (Frontend):

// 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:

{
  phoneNumber: string       // Requerido
  countryCode: string       // Requerido
  otpCode: string           // Requerido, exactamente 6 dígitos
}

Response Schema (Success):

{
  success: true
  data: {
    tokens: {
      accessToken: string
      refreshToken: string
      expiresIn: number
      tokenType: 'Bearer'
    }
    user: {
      id: string
      phone: string
      phoneVerified: true
      status: 'active'
    }
  }
}

Response Schema (Error):

{
  success: false
  error: string             // "Invalid OTP" / "OTP expired" / "Invalid phone number"
}

Manejo de Error (Frontend):

// 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:

{
  success: true
  data: {
    authUrl: string         // URL de OAuth del proveedor
    state: string           // State token para CSRF protection
    codeChallenge?: string  // PKCE
  }
}

Manejo de Error (Frontend):

// 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):

// 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:

{
  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):

// 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:

{
  success: true
  data: {
    message: "Session revoked successfully"
  }
}

Manejo de Error (Frontend):

// 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:

{
  method: 'totp' | 'sms'
}

Response Schema Esperado:

{
  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:

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

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)

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)

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:

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