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