# 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 ``` **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 ``` **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 ``` **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 ``` **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 ` 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 ' (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)*