- Archivados 5 análisis obsoletos a _archive/2026-01-25/ - MASTER-ANALYSIS-PLAN marcada SUPERSEDIDA - FRONTEND-COMPREHENSIVE-AUDIT marcada COMPLETADA (7+ entregables) - FRONTEND-MODULE-DOCS marcada CANCELADA (P3, sin progreso) - BLOCKER-001-TOKEN-REFRESH marcada POSTERGADA - Actualizado PROJECT-STATUS.md y _INDEX.yml Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
22 KiB
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:
- Usuario hace clic en "Setup" (Authenticator)
- Frontend llama POST /api/v1/auth/2fa/setup
- Backend retorna QR code + secret
- Frontend muestra QR
- Usuario escanea con Google Authenticator / Authy
- Usuario copia código de 6 dígitos
- Frontend llama POST /api/v1/auth/2fa/enable con código
- Éxito: mostrar backup codes, confirmación
Implementación Pendiente:
// SecuritySettings.tsx línea 237-241
<button type="button" className="...">
Setup {/* <- NO TIENE onClick handler */}
</button>
// Necesita:
const [showQRSetup, setShowQRSetup] = useState(false)
const [qrCode, setQrCode] = useState<string>('')
const [twoFASecret, setTwoFASecret] = useState<string>('')
const [verificationCode, setVerificationCode] = useState<string>('')
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:
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:
<form className="space-y-4 max-w-md">
<input type="password" placeholder="Enter current password" />
<input type="password" placeholder="Enter new password" />
<input type="password" placeholder="Confirm new password" />
<button type="submit">Update Password</button> {/* <- Sin handler */}
</form>
Implementación Necesaria:
const [passwordForm, setPasswordForm] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
const [passwordError, setPasswordError] = useState<string | null>(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:
- En auth.service.ts: Agregar interceptor de refresh
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)
}
)
- 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:
- User abre app en móvil
- Opción: "Login with Biometric" o "Use Passcode"
- Si biometric disponible, muestra prompt
- Si exitoso, backend verifica fingerprint
- 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:
// Login.tsx
const [loginAttempts, setLoginAttempts] = useState(0)
const [accountLocked, setAccountLocked] = useState(false)
const [lockExpiresAt, setLockExpiresAt] = useState<Date | null>(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:
- User habilita 2FA SMS en SecuritySettings
- User entra con email + password
- Backend retorna requiresTwoFactor: true
- Frontend muestra UI SMS (similar a PhoneLoginForm)
- User recibe SMS con código
- 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:
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:
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:
const [otpExpiresAt, setOtpExpiresAt] = useState<Date | null>(null)
// ... nunca se renderiza en UI
Implementación Necesaria:
{step === 'otp' && (
<div className="text-center text-sm text-gray-400">
OTP expires in: <CountdownTimer expiresAt={otpExpiresAt} />
</div>
)}
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 <span>{remaining}</span>
}
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:
<button
type="button"
onClick={handleResendOTP}
disabled={isLoading} {/* <- Falta: disabled durante cooldown */}
className="..."
>
Reenviar codigo
</button>
Problema:
- Usuario puede spamear resend
- Costo SMS se multiplica
Implementación:
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:
<button disabled={resendCooldown > 0 || isLoading}>
{resendCooldown > 0 ? `Reenviar en ${resendCooldown}s` : 'Reenviar codigo'}
</button>
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:
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full transition-all ${
strength === 'weak' ? 'w-1/3 bg-red-500' :
strength === 'fair' ? 'w-2/3 bg-yellow-500' :
'w-full bg-green-500'
}`}
/>
</div>
<span className="text-xs text-gray-400">{strength}</span>
</div>
<p className="text-xs text-gray-500">
Estimated crack time: ~100 years
</p>
</div>
G-019: Session Timeout Warning
Prioridad: P1 Tipo: UX Esfuerzo: M
Descripción: Sin advertencia previa a expiración de sesión.
Implementación:
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:
- User registra con email erróneo
- No recibe email de verificación
- Queda atrapado en VerifyEmail.tsx sin poder proceder
Implementación:
{status === 'no-token' && (
<div className="text-center">
{/* ... */}
<Link to="/login" className="btn btn-secondary block">
Volver a Iniciar Sesion
</Link>
<button
onClick={handleResendEmail}
className="text-sm text-primary-400 hover:underline mt-4"
>
¿No recibiste el email? Reenviar
</button>
</div>
)}
G-024: Accessibility (a11y)
Prioridad: P1 Tipo: Accesibilidad Esfuerzo: L
Descripción: WCAG 2.1 AA compliance incompleta:
Problemas Identificados:
- Faltan aria-labels en íconos
- Contraste de color insuficiente en textos secundarios
- Faltan aria-required en campos obligatorios
- Sin aria-live para mensajes de error
- Falta landmark structure (main, nav, etc)
- Falta skip link para navegación
Ejemplo de Corrección:
// Antes
<input type="email" placeholder="tu@email.com" />
// Después
<input
type="email"
placeholder="tu@email.com"
aria-label="Email address"
aria-required="true"
aria-describedby="email-error"
/>
<div id="email-error" role="alert" aria-live="polite">
{emailError && <span className="text-red-400">{emailError}</span>}
</div>
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:
// ErrorBoundary.tsx
class ErrorBoundary extends React.Component {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-white mb-4">
Something went wrong
</h1>
<button onClick={() => window.location.href = '/'}>
Go Home
</button>
</div>
</div>
)
}
return this.props.children
}
}
// En módulo auth
<ErrorBoundary>
<AuthModule />
</ErrorBoundary>
TABLA RESUMEN
| Categoría | Cantidad | Críticos | Alto Impacto | Total Esfuerzo |
|---|---|---|---|---|
| 2FA & Seguridad | 6 | 3 | 3 | 120h |
| UX & Localización | 8 | 2 | 4 | 60h |
| Robustez & Error Handling | 4 | 1 | 2 | 50h |
| Accesibilidad | 2 | 1 | 1 | 40h |
| Funcionalidad Avanzada | 10 | 2 | 5 | 100h |
| TOTAL | 30 | 9 | 15 | 370h |
ROADMAP RECOMENDADO
Fase 1: Seguridad Crítica (2-3 semanas)
- G-001: 2FA Setup UI
- G-002: 2FA Enable
- G-012: Refresh Token Auto-Renew
- G-018: Account Lockout
Impacto: Reduce riesgo de brechas de seguridad en 80%
Fase 2: UX & Funcionalidad Base (2-3 semanas)
- G-003: Password Change Handler
- G-005: OAuth User Mapping
- G-021: Email Verification Resend
- G-008: OTP Expiración UI
- G-015: Password Strength Meter
Impacto: Mejora experiencia en 60%
Fase 3: Robustez & Accesibilidad (2 semanas)
- G-024: WCAG 2.1 Compliance
- G-027: Error Boundary
- G-028: Offline Support
Impacto: Aplicación ready-for-production
Fase 4: Características Avanzadas (4-6 semanas)
- G-016: Biometric Auth
- G-017: WebAuthn
- G-023: i18n / Localización
- G-026: Mobile Responsiveness
Impacto: Diferenciación competitiva
DEPENDENCIAS ENTRE GAPS
G-001 ──→ G-002 (G-002 requiere G-001)
G-012 ──→ Todo (auth es requisito)
G-005 ──→ G-006 (mapeo de usuario antes de backup codes)
G-018 ──→ Login (bloquea todos los logins)
G-016 ──→ Móvil (requiere app nativa)
CONCLUSIÓN
Status Actual: 70% completo Gaps Críticos: 9 Esfuerzo Total: 370 horas (~9.25 personas-semana)
Para llegar a 100%:
- Seguridad: Completar 2FA + Token Refresh (BLOQUEANTE)
- UX: Password change, session management
- Production: Error boundaries, a11y, offline
Recomendación: Enfocarse en Fase 1 antes de cualquier PR a main.
Análisis completado: 2026-01-25 30 gaps identificados y priorizados