- 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>
515 lines
19 KiB
Markdown
515 lines
19 KiB
Markdown
---
|
|
id: "US-AUTH-013"
|
|
title: "Logout Global"
|
|
type: "User Story"
|
|
status: "To Do"
|
|
priority: "Alta"
|
|
epic: "OQI-001"
|
|
story_points: 3
|
|
created_date: "2026-01-25"
|
|
updated_date: "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](../_MAP.md)
|
|
|
|
---
|
|
|
|
## 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:
|
|
1. Invalidar TODOS los tokens JWT de todas las sesiones
|
|
2. Invalidar TODOS los refresh tokens
|
|
3. Marcar todas las sesiones como "closed" en la DB
|
|
4. 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:
|
|
1. Mostrar mensaje "Sesión cerrada en todos los dispositivos"
|
|
2. Redirigir a la página de login después de 2 segundos
|
|
3. Limpiar localStorage y cookies
|
|
4. 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')
|
|
|
|
- [ ] Tabla `session_audit_log`:
|
|
```sql
|
|
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 `LogoutService`
|
|
- `logoutGlobal(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()`
|
|
|
|
- [ ] Logging y auditoria:
|
|
- Registrar en `session_audit_log`
|
|
- Incluir: timestamp, user_id, acción, IP, user_agent
|
|
- Service: `AuditService.logLogoutAll()`
|
|
|
|
- [ ] 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
|
|
|
|
- [ ] 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)**
|
|
```typescript
|
|
// 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)**
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```html
|
|
<!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>
|
|
```
|
|
|
|
---
|
|
|
|
## Requerimientos Relacionados
|
|
|
|
- [RF-AUTH-007: Logout Global](../requerimientos/RF-AUTH-007-logout-global.md)
|
|
|
|
## Especificaciones Relacionadas
|
|
|
|
- [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md)
|
|
- [ET-AUTH-003: Database](../especificaciones/ET-AUTH-003-database.md)
|
|
- [ET-AUTH-006: Session Management](../especificaciones/ET-AUTH-006-sessions.md)
|