ML Engine Updates: - Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records - Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence) - Backtest results: +176.71R profit with aggressive_filter strategy Documentation Consolidation: - Created docs/99-analisis/_MAP.md index with 13 new analysis documents - Consolidated inventories: removed duplicates from orchestration/inventarios/ - Updated ML_INVENTORY.yml with BTCUSD metrics and training results - Added execution reports: FASE11-BTCUSD, correction issues, alignment validation Architecture & Integration: - Updated all module documentation with NEXUS v3.4 frontmatter - Fixed _MAP.md indexes across all folders - Updated orchestration plans and traces Files: 229 changed, 5064 insertions(+), 1872 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
22 KiB
| id | title | type | status | priority | epic | story_points | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|
| US-AUTH-011 | Recuperacion de Contrasena | User Story | To Do | Alta | OQI-001 | 3 | 2025-12-05 | 2026-01-04 |
US-AUTH-011: Recuperación de Contraseña
Version: 1.0.0 Fecha: 2025-12-05 Estado: Pendiente Story Points: 3 Prioridad: P0 (Crítica) Épica: OQI-001
Historia de Usuario
Como usuario de Trading Platform que olvidó su contraseña Quiero poder recuperar el acceso a mi cuenta Para poder volver a iniciar sesión sin necesidad de crear una nueva cuenta
Criterios de Aceptación
AC-001: Link de recuperación
Dado que estoy en la página de login Cuando veo el formulario Entonces debería ver un link "¿Olvidaste tu contraseña?" Y debería estar claramente visible
AC-002: Formulario de solicitud
Dado que hago click en "¿Olvidaste tu contraseña?" Cuando accedo a la página de recuperación Entonces debería ver:
- Título "Recupera tu contraseña"
- Campo para ingresar email
- Botón "Enviar instrucciones"
- Link para volver a login
AC-003: Validación de email
Dado que estoy en el formulario de recuperación Cuando ingreso un email inválido Entonces debería ver mensaje "Email inválido" Y el botón debería estar deshabilitado
AC-004: Email enviado (cuenta existe)
Dado que ingresé un email registrado Cuando hago click en "Enviar instrucciones" Entonces debería:
- Ver mensaje "Si el email está registrado, recibirás instrucciones"
- Recibir un email con link de recuperación
- El link debería expirar en 1 hora
AC-005: Email no registrado
Dado que ingresé un email no registrado Cuando hago click en "Enviar instrucciones" Entonces debería ver el mismo mensaje genérico Y NO debería revelar que el email no existe (seguridad)
AC-006: Contenido del email
Dado que solicité recuperación de contraseña Cuando reviso mi email Entonces debería recibir un email con:
- Asunto: "Recupera tu contraseña de Trading Platform"
- Saludo personalizado con mi nombre
- Explicación clara del proceso
- Botón/link de "Restablecer contraseña"
- Nota de que expira en 1 hora
- Nota de ignorar si no solicité el cambio
AC-007: Click en link de recuperación
Dado que recibí el email de recuperación Cuando hago click en el link Entonces debería ser redirigido a una página de "Nueva contraseña" Y el token debería validarse automáticamente
AC-008: Formulario de nueva contraseña
Dado que accedí con un token válido Cuando veo el formulario Entonces debería ver:
- Campo "Nueva contraseña"
- Campo "Confirmar nueva contraseña"
- Indicadores de fortaleza de contraseña
- Botón "Guardar nueva contraseña"
AC-009: Validación de nueva contraseña
Dado que estoy ingresando nueva contraseña Cuando ingreso una contraseña débil Entonces debería ver los mismos requisitos que en registro:
- ✅/❌ Mínimo 8 caracteres
- ✅/❌ Al menos una mayúscula
- ✅/❌ Al menos una minúscula
- ✅/❌ Al menos un número
- ✅/❌ Al menos un carácter especial
AC-010: Contraseñas no coinciden
Dado que las contraseñas no coinciden Cuando intento guardar Entonces debería ver mensaje "Las contraseñas no coinciden"
AC-011: Cambio exitoso
Dado que ingresé una contraseña válida Cuando hago click en "Guardar nueva contraseña" Entonces debería:
- Ver mensaje "Contraseña actualizada exitosamente"
- Invalidar el token de recuperación
- Cerrar todas las sesiones activas
- Ser redirigido a login
- Poder iniciar sesión con la nueva contraseña
AC-012: Token expirado
Dado que pasó más de 1 hora desde la solicitud Cuando intento usar el link de recuperación Entonces debería ver mensaje:
- "Este link ha expirado. Solicita uno nuevo" Y debería ver botón "Solicitar nuevo link"
AC-013: Token ya usado
Dado que ya usé un token para cambiar contraseña Cuando intento usar el mismo link nuevamente Entonces debería ver mensaje:
- "Este link ya fue usado" Y debería ver link a login
AC-014: Token inválido
Dado que el token fue manipulado o es inválido Cuando intento acceder Entonces debería ver mensaje:
- "Link inválido. Solicita uno nuevo"
AC-015: Rate limiting
Dado que solicité 3 recuperaciones en 1 hora Cuando intento solicitar otra Entonces debería ver mensaje:
- "Demasiadas solicitudes. Intenta en 1 hora"
Mockup
Paso 1: Solicitud de recuperación
┌─────────────────────────────────────────────────────────────┐
│ │
│ 🔐 Recupera tu contraseña │
│ │
│ Ingresa tu email y te enviaremos instrucciones │
│ para recuperar tu contraseña. │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Email │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ usuario@example.com │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Enviar instrucciones │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ← Volver a inicio de sesión │
│ │
└─────────────────────────────────────────────────────────────┘
Paso 2: Confirmación
┌─────────────────────────────────────────────────────────────┐
│ │
│ ✉️ Revisa tu email │
│ │
│ Si el email está registrado, recibirás instrucciones │
│ para recuperar tu contraseña. │
│ │
│ Revisa también tu carpeta de spam. │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Volver a inicio de sesión │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ¿No recibiste el email? Intenta de nuevo en 5 minutos │
│ │
└─────────────────────────────────────────────────────────────┘
Email de recuperación:
┌─────────────────────────────────────────────────────────────┐
│ │
│ De: Trading Platform <noreply@trading.com> │
│ Para: usuario@example.com │
│ Asunto: Recupera tu contraseña de Trading Platform │
│ │
│ ───────────────────────────────────────────────────── │
│ │
│ Hola Juan, │
│ │
│ Recibimos una solicitud para recuperar tu contraseña. │
│ │
│ Haz click en el botón de abajo para crear una nueva: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Restablecer mi contraseña │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ O copia este link en tu navegador: │
│ https://trading.com/reset-password?token=abc123... │
│ │
│ Este link expirará en 1 hora. │
│ │
│ Si no solicitaste este cambio, puedes ignorar este │
│ email. Tu contraseña no cambiará. │
│ │
│ Saludos, │
│ El equipo de Trading Platform │
│ │
└─────────────────────────────────────────────────────────────┘
Paso 3: Nueva contraseña
┌─────────────────────────────────────────────────────────────┐
│ │
│ 🔒 Crea una nueva contraseña │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Nueva contraseña │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ •••••••••••• 👁 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ✅ Mínimo 8 caracteres │ │
│ │ ✅ Una mayúscula │ │
│ │ ✅ Una minúscula │ │
│ │ ✅ Un número │ │
│ │ ❌ Un carácter especial │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Confirmar nueva contraseña │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ •••••••••••• 👁 │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Guardar nueva contraseña │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Paso 4: Éxito
┌─────────────────────────────────────────────────────────────┐
│ │
│ ✅ Contraseña actualizada │
│ │
│ Tu contraseña ha sido actualizada exitosamente. │
│ Por seguridad, cerramos todas tus sesiones activas. │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Iniciar sesión │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Tareas Técnicas
Database (DB)
- Tabla
password_reset_tokens:CREATE TABLE password_reset_tokens ( id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), token VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP, ip_address VARCHAR(45), created_at TIMESTAMP DEFAULT NOW(), INDEX idx_token_expires (token, expires_at) ); - Tabla
password_reset_rate_limits:CREATE TABLE password_reset_rate_limits ( id UUID PRIMARY KEY, email VARCHAR(255) NOT NULL, ip_address VARCHAR(45), attempts INT DEFAULT 1, window_start TIMESTAMP DEFAULT NOW(), INDEX idx_email_window (email, window_start) );
Backend (BE)
- Endpoint
POST /api/v1/auth/password/forgot- Validar email
- Rate limiting (3 por hora)
- Generar token seguro (crypto.randomBytes)
- Guardar token con expiración de 1 hora
- Enviar email
- Respuesta genérica (no revelar si existe)
- Endpoint
GET /api/v1/auth/password/reset/:token- Validar token existe y no expiró
- Validar no fue usado
- Devolver status del token
- Endpoint
POST /api/v1/auth/password/reset- Validar token
- Validar nueva contraseña
- Hash nueva contraseña
- Actualizar password
- Marcar token como usado
- Invalidar todas las sesiones
- Enviar email de confirmación
- Service
PasswordResetServicerequestReset(email)validateToken(token)resetPassword(token, newPassword)generateSecureToken()
- Email templates (Nodemailer)
- Template de recuperación
- Template de confirmación
- Tests unitarios (12 casos)
- Tests de integración (8 escenarios)
Frontend (FE)
- Página
ForgotPassword.tsx - Página
ResetPassword.tsx - Form validation con React Hook Form
- Password strength indicator
- Manejo de estados (loading, success, error)
- Manejo de tokens expirados/inválidos
- Tests con React Testing Library
Testing (QA)
- E2E: Flujo completo de recuperación
- E2E: Token expirado
- E2E: Token ya usado
- E2E: Token inválido
- E2E: Rate limiting
- E2E: Contraseña débil rechazada
- Test de seguridad: Token debe ser criptográficamente seguro
- Test de seguridad: No revelar si email existe
- Performance: Email enviado en < 2s
Dependencias
- Bloqueantes:
- US-AUTH-001: Necesita usuarios con contraseñas
- US-AUTH-002: Para el flujo de login posterior
Definition of Ready (DoR)
- Mockups aprobados
- Email templates diseñados
- API contract definido
- Estrategia de seguridad revisada
- Rate limiting definido
Definition of Done (DoD)
- Código implementado y revisado
- Tests unitarios con 80%+ cobertura
- Tests de integración pasando
- Tests E2E implementados
- Email templates funcionales
- Rate limiting implementado
- Tokens seguros (crypto.randomBytes)
- No se revela existencia de emails
- Logs implementados
- Documentación actualizada
- QA aprobado en staging
- Deploy a producción exitoso
Notas Técnicas
Token Generation
import crypto from 'crypto';
// Generar token seguro
const token = crypto.randomBytes(32).toString('hex');
// Resultado: "a1b2c3d4e5f6..." (64 caracteres)
// Hash del token antes de guardar (opcional, extra seguridad)
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
Password Reset Flow
- Usuario solicita recuperación
- Backend verifica rate limits
- Backend genera token seguro
- Backend guarda token con expiración (1 hora)
- Backend envía email con link
- Usuario click en link
- Frontend valida token con backend
- Usuario ingresa nueva contraseña
- Backend valida token nuevamente
- Backend actualiza contraseña
- Backend marca token como usado
- Backend invalida sesiones activas
- Backend envía email de confirmación
Email Template (HTML)
<!DOCTYPE html>
<html>
<head>
<style>
/* Email styles */
</style>
</head>
<body>
<div style="max-width: 600px; margin: 0 auto;">
<h1>Recupera tu contraseña</h1>
<p>Hola {{userName}},</p>
<p>Recibimos una solicitud para recuperar tu contraseña.</p>
<a href="{{resetLink}}" style="display: inline-block; padding: 12px 24px; background: #007bff; color: white; text-decoration: none; border-radius: 4px;">
Restablecer mi contraseña
</a>
<p>O copia este link: {{resetLink}}</p>
<p>Este link expirará en 1 hora.</p>
<p>Si no solicitaste este cambio, puedes ignorar este email.</p>
</div>
</body>
</html>
Environment Variables
PASSWORD_RESET_TOKEN_EXPIRY=3600 # 1 hora en segundos
PASSWORD_RESET_RATE_LIMIT=3 # intentos por ventana
PASSWORD_RESET_RATE_WINDOW=3600 # 1 hora
FRONTEND_URL=https://trading.com
Security Best Practices
-
Tokens seguros:
- Usar crypto.randomBytes (no Math.random)
- Mínimo 32 bytes (256 bits)
- Considerar hashear antes de guardar
-
No revelar información:
- Mismo mensaje para email existente o no
- No indicar si un email está registrado
-
Rate limiting:
- Máximo 3 intentos por hora por email
- También limitar por IP
-
Expiración:
- Tokens expiran en 1 hora
- Tokens de un solo uso
-
Invalidar sesiones:
- Al cambiar contraseña, cerrar todas las sesiones
- Forzar nuevo login
-
Logging:
- Log de todas las solicitudes
- Log de tokens usados/expirados
- No loggear el token en texto plano
-
Email de confirmación:
- Notificar al usuario que su contraseña cambió
- Incluir información de cuándo y desde dónde
Rate Limiting Implementation
async function checkRateLimit(email: string, ip: string) {
const oneHourAgo = new Date(Date.now() - 3600000);
const attempts = await db.passwordResetRateLimits.count({
where: {
email,
window_start: { gte: oneHourAgo }
}
});
if (attempts >= 3) {
throw new Error('Too many password reset attempts');
}
// Registrar intento
await db.passwordResetRateLimits.create({
data: { email, ip_address: ip }
});
}