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>
785 lines
22 KiB
Markdown
785 lines
22 KiB
Markdown
# 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
|
|
<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:**
|
|
```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
|
|
<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:**
|
|
```typescript
|
|
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:**
|
|
|
|
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<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:**
|
|
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<Date | null>(null)
|
|
// ... nunca se renderiza en UI
|
|
```
|
|
|
|
**Implementación Necesaria:**
|
|
```tsx
|
|
{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:
|
|
```typescript
|
|
<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:**
|
|
```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:
|
|
<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:**
|
|
```tsx
|
|
<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:**
|
|
```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' && (
|
|
<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:**
|
|
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
|
|
<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:**
|
|
```tsx
|
|
// 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)
|
|
1. **G-001:** 2FA Setup UI
|
|
2. **G-002:** 2FA Enable
|
|
3. **G-012:** Refresh Token Auto-Renew
|
|
4. **G-018:** Account Lockout
|
|
|
|
**Impacto:** Reduce riesgo de brechas de seguridad en 80%
|
|
|
|
### Fase 2: UX & Funcionalidad Base (2-3 semanas)
|
|
1. **G-003:** Password Change Handler
|
|
2. **G-005:** OAuth User Mapping
|
|
3. **G-021:** Email Verification Resend
|
|
4. **G-008:** OTP Expiración UI
|
|
5. **G-015:** Password Strength Meter
|
|
|
|
**Impacto:** Mejora experiencia en 60%
|
|
|
|
### Fase 3: Robustez & Accesibilidad (2 semanas)
|
|
1. **G-024:** WCAG 2.1 Compliance
|
|
2. **G-027:** Error Boundary
|
|
3. **G-028:** Offline Support
|
|
|
|
**Impacto:** Aplicación ready-for-production
|
|
|
|
### Fase 4: Características Avanzadas (4-6 semanas)
|
|
1. **G-016:** Biometric Auth
|
|
2. **G-017:** WebAuthn
|
|
3. **G-023:** i18n / Localización
|
|
4. **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*
|