- Add 5 frontend specification documents (ET-*-frontend.md): - ET-AUTH-006: Authentication module frontend spec - ET-ML-008: ML Signals module frontend spec - ET-LLM-007: LLM Agent module frontend spec - ET-PFM-008: Portfolio Manager frontend spec (design) - ET-MKT-003: Marketplace frontend spec (design) - Add 8 new user stories: - US-AUTH-013: Global logout - US-AUTH-014: Device management - US-ML-008: Ensemble signal view - US-ML-009: ICT analysis view - US-ML-010: Multi-symbol scan - US-LLM-011: Execute trade from chat - US-PFM-013: Rebalance alerts - US-PFM-014: PDF report generation - Update task index with completed analysis Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
19 KiB
| id | title | type | status | priority | epic | story_points | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|
| US-AUTH-013 | Logout Global | User Story | To Do | Alta | OQI-001 | 3 | 2026-01-25 | 2026-01-25 |
US-AUTH-013: Logout Global
Version: 1.0.0 Fecha: 2026-01-25 Estado: Pendiente Story Points: 3 Prioridad: P1 (Alta) Épica: OQI-001
Historia de Usuario
Como usuario de Trading Platform Quiero poder cerrar sesión en todos mis dispositivos simultáneamente Para asegurarme de que mi cuenta está completamente segura, especialmente si creo que ha sido comprometida o si pierdo un dispositivo
Criterios de Aceptación
AC-001: Opción de logout global en perfil
Dado que estoy autenticado y en mi perfil Cuando accedo a Configuración > Seguridad > Sesiones Entonces debería ver un botón "Cerrar sesión en todos los dispositivos" o similar
AC-002: Confirmación de logout global
Dado que quiero cerrar sesión en todos mis dispositivos Cuando hago click en el botón de logout global Entonces debería ver un modal de confirmación que indique:
- "Esto cerrará sesión en TODOS tus dispositivos"
- "Incluida esta sesión actual"
- "Tendrás que volver a iniciar sesión"
- Botón "Cancelar" y "Cerrar todas las sesiones"
AC-003: Invalidación de todos los tokens
Dado que confirmo el logout global Cuando se ejecuta la acción Entonces debería:
- Invalidar TODOS los tokens JWT de todas las sesiones
- Invalidar TODOS los refresh tokens
- Marcar todas las sesiones como "closed" en la DB
- No permitir que ningún token anterior funcione
AC-004: Redirección a login
Dado que logout global se completó exitosamente Cuando se cierre la sesión Entonces debería:
- Mostrar mensaje "Sesión cerrada en todos los dispositivos"
- Redirigir a la página de login después de 2 segundos
- Limpiar localStorage y cookies
- No permitir acceso a rutas protegidas
AC-005: Email de notificación
Dado que ejecuté logout global Cuando se cierre la sesión Entonces debería recibir email en la dirección registrada indicando:
- "Se cerró sesión en todos tus dispositivos"
- Fecha y hora
- Si no fuiste tú, link para cambiar contraseña
AC-006: Impossibilidad de continuar con tokens antiguos
Dado que hice logout global Cuando intento usar un token anterior en cualquier dispositivo Entonces debería recibir:
- Código 401 Unauthorized
- Mensaje "Sesión inválida"
- Redirección a login
AC-007: Operación atómica
Dado que inicio logout global Cuando hay una falla de red durante el proceso Entonces debería:
- Reintentar automáticamente 3 veces
- Si continúa fallando, mostrar error al usuario
- Los tokens deben estar invalidados (garantizar atomicidad)
AC-008: Auditoria y registro
Dado que ejecuté logout global Cuando se complete la acción Entonces debería:
- Quedar registrado en logs de auditoria
- Incluir timestamp, user_id, tipo de acción
- Ser visible en historial de sesiones como "Cerrado por logout global"
Mockup
┌─────────────────────────────────────────────────────────────┐
│ Configuración > Seguridad > Sesiones │
├─────────────────────────────────────────────────────────────┤
│ │
│ Sesiones activas │
│ Gestiona dónde está abierta tu cuenta │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 💻 Chrome en macOS 🟢 Esta sesión │ │
│ │ San Francisco, Estados Unidos • 201.45.67.89 │ │
│ │ Última actividad: Hace 2 minutos │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 📱 Safari en iPhone [Cerrar sesión]│ │
│ │ Ciudad de México, México • 189.203.45.12 │ │
│ │ Última actividad: Hace 3 horas │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 🔒 CERRAR SESIÓN EN TODOS LOS DISPOSITIVOS │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ Esto cerrará sesión en esta y todas las otras │
│ sesiones. Tendrás que volver a iniciar sesión. │
│ │
└─────────────────────────────────────────────────────────────┘
Modal de confirmación:
┌─────────────────────────────────────────────────────────────┐
│ │
│ ⚠️ Cerrar sesión en todos los dispositivos │
│ │
│ Esto cerrará sesión INMEDIATAMENTE en: │
│ │
│ • Esta sesión (Chrome en macOS) │
│ • Safari en iPhone │
│ • Firefox en Windows │
│ │
│ Se cerrarán 3 sesiones en total. │
│ │
│ Si no fuiste tú, considera cambiar tu contraseña después. │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Cancelar │ │ Cerrar todo │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Email de notificación:
┌─────────────────────────────────────────────────────────────┐
│ De: Trading Platform Security <security@trading.com> │
│ Asunto: Se cerró sesión en todos tus dispositivos │
│ │
│ Hola Juan, │
│ │
│ Se cerró sesión en TODOS tus dispositivos. │
│ │
│ 🕐 25 de Enero, 2026 a las 14:30 CST │
│ 📍 Desde: San Francisco, Estados Unidos │
│ │
│ ¿Fuiste tú? │
│ Si reconoces esta actividad, puedes ignorar este email. │
│ │
│ ¿No fuiste tú? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Cambiar contraseña inmediatamente │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Para seguridad adicional, considera: │
│ • Verificar tu email de recuperación │
│ • Revisar actividades recientes │
│ • Habilitar autenticación de dos factores │
│ │
└─────────────────────────────────────────────────────────────┘
Mensaje después de logout:
┌─────────────────────────────────────────────────────────────┐
│ │
│ ✓ Sesión cerrada en todos los dispositivos │
│ │
│ Se cerrará sesión automáticamente en 2 segundos... │
│ │
└─────────────────────────────────────────────────────────────┘
Tareas Técnicas
Database (DB)
-
Tabla
user_sessions(ya existe en US-AUTH-012)- Verificar columnas:
expires_at,status - Agregar índice:
idx_user_active(user_id, WHERE status = 'active')
- Verificar columnas:
-
Tabla
session_audit_log:CREATE TABLE session_audit_log ( id UUID PRIMARY KEY, user_id UUID REFERENCES users(id) NOT NULL, action VARCHAR(100) NOT NULL, -- 'logout_global', 'logout_single', etc. session_id UUID REFERENCES user_sessions(id), ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMP DEFAULT NOW(), INDEX idx_user_created (user_id, created_at), INDEX idx_action_created (action, created_at) );
Backend (BE)
-
Endpoint
POST /api/v1/auth/logout-all- Autenticación requerida
- Parámetro:
confirmation: boolean - Respuesta:
{ success: boolean, message: string, redirectUrl: string } - Rate limit: 1 request/min por usuario
-
Service
LogoutServicelogoutGlobal(userId: string)- Buscar todas las sesiones activas del usuario
- Invalidar todos los tokens
- Marcar sesiones como 'closed'
- Registrar en auditoria
- Retornar conteo de sesiones cerradas
-
Invalidación de tokens:
- Crear tabla/cache de tokens invalidados (blacklist)
- Middleware: verificar si token está en blacklist
- Usar Redis para expiración automática
-
Email notification:
- Template:
logout-all.html - Enviar a dirección registrada
- Incluir: fecha, hora, ubicación, link de seguridad
- Service:
EmailService.sendLogoutAllNotification()
- Template:
-
Logging y auditoria:
- Registrar en
session_audit_log - Incluir: timestamp, user_id, acción, IP, user_agent
- Service:
AuditService.logLogoutAll()
- Registrar en
-
Tests unitarios (8 casos)
- Logout exitoso
- Logout con error de DB
- Email enviado correctamente
- Tokens invalidados
- Auditoria registrada
- Rate limiting
- Concurrencia (múltiples requests simultáneos)
- Validación de autenticación
-
Tests de integración (5 escenarios)
- Logout global desde múltiples dispositivos
- Verificar 401 con tokens anteriores
- Email recibido
- Auditoria registrada
- Redirección correcta
Frontend (FE)
-
Componente
LogoutAllButton.tsx- Botón de logout global
- En Settings > Security > Sessions
-
Modal
LogoutAllConfirmationModal.tsx- Mostrar lista de sesiones que se cerrarán
- Advertencia clara
- Botones Cancelar/Confirmar
-
Integración en
Sessions.tsx- Agregar botón y modal
- Manejar respuesta de API
- Mostrar mensaje de éxito
- Redirigir a login
-
Servicio
AuthService- Método
logoutAll(): Promise<{ success: boolean }> - Llamar a POST /api/v1/auth/logout-all
- Limpiar tokens locales
- Redirigir a login
- Método
-
Tests con React Testing Library
- Renderizar componente
- Click en botón
- Mostrar modal
- Enviar confirmación
- Verificar redirección
Testing (QA)
- E2E: Logout global exitoso
- E2E: Modal de confirmación
- E2E: Email recibido
- E2E: Tokens invalidados en todos los dispositivos
- E2E: Redirección a login
- E2E: Auditoria registrada
- Test de concurrencia: Múltiples logouts globales simultáneos
- Test de seguridad: Tokens antiguos no funcionan
- Performance: Logout global < 500ms
Dependencias
-
Bloqueantes:
- US-AUTH-012: Gestión de Sesiones (para tablas y flujos)
- Servicio de email configurado
-
Relacionadas:
- US-AUTH-002: Login genera tokens
- US-AUTH-014: Cambio de contraseña por seguridad
Definition of Ready (DoR)
- Mockups aprobados
- API contract definido
- Estrategia de invalidación de tokens definida
- Template de email diseñado
- Esquema de auditoria revisado
Definition of Done (DoD)
- Código implementado y revisado
- Tests unitarios con 80%+ cobertura
- Tests de integración pasando
- Tests E2E implementados
- Email de notificación funcional
- Auditoria registrada correctamente
- Rate limiting configurado
- Tokens invalidados en todos los dispositivos
- Redirección a login funcional
- Documentación actualizada
- QA aprobado en staging
- Deploy a producción exitoso
Notas Técnicas
Estrategia de Invalidación de Tokens
Opción 1: Token Blacklist (Recomendado)
// En Redis, crear entrada para cada token invalidado
await redis.setex(
`token_blacklist:${jti}`,
tokenExpirationTime,
'revoked'
);
// En middleware, verificar si token está en blacklist
const isBlacklisted = await redis.exists(`token_blacklist:${jti}`);
if (isBlacklisted) {
throw new UnauthorizedException('Token revoked');
}
Opción 2: Token Version (Alternativa)
// En tabla users, guardar version_token
// Al hacer logout global, incrementar version
UPDATE users SET token_version = token_version + 1 WHERE id = ?
// En JWT, incluir token_version
const token = generateJWT({
sub: user.id,
token_version: user.token_version,
...
});
// En middleware, validar que token_version coincida
const user = await findUser(jti.sub);
if (token.token_version !== user.token_version) {
throw new UnauthorizedException('Token revoked');
}
Recomendamos Opción 1 (Redis) por precisión inmediata.
Flujo de Logout Global
1. Usuario hace click en "Cerrar sesión en todos los dispositivos"
↓
2. Frontend muestra modal de confirmación
↓
3. Usuario confirma
↓
4. Frontend envía POST /api/v1/auth/logout-all
↓
5. Backend:
a. Verifica autenticación
b. Busca todas las sesiones del usuario
c. Marca todas como 'closed'
d. Invalida todos los tokens en Redis/Cache
e. Registra acción en auditoria
f. Envía email de notificación
↓
6. Frontend recibe respuesta exitosa
↓
7. Frontend limpia tokens locales
↓
8. Frontend muestra mensaje "Sesión cerrada"
↓
9. Frontend redirige a /login después de 2 segundos
Rate Limiting
// Usar express-rate-limit o similar
const logoutAllLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 1, // 1 request por minuto
message: 'Solo puedes hacer logout global 1 vez por minuto'
});
app.post('/api/v1/auth/logout-all', logoutAllLimiter, logoutAllHandler);
Transacción en DB
async function logoutGlobal(userId: string) {
return await db.$transaction(async (tx) => {
// 1. Buscar sesiones
const sessions = await tx.userSessions.findMany({
where: { user_id: userId, status: 'active' }
});
// 2. Marcar como closed
await tx.userSessions.updateMany({
where: { user_id: userId, status: 'active' },
data: { status: 'closed', expires_at: new Date() }
});
// 3. Registrar auditoria
await tx.sessionAuditLog.create({
data: {
user_id: userId,
action: 'logout_global',
created_at: new Date()
}
});
return sessions.length;
});
}
Email Template
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.container { max-width: 600px; margin: 0 auto; }
.header { background-color: #f44336; color: white; padding: 20px; }
.content { padding: 20px; }
.warning { background-color: #fff3cd; padding: 10px; border-left: 4px solid #ffc107; }
.button { background-color: #2196f3; color: white; padding: 10px 20px; text-decoration: none; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Sesión cerrada en todos tus dispositivos</h1>
</div>
<div class="content">
<p>Hola {{ user_name }},</p>
<p>Se cerró sesión en TODOS tus dispositivos.</p>
<div class="warning">
<strong>Detalles:</strong><br>
Fecha: {{ date }} a las {{ time }} {{ timezone }}<br>
Ubicación: {{ location }}<br>
</div>
<p>¿Fuiste tú? Si reconoces esta actividad, puedes ignorar este email.</p>
<p>¿No fuiste tú? Considera cambiar tu contraseña inmediatamente:</p>
<a href="{{ password_reset_url }}" class="button">Cambiar contraseña</a>
</div>
</div>
</body>
</html>