# OQI-001: Análisis de Gaps - Fundamentos-Auth
**Módulo:** OQI-001 (fundamentos-auth)
**Ruta:** `apps/frontend/src/modules/auth/`
**Progreso Actual:** 70%
**Fecha:** 2026-01-25
**Status:** ANÁLISIS COMPLETO
---
## TABLA CONSOLIDADA DE GAPS
| Gap ID | Descripción | Prioridad | Tipo | Esfuerzo | Status | Impacto | Notas |
|--------|-------------|-----------|------|----------|--------|---------|-------|
| **G-001** | 2FA Setup UI - Formulario de configuración de Authenticator App | P0 | Implementación | L | BLOQUEANTE | CRÍTICO | Tab "Two-Factor Auth" tiene botones "Setup" sin handler |
| **G-002** | 2FA Enable Endpoint - Consumir POST /auth/2fa/enable | P0 | Implementación | M | BLOQUEANTE | CRÍTICO | Necesario para completar flujo 2FA |
| **G-003** | Password Change Handler - Implementar submit en SecuritySettings tab "password" | P1 | Implementación | M | TODO | ALTO | Formulario existe pero sin funcionalidad |
| **G-004** | SMS 2FA Method - Soporte para verificación por SMS en login | P1 | Implementación | M | PARCIAL | ALTO | PhoneLoginForm existe pero no vinculado a 2FA |
| **G-005** | OAuth User Mapping - Mapeo de datos desde OAuth a User object | P1 | Implementación | M | TODO | ALTO | AuthCallback solo maneja tokens, no perfil |
| **G-006** | Backup Codes - UI para mostrar y descargar códigos de backup 2FA | P1 | Documentación | M | TODO | MEDIO | Backend soporta, frontend no lo muestra |
| **G-007** | Rate Limiting UI - Mostrar contador de intentos fallidos | P2 | UX | S | TODO | BAJO | No hay feedback visual de rate limiting |
| **G-008** | OTP Expiración UI - Mostrar countdown de expiración en PhoneLoginForm | P2 | UX | M | TODO | BAJO | otpExpiresAt se calcula pero no se muestra |
| **G-009** | Resend OTP Cooldown - Deshabilitar botón resend durante cooldown | P2 | UX | S | PARCIAL | BAJO | handleResendOTP existe pero sin throttle |
| **G-010** | Error Recovery - Sin auto-retry en conexión fallida | P2 | Implementación | S | TODO | BAJO | Requiere reintentar manualmente |
| **G-011** | Session Device Fingerprinting - Información de dispositivo incompleta | P1 | Integración | M | PARCIAL | MEDIO | DeviceCard muestra navegador/OS pero no CPU/memoria |
| **G-012** | Refresh Token Auto-Renew - No hay renovación automática de tokens | P1 | Implementación | L | BLOQUEANTE | CRÍTICO | Tokens expiran sin renovación |
| **G-013** | Social Provider Error Handling - Mensajes genéricos de OAuth error | P1 | UX | S | PARCIAL | MEDIO | AuthCallback tiene mapeo básico pero incompleto |
| **G-014** | Remember Me Persistence - rememberMe no persiste sesión | P2 | Implementación | S | PARCIAL | BAJO | Checkbox existe pero sin lógica backend |
| **G-015** | Password Strength Meter - Indicador visual de fortaleza | P1 | UX | S | PARCIAL | MEDIO | Register/ResetPassword muestran reqs pero sin barra |
| **G-016** | Biometric Auth - Touch/Face ID no soportado | P1 | Implementación | L | TODO | CRÍTICO | Importante para móvil |
| **G-017** | WebAuthn (FIDO2) - No soportado | P2 | Implementación | XL | TODO | ALTO | Estándar moderno de seguridad |
| **G-018** | Account Lockout - Sin mecanismo de bloqueo tras intentos fallidos | P1 | Seguridad | M | TODO | CRÍTICO | Riesgo de fuerza bruta |
| **G-019** | Session Timeout Warning - Sin advertencia antes de expiración | P1 | UX | M | TODO | MEDIO | Usuario no sabe cuándo expira sesión |
| **G-020** | Logout Confirmation - Sin confirmación antes de cerrar sesión | P2 | UX | S | TODO | BAJO | Riesgo de logout accidental |
| **G-021** | Email Verification Resend - No hay opción de reenviar email | P1 | Implementación | S | TODO | ALTO | Usuario sin acceso a email original queda atrapado |
| **G-022** | Account Recovery - No hay opción de recuperación sin email | P2 | Implementación | M | TODO | MEDIO | Si email está comprometido |
| **G-023** | Language/Locale Support - Todo hardcodeado en español | P2 | Localización | M | TODO | BAJO | i18n no implementado |
| **G-024** | Accessibility (a11y) - WCAG 2.1 compliance incompleta | P1 | Accesibilidad | L | PARCIAL | MEDIO | Faltan aria-labels, color contrast |
| **G-025** | Loading States - Estados intermedios ambiguos | P2 | UX | S | PARCIAL | BAJO | No hay feedback de progreso en algunos flujos |
| **G-026** | Mobile Responsiveness - Algunos layouts no optimizados móvil | P2 | UX | M | PARCIAL | BAJO | SecuritySettings podría ser más responsive |
| **G-027** | Error Boundary - Sin error boundary en módulo | P1 | Robustez | M | TODO | ALTO | Errores rompen toda la app |
| **G-028** | Offline Support - Sin detección de conexión | P2 | UX | M | TODO | BAJO | Mensajes confusos si sin internet |
| **G-029** | Deep Linking - OAuth redirect puede fallar | P1 | Robustez | S | PARCIAL | MEDIO | returnUrl no siempre válido |
| **G-030** | Audit Logging - Sin registro de eventos en frontend | P2 | Seguridad | S | TODO | BAJO | Backend lo hace, frontend no |
---
## GAPS CRÍTICOS (P0-P1 de Alto Impacto)
### G-001: 2FA Setup UI
**Prioridad:** P0 (BLOQUEANTE)
**Tipo:** Implementación
**Esfuerzo:** L (Large - 40-80 horas)
**Impacto:** CRÍTICO
**Descripción:**
Tab "two-factor" en SecuritySettings.tsx (líneas 192-267) tiene:
- Opción para "Authenticator App" con botón "Setup"
- Opción para "SMS" con botón "Setup"
- AMBOS botones sin handler implementado
**Flujo Esperado:**
1. Usuario hace clic en "Setup" (Authenticator)
2. Frontend llama POST /api/v1/auth/2fa/setup
3. Backend retorna QR code + secret
4. Frontend muestra QR
5. Usuario escanea con Google Authenticator / Authy
6. Usuario copia código de 6 dígitos
7. Frontend llama POST /api/v1/auth/2fa/enable con código
8. Éxito: mostrar backup codes, confirmación
**Implementación Pendiente:**
```typescript
// SecuritySettings.tsx línea 237-241
// Necesita:
const [showQRSetup, setShowQRSetup] = useState(false)
const [qrCode, setQrCode] = useState('')
const [twoFASecret, setTwoFASecret] = useState('')
const [verificationCode, setVerificationCode] = useState('')
const handleSetup2FA = async () => {
// POST /api/v1/auth/2fa/setup con method='totp'
// Mostrar QR
}
```
**Archivos Afectados:**
- C:\Empresas\ISEM\workspace-v2\projects\trading-platform\apps\frontend\src\modules\auth\pages\SecuritySettings.tsx (línea 237-241, 258-262)
---
### G-002: 2FA Enable Endpoint Integration
**Prioridad:** P0 (BLOQUEANTE)
**Tipo:** Implementación
**Esfuerzo:** M (Medium - 20-40 horas)
**Impacto:** CRÍTICO
**Descripción:**
Después de escanear QR (G-001), necesita validar el código TOTP antes de activar.
**Endpoint Necesario:**
```
POST /api/v1/auth/2fa/enable
{
method: 'totp',
verificationCode: '123456'
}
```
**Lógica Pendiente:**
```typescript
const handleVerify2FA = async (code: string) => {
const response = await fetch('/api/v1/auth/2fa/enable', {
method: 'POST',
body: JSON.stringify({
method: 'totp',
verificationCode: code
})
})
const data = await response.json()
// Si éxito: mostrar backup codes
// data.backupCodes = ['code1', 'code2', ...]
}
```
**Dependencia:** G-001 (debe completarse primero)
---
### G-003: Password Change Handler
**Prioridad:** P1 (TODO)
**Tipo:** Implementación
**Esfuerzo:** M (Medium)
**Impacto:** ALTO
**Descripción:**
SecuritySettings.tsx tab "password" (líneas 138-188) tiene formulario completo:
```tsx
```
**Implementación Necesaria:**
```typescript
const [passwordForm, setPasswordForm] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
const [passwordError, setPasswordError] = useState(null)
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault()
// Validar que newPassword !== currentPassword
// Validar requerimientos de contraseña
try {
const response = await fetch('/api/v1/auth/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword
})
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error)
}
setPasswordError(null)
// Mostrar success toast
} catch (err) {
setPasswordError(err.message)
}
}
```
**Archivos Afectados:**
- SecuritySettings.tsx (línea 147-187)
---
### G-012: Refresh Token Auto-Renew
**Prioridad:** P1 (BLOQUEANTE)
**Tipo:** Implementación
**Esfuerzo:** L (Large)
**Impacto:** CRÍTICO
**Descripción:**
AccessToken típicamente expira en 15 minutos. Sin refresh automático:
- Usuario obtiene error 401 de repente
- Requiere login manual
- Experiencia pobre
**Implementación Necesaria:**
1. **En auth.service.ts:** Agregar interceptor de refresh
```typescript
api.interceptors.response.use(
response => response,
async (error) => {
if (error.response?.status === 401) {
try {
const refreshToken = localStorage.getItem('refreshToken')
const response = await api.post('/auth/refresh', {
refreshToken
})
localStorage.setItem('accessToken', response.data.accessToken)
localStorage.setItem('refreshToken', response.data.refreshToken)
// Reintentar request original
return api(error.config)
} catch {
// Logout
localStorage.removeItem('token')
window.location.href = '/login'
}
}
return Promise.reject(error)
}
)
```
2. **En Backend:** Implementar POST /api/v1/auth/refresh
```
POST /api/v1/auth/refresh
{
refreshToken: string
}
Response:
{
accessToken: string
refreshToken: string (nuevo)
}
```
**Archivos Afectados:**
- auth.service.ts
- Interceptor de axios
---
### G-016: Biometric Authentication
**Prioridad:** P1 (IMPLEMENTACIÓN FUTURA)
**Tipo:** Implementación
**Esfuerzo:** L (Large - 60-120 horas)
**Impacto:** CRÍTICO
**Descripción:**
Soporte para Touch/Face ID en dispositivos móviles.
**Scope:**
- Android: BiometricPrompt
- iOS: LocalAuthentication
- Web: WebAuthn (G-017)
**Flujo Esperado:**
1. User abre app en móvil
2. Opción: "Login with Biometric" o "Use Passcode"
3. Si biometric disponible, muestra prompt
4. Si exitoso, backend verifica fingerprint
5. Obtiene tokens
**Nota:** Requiere integración con React Native (si aplica)
---
### G-018: Account Lockout
**Prioridad:** P1 (SEGURIDAD CRÍTICA)
**Tipo:** Implementación
**Esfuerzo:** M (Medium)
**Impacto:** CRÍTICO
**Descripción:**
SIN mecanismo de bloqueo tras intentos fallidos de login.
**Riesgo:**
- Fuerza bruta masiva
- Compromiso de cuenta
**Implementación Necesaria:**
**Frontend:**
```typescript
// Login.tsx
const [loginAttempts, setLoginAttempts] = useState(0)
const [accountLocked, setAccountLocked] = useState(false)
const [lockExpiresAt, setLockExpiresAt] = useState(null)
const handleEmailLogin = async (e: React.FormEvent) => {
if (accountLocked) {
const remaining = new Date(lockExpiresAt!).getTime() - Date.now()
throw new Error(`Account locked. Try again in ${Math.ceil(remaining / 60000)} minutes`)
}
try {
// ... login logic ...
setLoginAttempts(0)
} catch (err) {
const newAttempts = loginAttempts + 1
setLoginAttempts(newAttempts)
if (newAttempts >= 5) {
setAccountLocked(true)
setLockExpiresAt(new Date(Date.now() + 15 * 60 * 1000)) // 15 mins
}
}
}
```
**Backend:**
- Rastrear intentos fallidos por IP/email
- Bloquear tras N intentos (típico: 5)
- Desbloquear automáticamente tras M minutos (típico: 15)
- Log de intentos para auditoría
---
## GAPS FUNCIONALES (P1-P2)
### G-004: SMS 2FA Method
**Prioridad:** P1
**Tipo:** Implementación
**Esfuerzo:** M
**Descripción:**
PhoneLoginForm.tsx soporta enviar OTP por SMS/WhatsApp, pero:
- No está vinculado a flujo de 2FA en login
- SecuritySettings.tsx tiene botón "Setup SMS" sin funcionalidad
**Flujo Esperado:**
1. User habilita 2FA SMS en SecuritySettings
2. User entra con email + password
3. Backend retorna requiresTwoFactor: true
4. Frontend muestra UI SMS (similar a PhoneLoginForm)
5. User recibe SMS con código
6. User verifica código
**Nota:** PhoneLoginForm es alternativa a email (no es 2FA)
---
### G-005: OAuth User Mapping
**Prioridad:** P1
**Tipo:** Implementación
**Esfuerzo:** M
**Descripción:**
AuthCallback.tsx (líneas 1-96) solo maneja tokens:
```javascript
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
```
**Falta:**
- Obtener perfil de usuario desde /api/v1/auth/me
- Guardar en contexto/store de usuario
- Mostrar nombre/avatar en dashboard
**Implementación Necesaria:**
```typescript
useEffect(() => {
if (accessToken && refreshToken) {
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
// Nuevo: Fetch user profile
fetch('/api/v1/auth/me', {
headers: { 'Authorization': `Bearer ${accessToken}` }
})
.then(res => res.json())
.then(data => {
// Guardar en store/context
userStore.setUser(data.user)
})
setTimeout(() => {
navigate(isNewUser ? '/onboarding' : returnUrl)
}, 1500)
}
}, [...])
```
---
### G-011: Session Device Fingerprinting
**Prioridad:** P1
**Tipo:** Integración
**Esfuerzo:** M
**Descripción:**
DeviceCard.tsx muestra navegador/OS parseados de User-Agent:
```
"Chrome on Windows 10/11"
```
**Falta:**
- CPU arquitectura (x86, ARM, etc)
- Memoria RAM
- Resolución de pantalla
- ID único de dispositivo (localStorage fingerprint)
**Beneficio:**
- Usuario puede identificar dispositivos extraños
- Detección de dispositivos comprometidos
---
## GAPS DE UX (P2)
### G-008: OTP Expiración UI
**Prioridad:** P2
**Tipo:** UX
**Esfuerzo:** M
**Descripción:**
PhoneLoginForm.tsx calcula `otpExpiresAt` pero no lo muestra:
```typescript
const [otpExpiresAt, setOtpExpiresAt] = useState(null)
// ... nunca se renderiza en UI
```
**Implementación Necesaria:**
```tsx
{step === 'otp' && (
OTP expires in:
)}
function CountdownTimer({ expiresAt }: { expiresAt: Date | null }) {
const [remaining, setRemaining] = useState('')
useEffect(() => {
const interval = setInterval(() => {
const diff = expiresAt!.getTime() - Date.now()
const secs = Math.ceil(diff / 1000)
setRemaining(`${secs}s`)
}, 1000)
return () => clearInterval(interval)
}, [expiresAt])
return {remaining}
}
```
---
### G-009: Resend OTP Cooldown
**Prioridad:** P2
**Tipo:** UX
**Esfuerzo:** S
**Descripción:**
Botón "Reenviar código" en PhoneLoginForm (línea 167-174) sin throttle:
```typescript
```
**Problema:**
- Usuario puede spamear resend
- Costo SMS se multiplica
**Implementación:**
```typescript
const [resendCooldown, setResendCooldown] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setResendCooldown(prev => Math.max(0, prev - 1))
}, 1000)
return () => clearInterval(interval)
}, [])
const handleResendOTP = async () => {
setResendCooldown(30) // 30 segundos
// ... send OTP ...
}
// En JSX:
```
---
### G-015: Password Strength Meter
**Prioridad:** P1
**Tipo:** UX
**Esfuerzo:** S
**Descripción:**
Register.tsx y ResetPassword.tsx muestran checkmarks de requerimientos:
```
✓ 8+ caracteres
✓ Una mayuscula
✗ Una minuscula
✗ Un numero
✗ Un caracter especial
```
**Falta:**
- Barra visual de fortaleza
- Estimación de tiempo para crackear
- Sugerencias en tiempo real
**Implementación:**
```tsx
{strength}
Estimated crack time: ~100 years
```
---
### G-019: Session Timeout Warning
**Prioridad:** P1
**Tipo:** UX
**Esfuerzo:** M
**Descripción:**
Sin advertencia previa a expiración de sesión.
**Implementación:**
```typescript
useEffect(() => {
const token = localStorage.getItem('accessToken')
const decoded = jwt_decode(token)
const expiresAt = decoded.exp * 1000
const warningTime = expiresAt - 5 * 60 * 1000 // 5 mins antes
const timeUntilWarning = warningTime - Date.now()
if (timeUntilWarning > 0) {
const timeout = setTimeout(() => {
showModal("Your session expires in 5 minutes. Click to stay logged in.")
}, timeUntilWarning)
return () => clearTimeout(timeout)
}
}, [])
```
---
## GAPS DE SEGURIDAD
### G-021: Email Verification Resend
**Prioridad:** P1
**Tipo:** Implementación
**Esfuerzo:** S
**Descripción:**
VerifyEmail.tsx no ofrece opción de reenviar email.
**Escenario:**
1. User registra con email erróneo
2. No recibe email de verificación
3. Queda atrapado en VerifyEmail.tsx sin poder proceder
**Implementación:**
```tsx
{status === 'no-token' && (
{/* ... */}
Volver a Iniciar Sesion
)}
```
---
### G-024: Accessibility (a11y)
**Prioridad:** P1
**Tipo:** Accesibilidad
**Esfuerzo:** L
**Descripción:**
WCAG 2.1 AA compliance incompleta:
**Problemas Identificados:**
1. Faltan aria-labels en íconos
2. Contraste de color insuficiente en textos secundarios
3. Faltan aria-required en campos obligatorios
4. Sin aria-live para mensajes de error
5. Falta landmark structure (main, nav, etc)
6. Falta skip link para navegación
**Ejemplo de Corrección:**
```tsx
// Antes
// Después
{emailError && {emailError}}
```
---
## GAPS DE ROBUSTEZ
### G-027: Error Boundary
**Prioridad:** P1
**Tipo:** Robustez
**Esfuerzo:** M
**Descripción:**
Sin ErrorBoundary en módulo auth.
**Riesgo:**
- Excepción en componente = crash de toda la app
- Usuario no sabe qué pasó
**Implementación:**
```tsx
// ErrorBoundary.tsx
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return (