# RF-AUTH-002: Estados de Cuenta de Usuario ## 📋 Metadata | Campo | Valor | |-------|-------| | **ID** | RF-AUTH-002 | | **Épica** | MAI-001 - Fundamentos | | **Módulo** | Autenticación y Autorización | | **Prioridad** | Alta | | **Estado** | 🚧 Planificado | | **Versión** | 1.0 | | **Fecha creación** | 2025-11-17 | | **Última actualización** | 2025-11-17 | | **Esfuerzo estimado** | 12h (vs 15h GAMILIT - 20% ahorro por reutilización) | | **Story Points** | 5 SP | ## 🔗 Referencias ### Especificación Técnica 📐 [ET-AUTH-002: Gestión de Estados de Cuenta](../especificaciones/ET-AUTH-002-estados-cuenta.md) *(Pendiente)* ### Reutilización de Catálogo ♻️ **Reutilización:** 85% - **Catálogo de referencia:** `shared/catalog/auth/` *(Patrón estados de cuenta)* - **Diferencias clave:** - Estados adaptados a contexto de construcción - Casos de uso específicos para roles de obra - Integración con sistema de constructoras ### Implementación DDL 🗄️ **ENUM Canónico:** ```sql -- Location: apps/database/ddl/00-prerequisites.sql CREATE TYPE auth_management.user_status AS ENUM ( 'active', -- Usuario verificado, acceso completo 'inactive', -- Usuario desactivó su cuenta temporalmente 'suspended', -- Admin suspendió cuenta (reversible) 'banned', -- Admin baneó cuenta (permanente) 'pending' -- Email no verificado ); ``` 🗄️ **Tablas que usan el ENUM:** 1. `auth_management.profiles` - Columna: `status auth_management.user_status NOT NULL DEFAULT 'pending'` 2. `auth_management.user_constructoras` - Columna: `status auth_management.user_status NOT NULL DEFAULT 'active'` - **Nota:** Permite diferentes estados por constructora 🗄️ **Funciones:** ```sql -- Validar estado del usuario CREATE FUNCTION auth_management.verify_user_status( p_user_id UUID, p_constructora_id UUID ) RETURNS BOOLEAN; -- Suspender usuario CREATE FUNCTION auth_management.suspend_user( p_user_id UUID, p_reason TEXT, p_duration_days INTEGER, p_suspended_by UUID ) RETURNS VOID; -- Banear usuario permanentemente CREATE FUNCTION auth_management.ban_user( p_user_id UUID, p_reason TEXT, p_banned_by UUID ) RETURNS VOID; -- Reactivar usuario (desde inactive o suspended) CREATE FUNCTION auth_management.reactivate_user( p_user_id UUID, p_reactivated_by UUID ) RETURNS VOID; ``` 🗄️ **Triggers:** ```sql -- Auditar cambios de estado CREATE TRIGGER trg_profiles_status_change AFTER UPDATE OF status ON auth_management.profiles FOR EACH ROW WHEN (OLD.status IS DISTINCT FROM NEW.status) EXECUTE FUNCTION audit_logging.log_status_change(); ``` ### Backend 💻 **Implementación:** - **Enum:** `apps/backend/src/modules/auth/enums/user-status.enum.ts` ```typescript export enum UserStatus { ACTIVE = 'active', INACTIVE = 'inactive', SUSPENDED = 'suspended', BANNED = 'banned', PENDING = 'pending', } ``` - **Middleware:** `apps/backend/src/modules/auth/middleware/user-status.middleware.ts` - **Service:** `apps/backend/src/modules/auth/services/user-management.service.ts` - **DTOs:** - `apps/backend/src/modules/auth/dto/update-user-status.dto.ts` - `apps/backend/src/modules/auth/dto/suspend-user.dto.ts` ### Frontend 🎨 **Componentes:** - **Types:** `apps/frontend/src/types/auth.types.ts` ```typescript interface UserProfile { id: string; email: string; status: UserStatus; role: ConstructionRole; constructoraId: string; // ...otros campos } ``` - **Componentes:** - `apps/frontend/src/components/ui/UserStatusBadge.tsx` - Badge visual del estado - `apps/frontend/src/features/admin/UserManagementPanel.tsx` - Panel de gestión - `apps/frontend/src/features/admin/SuspendUserModal.tsx` - Modal para suspender - `apps/frontend/src/features/auth/AccountStatusPage.tsx` - Página de estado de cuenta ### Trazabilidad 📊 [TRACEABILITY.yml](../implementacion/TRACEABILITY.yml#L45-L78) --- ## 📝 Descripción del Requerimiento ### Contexto En un sistema de gestión de obra, las cuentas de usuario requieren un control riguroso del ciclo de vida completo: - **Verificación inicial:** Usuario invitado debe verificar su email antes de acceder - **Seguridad:** Suspender cuentas comprometidas o con comportamiento inadecuado - **Compliance:** Dar de baja usuarios que ya no trabajan en la constructora - **Flexibilidad:** Permitir desactivación temporal voluntaria - **Multi-tenancy:** Un usuario puede tener diferentes estados en diferentes constructoras ### Necesidad del Negocio **Problema:** Sin un sistema de estados bien definido: - ❌ No se puede verificar email antes de dar acceso a datos sensibles de obra - ❌ No hay forma de suspender temporalmente usuarios problemáticos - ❌ No se puede diferenciar entre baja temporal (inactivo) y baja permanente (baneado) - ❌ Empleados que ya no trabajan siguen teniendo acceso a información confidencial - ❌ No hay auditoría de cambios de estado **Solución:** Implementar un sistema de 5 estados que modela el ciclo de vida completo, con transiciones controladas, auditadas y específicas para construcción. --- ## 🎯 Requerimiento Funcional ### RF-AUTH-002.1: Estados Disponibles El sistema **DEBE** soportar exactamente 5 estados de cuenta: #### 1. Pendiente (`pending`) **Descripción:** Cuenta recién creada por invitación, email no verificado **Características:** - Estado inicial al ser invitado a una constructora - Usuario no puede acceder al sistema - Se envía email de verificación automáticamente - ⏱️ **Expira después de 7 días** si no se verifica - 🔒 No tiene acceso a datos de obra **Acceso Permitido:** - ❌ Dashboard - ❌ Proyectos/Obras - ❌ Módulos del sistema - ✅ Página de verificación de email - ✅ Reenviar email de verificación **Transiciones:** - → `active`: Al verificar email exitosamente - → ∅ (eliminación automática): Después de 7 días sin verificar **Caso de Uso Típico:** > Residente de Obra es invitado por el Director. Recibe email, tiene 7 días para verificar antes de que la invitación expire. --- #### 2. Activo (`active`) **Descripción:** Cuenta verificada, acceso completo según rol asignado **Características:** - ✅ Email verificado exitosamente - ✅ Acceso completo según rol (director, engineer, resident, etc.) - ✅ Puede usar app móvil de asistencias - ✅ Recibe notificaciones del sistema - ✅ Aparece en búsquedas de usuarios **Acceso Permitido:** - ✅ Todo el sistema según permisos de rol - ✅ Dashboard personalizado por rol - ✅ Módulos asignados (proyectos, presupuestos, RRHH, etc.) - ✅ App móvil (si tiene rol de resident o hr) **Transiciones:** - → `inactive`: Usuario desactiva su propia cuenta - → `suspended`: Admin suspende la cuenta (reversible) - → `banned`: Admin banea la cuenta (irreversible) **Caso de Uso Típico:** > Ingeniero accede diariamente para revisar presupuestos, actualizar programación y aprobar requisiciones. --- #### 3. Inactivo (`inactive`) **Descripción:** Usuario desactivó temporalmente su cuenta (voluntario) **Características:** - 🔵 Desactivación **voluntaria** por el usuario - 💾 Datos conservados intactos - 🔄 **Reversible** por el propio usuario en cualquier momento - 📧 No recibe notificaciones - 🚫 No aparece en búsquedas de usuarios **Acceso Permitido:** - ❌ Dashboard y funcionalidades principales - ❌ Módulos del sistema - ✅ Página de reactivación de cuenta - ✅ Descargar datos personales (GDPR compliance) **Transiciones:** - → `active`: Usuario reactiva su cuenta (máximo 3 veces por día) - → ∅ (eliminación voluntaria): Usuario solicita eliminación de cuenta **Caso de Uso Típico:** > Residente que toma vacaciones de 2 semanas desactiva temporalmente su cuenta para no recibir notificaciones. **Rate Limiting:** - Máximo **3 reactivaciones por día** (prevenir abuso) --- #### 4. Suspendido (`suspended`) **Descripción:** Admin suspendió la cuenta (reversible) **Características:** - 🔴 Suspensión por admin debido a comportamiento inapropiado - 🔄 **REVERSIBLE** (diferencia clave con `banned`) - 📝 Requiere **razón documentada** obligatoria - 🔍 Requiere revisión de admin para levantar suspensión - 📬 Usuario recibe notificación con razón de suspensión **Acceso Permitido:** - ❌ Todo el sistema (bloqueado completamente) - ✅ Ver notificación de suspensión con razón - ✅ Contactar soporte **Restricciones:** - 🚫 No puede iniciar sesión - 📧 No recibe notificaciones del sistema - 🔍 No aparece en búsquedas de usuarios - 📱 App móvil bloqueada **Transiciones:** - → `active`: Admin levanta suspensión después de revisión - → `banned`: Admin decide hacer baneo permanente **Duración:** - Típicamente: **7-30 días** - Requiere revisión periódica por directores/admins **Razones Comunes (Construcción):** - Registro de asistencia fraudulento (check-in sin estar en obra) - Captura incorrecta de avances de obra - Modificación no autorizada de presupuestos - Comportamiento inapropiado en obra **Caso de Uso Típico:** > Residente registró asistencias falsas de empleados. Director lo suspende 14 días mientras investiga. --- #### 5. Baneado (`banned`) **Descripción:** Admin baneó la cuenta **permanentemente** (irreversible) **Características:** - 🔴 Baneo por violación **grave** de términos - ❌ **IRREVERSIBLE** (no hay transición de vuelta) - 📧 Email y username quedan **bloqueados permanentemente** - 💾 Datos se conservan por auditoría pero cuenta **inaccesible** - 📝 Requiere razón grave documentada **Acceso Permitido:** - ❌ Todo el sistema - ✅ Ver notificación de baneo con razón **Restricciones:** - 🚫 No puede iniciar sesión - 🚫 No puede crear nueva cuenta con mismo email - 🚫 Username queda reservado permanentemente - 🚫 No puede ser invitado nuevamente a ninguna constructora **Razones Comunes (Construcción):** - Fraude financiero (apropiación indebida de recursos) - Robo de información confidencial de la constructora - Suplantación de identidad (firmar como otro usuario) - Alteración de documentos oficiales (contratos, presupuestos) - Actividad criminal relacionada con la obra **Caso de Uso Típico:** > Empleado de compras desvió recursos, realizó órdenes de compra falsas. Director lo banea permanentemente y procede legalmente. --- ### RF-AUTH-002.2: Flujo de Estados ``` ┌──────────────────────┐ │ INVITACIÓN A │ │ CONSTRUCTORA │ └──────────┬───────────┘ │ ▼ pending ──────(verificar email)─────> active │ │ │ ├──(usuario desactiva)──> inactive │ │ │ │ │ └──(reactiva)──> active │ │ │ ├──(admin suspende)──> suspended │ │ │ │ │ ├──(admin levanta)──> active │ │ └──(admin decide)──> banned │ │ └────(7 días)───> ∅ └──(admin banea)──> banned (eliminación) │ └──(permanente)──> ∅ ``` **Leyenda:** - ∅ = Registro eliminado/cuenta no recuperable - Flechas sólidas = Transiciones permitidas - Flechas punteadas = Eliminación automática --- ### RF-AUTH-002.3: Reglas de Transición #### Transiciones Permitidas | Estado Actual | Puede Transicionar A | Quién Puede Hacerlo | Requiere | |---------------|---------------------|---------------------|----------| | `pending` | `active` | Usuario | Verificar email (click en link) | | `pending` | ∅ (eliminación) | Sistema | 7 días sin verificar | | `active` | `inactive` | Usuario | Confirmación + password | | `active` | `suspended` | director, super_admin | Razón documentada obligatoria | | `active` | `banned` | director, super_admin | Razón grave documentada + evidencia | | `inactive` | `active` | Usuario | Click en reactivar (máx 3/día) | | `suspended` | `active` | director, super_admin | Revisión completada + justificación | | `suspended` | `banned` | director, super_admin | Decisión justificada + evidencia | | `banned` | ∅ | N/A | **Irreversible** | #### Transiciones Prohibidas | Transición | Razón | |------------|-------| | ❌ `pending` → `suspended` | No tiene sentido suspender cuenta no verificada (simplemente eliminar invitación) | | ❌ `pending` → `banned` | No tiene sentido banear cuenta no verificada | | ❌ `inactive` → `suspended` | Usuario ya desactivó voluntariamente, no hay necesidad de suspender | | ❌ `inactive` → `banned` | Si se requiere baneo, reactivar primero y luego banear desde `active` | | ❌ `suspended` → `inactive` | Confusión de estados (uno es admin-driven, otro user-driven) | | ❌ `banned` → cualquier otro | **Irreversible por diseño** | --- ### RF-AUTH-002.4: Validación de Estado en Sistema Multi-tenancy **Importante:** En este sistema, un usuario puede pertenecer a **múltiples constructoras** con diferentes estados en cada una. #### Escenario Multi-tenancy ```typescript // Usuario puede tener diferentes estados en diferentes constructoras const userConstructoras = [ { constructoraId: 'A', status: 'active', role: 'director' }, { constructoraId: 'B', status: 'suspended', role: 'engineer' }, { constructoraId: 'C', status: 'active', role: 'resident' }, ]; // Al hacer login, usuario ve solo constructoras donde status = 'active' const availableConstructoras = userConstructoras.filter( uc => uc.status === 'active' ); // ['A', 'C'] ``` #### 1. Validación en Login ```typescript // POST /api/auth/login // Retorna solo constructoras donde user.status = 'active' async login(email: string, password: string) { const user = await this.findByEmail(email); // Validar estado global del perfil if (user.profile.status === 'banned') { throw new UnauthorizedException( 'Tu cuenta ha sido baneada permanentemente. Contacta soporte para más información.' ); } if (user.profile.status === 'pending') { throw new UnauthorizedException( 'Debes verificar tu email antes de acceder. Revisa tu bandeja de entrada.' ); } // Obtener constructoras donde el usuario está activo const activeConstructoras = await this.getActiveConstructoras(user.id); if (activeConstructoras.length === 0) { throw new UnauthorizedException( 'No tienes acceso activo a ninguna constructora. Contacta al administrador.' ); } return { user: user, accessToken: this.generateToken(user, activeConstructoras[0]), constructoras: activeConstructoras, // Usuario puede elegir }; } ``` #### 2. Middleware en Cada Request ```typescript // UserStatusMiddleware valida en CADA request autenticado @Injectable() export class UserStatusMiddleware implements NestMiddleware { use(req: any, res: any, next: () => void) { const user = req.user; // Incluye constructoraId del JWT // Excepciones: endpoints de reactivación y cambio de constructora const allowedPaths = [ '/auth/reactivate', '/auth/status', '/auth/switch-constructora', ]; if (allowedPaths.some(path => req.path.startsWith(path))) { return next(); } // Validar estado global if (user.profileStatus === 'banned') { throw new ForbiddenException('Cuenta baneada permanentemente.'); } if (user.profileStatus === 'pending') { throw new ForbiddenException('Email no verificado.'); } // Validar estado en constructora actual if (user.constructoraStatus !== 'active') { throw new ForbiddenException( `Tu acceso a esta constructora está ${user.constructoraStatus}. ` + `Contacta al administrador o cambia a otra constructora.` ); } next(); } } ``` #### 3. Frontend - Validación Temprana ```typescript // Redirigir según estado useEffect(() => { if (!user) return; // Estado global del perfil if (user.profile.status === 'pending') { navigate('/verify-email'); return; } if (user.profile.status === 'banned') { navigate('/account-banned'); return; } // Estado en constructora actual const currentConstructora = user.constructoras.find( c => c.id === user.currentConstructoraId ); if (!currentConstructora) { navigate('/select-constructora'); return; } if (currentConstructora.status === 'suspended') { navigate('/account-suspended-in-constructora'); return; } if (currentConstructora.status === 'inactive') { navigate('/reactivate-account'); return; } // Estado activo, continuar }, [user, navigate]); ``` --- ## 📊 Casos de Uso ### UC-AUTH-003: Usuario verifica email tras invitación **Actor:** Residente de Obra (nuevo usuario) **Precondiciones:** - Usuario invitado a constructora por Director - Email de invitación enviado - `profiles.status` = `pending` **Flujo Principal:** 1. Usuario recibe email de invitación a constructora "Constructora ABC" 2. Usuario hace click en link de verificación 3. Sistema valida token de verificación (válido por 24h) 4. Sistema actualiza `profiles.status` de `pending` → `active` 5. Sistema actualiza `user_constructoras.status` de `pending` → `active` 6. Sistema audita cambio en `audit_logs`: ```json { "action": "update", "resource_type": "user_status", "resource_id": "user_id", "details": { "old_status": "pending", "new_status": "active", "constructora_id": "abc-123", "verified_at": "2025-11-17T10:30:00Z" } } ``` 7. Sistema envía email de bienvenida con instrucciones 8. Usuario redirigido a `/auth/login` 9. Usuario hace login y selecciona constructora "Constructora ABC" 10. Usuario accede a dashboard según su rol (resident) **Resultado:** Usuario con status `active` puede acceder al sistema **Variantes:** - **V1: Token expirado (>24h):** 1. Sistema detecta token expirado 2. Sistema muestra mensaje: "Link expirado" 3. Sistema ofrece botón "Reenviar email de verificación" 4. Usuario hace click 5. Sistema envía nuevo email con nuevo token 6. Volver a flujo principal paso 2 - **V2: Token inválido:** 1. Sistema detecta token inválido 2. Sistema muestra error: "Link inválido" 3. Sistema ofrece contactar soporte 4. Sistema audita intento de verificación inválido - **V3: Usuario ya verificado:** 1. Sistema detecta que `status` ya es `active` 2. Sistema muestra: "Ya has verificado tu email" 3. Sistema redirige a login --- ### UC-ADMIN-002: Director suspende cuenta de Residente **Actor:** Director de Construcción **Precondiciones:** - Usuario con rol `director` - Residente con status `active` - Comportamiento inapropiado detectado (ej: asistencias falsas) **Flujo Principal:** 1. Director navega a **Panel de Gestión de Usuarios** (`/admin/users`) 2. Director filtra usuarios por constructora actual 3. Director busca residente por nombre: "Juan Pérez" 4. Director hace click en botón "Acciones" → "Suspender cuenta" 5. Sistema muestra **Modal de Suspensión** con formulario: - **Razón** (textarea obligatorio, min 20 caracteres) - **Duración sugerida** (select: 7 días, 14 días, 30 días, Indefinido) - **Evidencia** (opcional: subir screenshots, documentos) - **Fecha de revisión** (automática según duración) 6. Director completa formulario: - Razón: "Registró asistencias de empleados que no estaban en obra según GPS" - Duración: 14 días - Evidencia: Sube screenshots de GPS 7. Director hace click en "Confirmar Suspensión" 8. Sistema valida: - Razón tiene mínimo 20 caracteres ✅ - Usuario tiene permisos (rol = director) ✅ - Usuario a suspender no es director ✅ (no puede suspender otro director) 9. Sistema ejecuta transacción: ```sql BEGIN; -- Actualizar estado UPDATE auth_management.user_constructoras SET status = 'suspended', suspended_at = NOW(), suspended_by = :director_id, suspended_reason = :reason, suspended_until = NOW() + INTERVAL '14 days' WHERE user_id = :resident_id AND constructora_id = :constructora_id; -- Auditar INSERT INTO audit_logging.audit_logs ( action, resource_type, resource_id, performed_by, details ) VALUES ( 'update', 'user_status', :resident_id, :director_id, jsonb_build_object( 'old_status', 'active', 'new_status', 'suspended', 'reason', :reason, 'evidence_urls', :evidence_urls, 'suspended_by_name', 'Director López', 'review_date', NOW() + INTERVAL '14 days', 'constructora_id', :constructora_id ) ); COMMIT; ``` 10. Sistema cierra todas las sesiones activas del residente en esa constructora 11. Sistema envía **Notificación Push + Email** al residente: ``` Asunto: Tu cuenta ha sido suspendida temporalmente Hola Juan, Tu cuenta en Constructora ABC ha sido suspendida por 14 días. Razón: Registró asistencias de empleados que no estaban en obra según GPS Fecha de revisión: 2025-12-01 Si crees que esto es un error, puedes contactar a soporte en soporte@constructora.com --- Sistema de Gestión de Obra ``` 12. Sistema registra en timeline del usuario el evento de suspensión 13. Sistema muestra confirmación al director: "Usuario suspendido exitosamente" **Resultado:** - Residente no puede acceder a esa constructora por 14 días - Director puede revisar suspensión antes de la fecha - Suspensión queda auditada con evidencia **Variantes:** - **V1: Director intenta suspender otro director:** - Sistema lanza error: "No puedes suspender a otro director" - Solo super_admin puede suspender directores - **V2: Residente ya está suspendido:** - Sistema detecta status = 'suspended' - Sistema muestra opciones: - Levantar suspensión - Extender suspensión - Convertir en baneo **Postcondiciones:** - `user_constructoras.status` = 'suspended' - Registro en `audit_logs` - Email enviado - Sesiones cerradas --- ### UC-AUTH-004: Usuario desactiva su propia cuenta **Actor:** Ingeniero **Precondiciones:** Usuario con status `active` **Flujo Principal:** 1. Ingeniero navega a **Configuración** → **Mi Cuenta** (`/settings/account`) 2. Ingeniero hace scroll hasta sección "Zona Peligrosa" 3. Ingeniero hace click en botón "Desactivar mi cuenta" 4. Sistema muestra **Modal de Confirmación** con: - **Título:** "¿Estás seguro que quieres desactivar tu cuenta?" - **Explicación:** - ✅ Esta acción es temporal - ✅ Tus datos NO se eliminarán - ✅ Puedes reactivar en cualquier momento - ⚠️ No recibirás notificaciones mientras esté desactivada - ⚠️ No aparecerás en búsquedas de usuarios - **Campo:** Contraseña (requerido para confirmar) 5. Ingeniero ingresa su contraseña 6. Ingeniero hace click en "Confirmar Desactivación" 7. Sistema valida contraseña ✅ 8. Sistema actualiza `profiles.status` de `active` → `inactive` 9. Sistema audita cambio: ```json { "action": "update", "resource_type": "user_status", "resource_id": "user_id", "details": { "old_status": "active", "new_status": "inactive", "deactivated_by": "self", "reason": "voluntary_deactivation", "deactivated_at": "2025-11-17T15:45:00Z" } } ``` 10. Sistema envía email de confirmación: ``` Asunto: Cuenta desactivada temporalmente Hola, Tu cuenta ha sido desactivada exitosamente. Para reactivarla, simplemente haz login de nuevo en: https://app.constructora.com/auth/login Y haz click en "Reactivar cuenta". --- Sistema de Gestión de Obra ``` 11. Sistema cierra sesión del usuario (`logout`) 12. Usuario redirigido a página de login con mensaje: > "Tu cuenta ha sido desactivada. Puedes reactivarla en cualquier momento haciendo login." **Resultado:** Cuenta desactivada temporalmente, usuario puede reactivar cuando quiera **Reactivación (Flujo Secundario):** 1. Usuario navega a `/auth/login` 2. Usuario ingresa email + password 3. Sistema detecta `status` = `inactive` 4. Sistema muestra página de reactivación con: - Mensaje: "Tu cuenta está desactivada temporalmente" - Botón grande: "Reactivar mi cuenta" 5. Usuario hace click en "Reactivar mi cuenta" 6. Sistema valida rate limiting (máximo 3 reactivaciones por día) 7. Sistema actualiza `status` de `inactive` → `active` 8. Sistema audita reactivación 9. Sistema muestra: "Cuenta reactivada exitosamente" 10. Usuario redirigido a dashboard **Postcondiciones:** - `profiles.status` = 'inactive' - Email enviado - Sesión cerrada --- ### UC-ADMIN-003: Director banea cuenta permanentemente **Actor:** Director **Precondiciones:** - Usuario con rol `director` - Empleado de compras con status `active` o `suspended` - Violación grave detectada (fraude) **Flujo Principal:** 1. Director detecta fraude: Empleado de compras creó órdenes de compra falsas por $500,000 MXN 2. Director navega a **Panel de Gestión de Usuarios** → Usuario "Carlos Ramírez" 3. Director hace click en "Acciones" → "Banear cuenta permanentemente" 4. Sistema muestra **Modal de Baneo** con advertencia: ``` ⚠️ ADVERTENCIA: Esta acción es IRREVERSIBLE El usuario NO podrá: - Acceder al sistema nunca más - Crear nueva cuenta con este email - Ser invitado a ninguna constructora Esta acción debe reservarse para violaciones GRAVES: - Fraude financiero - Robo de información - Actividad criminal ¿Estás completamente seguro? ``` 5. Sistema requiere: - **Razón grave** (textarea obligatorio, min 50 caracteres) - **Evidencia** (obligatorio: mínimo 1 archivo) - **Confirmación:** Usuario debe escribir "BANEAR PERMANENTEMENTE" 6. Director completa: - Razón: "Empleado creó órdenes de compra falsas por $500,000 MXN a proveedores ficticios. Se detectó desvío de recursos. Se procederá legalmente." - Evidencia: Sube PDFs de órdenes falsas, capturas de pantalla, reporte de auditoría - Confirmación: Escribe "BANEAR PERMANENTEMENTE" 7. Director hace click en "Confirmar Baneo" 8. Sistema ejecuta: ```sql BEGIN; -- Banear en todas las constructoras UPDATE auth_management.profiles SET status = 'banned', banned_at = NOW(), banned_by = :director_id, banned_reason = :reason WHERE id = :user_id; -- Marcar email como bloqueado INSERT INTO auth_management.banned_emails (email, reason, banned_by) VALUES (:user_email, :reason, :director_id); -- Auditar con máxima prioridad INSERT INTO audit_logging.audit_logs ( action, resource_type, resource_id, performed_by, details, priority ) VALUES ( 'ban', 'user_status', :user_id, :director_id, jsonb_build_object( 'old_status', 'active', 'new_status', 'banned', 'reason', :reason, 'evidence_urls', :evidence_urls, 'banned_by_name', 'Director López', 'legal_action', true ), 'critical' ); -- Cerrar sesiones en TODAS las constructoras DELETE FROM auth_management.user_sessions WHERE user_id = :user_id; COMMIT; ``` 9. Sistema envía notificación CRÍTICA a: - Usuario baneado (email) - Todos los directores de todas las constructoras donde estaba el usuario - Super admins del sistema 10. Sistema envía email al usuario baneado: ``` Asunto: Cuenta baneada permanentemente Tu cuenta ha sido baneada permanentemente. Razón: [Razón documentada] Esta acción es irreversible. No podrás acceder al sistema ni crear nueva cuenta. Si tienes preguntas, contacta a legal@constructora.com ``` **Resultado:** - Usuario baneado en TODAS las constructoras - Email bloqueado permanentemente - Username reservado permanentemente - No puede ser invitado nunca más **Postcondiciones:** - `profiles.status` = 'banned' - `banned_emails` contiene email - Sesiones cerradas en todas las constructoras - Notificaciones críticas enviadas --- ## 🔐 Consideraciones de Seguridad ### 1. Prevención de Bypass de Estado **Problema:** Usuario suspendido podría intentar acceder vía API directamente, saltándose frontend **Solución: Middleware en CADA request autenticado** ```typescript // apps/backend/src/modules/auth/middleware/user-status.middleware.ts import { Injectable, NestMiddleware, ForbiddenException } from '@nestjs/common'; import { UserStatus } from '../enums/user-status.enum'; @Injectable() export class UserStatusMiddleware implements NestMiddleware { use(req: any, res: any, next: () => void) { const user = req.user; // Inyectado por JwtAuthGuard // Excepciones: endpoints que permiten ciertos estados const allowedPaths = [ '/auth/reactivate', // inactive puede reactivar '/auth/status', // consultar estado '/auth/switch-constructora', // cambiar constructora '/auth/download-data', // GDPR: inactive puede descargar datos ]; if (allowedPaths.some(path => req.path.startsWith(path))) { return next(); } // Validar estado global del perfil if (user.profileStatus === 'banned') { throw new ForbiddenException({ statusCode: 403, message: 'Tu cuenta ha sido baneada permanentemente.', errorCode: 'ACCOUNT_BANNED', contactSupport: true, }); } if (user.profileStatus === 'pending') { throw new ForbiddenException({ statusCode: 403, message: 'Debes verificar tu email antes de acceder.', errorCode: 'EMAIL_NOT_VERIFIED', action: 'verify_email', }); } // Validar estado en constructora actual if (user.constructoraStatus !== UserStatus.ACTIVE) { throw new ForbiddenException({ statusCode: 403, message: `Tu acceso a esta constructora está ${user.constructoraStatus}.`, errorCode: 'CONSTRUCTORA_ACCESS_DENIED', constructoraId: user.constructoraId, status: user.constructoraStatus, }); } next(); } } ``` **Aplicación global:** ```typescript // apps/backend/src/main.ts app.use(UserStatusMiddleware); // Aplica a TODOS los endpoints ``` --- ### 2. Auditoría Obligatoria de Cambios de Estado **Problema:** Cambios de estado sin auditar = no hay trazabilidad **Solución: Trigger automático en base de datos** ```sql -- apps/database/ddl/schemas/auth_management/triggers/audit-status-change.sql CREATE OR REPLACE FUNCTION audit_logging.log_status_change() RETURNS TRIGGER AS $$ BEGIN -- Solo auditar si cambió el estado IF OLD.status IS DISTINCT FROM NEW.status THEN INSERT INTO audit_logging.audit_logs ( action, resource_type, resource_id, performed_by, details, priority, ip_address ) VALUES ( CASE NEW.status WHEN 'suspended' THEN 'suspend' WHEN 'banned' THEN 'ban' WHEN 'active' THEN 'reactivate' WHEN 'inactive' THEN 'deactivate' ELSE 'update' END, 'user_status', NEW.id, COALESCE(current_setting('app.current_user_id', true)::UUID, NEW.id), jsonb_build_object( 'old_status', OLD.status, 'new_status', NEW.status, 'reason', NEW.suspended_reason, 'table', TG_TABLE_NAME, 'timestamp', NOW() ), CASE NEW.status WHEN 'banned' THEN 'critical' WHEN 'suspended' THEN 'high' ELSE 'medium' END, inet_client_addr() ); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Aplicar a profiles CREATE TRIGGER trg_audit_status_change_profiles AFTER UPDATE OF status ON auth_management.profiles FOR EACH ROW EXECUTE FUNCTION audit_logging.log_status_change(); -- Aplicar a user_constructoras CREATE TRIGGER trg_audit_status_change_constructoras AFTER UPDATE OF status ON auth_management.user_constructoras FOR EACH ROW EXECUTE FUNCTION audit_logging.log_status_change(); ``` **Resultado:** TODO cambio de estado queda auditado automáticamente --- ### 3. Rate Limiting en Reactivación **Problema:** Usuario abusa de desactivar/reactivar repetidamente **Solución: Limitar reactivaciones a 3 por día** ```typescript // apps/backend/src/modules/auth/services/user-management.service.ts import { TooManyRequestsException } from '@nestjs/common'; async reactivateAccount(userId: string): Promise { // Contar reactivaciones hoy const today = new Date(); today.setHours(0, 0, 0, 0); const reactivationCount = await this.auditLogRepository.count({ where: { resourceId: userId, action: 'reactivate', createdAt: MoreThan(today), }, }); // Limitar a 3 reactivaciones por día if (reactivationCount >= 3) { throw new TooManyRequestsException({ statusCode: 429, message: 'Has alcanzado el límite de reactivaciones por hoy (3 máximo).', errorCode: 'TOO_MANY_REACTIVATIONS', retryAfter: this.getSecondsUntilMidnight(), }); } // Proceder con reactivación await this.profileRepository.update( { id: userId }, { status: UserStatus.ACTIVE } ); } private getSecondsUntilMidnight(): number { const now = new Date(); const midnight = new Date(now); midnight.setHours(24, 0, 0, 0); return Math.floor((midnight.getTime() - now.getTime()) / 1000); } ``` --- ### 4. Notificación Obligatoria al Usuario **Problema:** Usuario no sabe por qué cambió su estado **Solución: Notificación automática en todo cambio de estado** ```typescript // apps/backend/src/modules/auth/services/user-status-notification.service.ts import { Injectable } from '@nestjs/common'; import { NotificationService } from '../../notifications/notification.service'; import { EmailService } from '../../email/email.service'; @Injectable() export class UserStatusNotificationService { constructor( private readonly notificationService: NotificationService, private readonly emailService: EmailService, ) {} async notifyStatusChange( userId: string, oldStatus: UserStatus, newStatus: UserStatus, reason: string, changedBy: string, ): Promise { // Solo notificar si el cambio NO fue iniciado por el propio usuario if (changedBy !== userId) { // Notificación in-app (push) await this.notificationService.send({ userId, type: 'system_announcement', priority: newStatus === 'banned' ? 'critical' : 'high', title: this.getNotificationTitle(newStatus), body: reason, icon: this.getNotificationIcon(newStatus), actions: this.getNotificationActions(newStatus), }); // Email await this.emailService.send({ to: await this.getUserEmail(userId), subject: this.getEmailSubject(newStatus), template: 'account-status-changed', data: { newStatus, oldStatus, reason, changedByName: await this.getUserName(changedBy), supportEmail: 'soporte@constructora.com', }, }); } } private getNotificationTitle(status: UserStatus): string { switch (status) { case UserStatus.SUSPENDED: return 'Tu cuenta ha sido suspendida temporalmente'; case UserStatus.BANNED: return 'Tu cuenta ha sido baneada permanentemente'; case UserStatus.ACTIVE: return 'Tu cuenta ha sido reactivada'; case UserStatus.INACTIVE: return 'Tu cuenta ha sido desactivada'; default: return 'Tu estado de cuenta ha cambiado'; } } private getNotificationIcon(status: UserStatus): string { switch (status) { case UserStatus.SUSPENDED: return '⚠️'; case UserStatus.BANNED: return '🚫'; case UserStatus.ACTIVE: return '✅'; case UserStatus.INACTIVE: return 'ℹ️'; default: return '🔔'; } } private getNotificationActions(status: UserStatus): any[] { switch (status) { case UserStatus.SUSPENDED: return [{ label: 'Contactar Soporte', action: 'contact_support' }]; case UserStatus.BANNED: return [{ label: 'Ver Detalles', action: 'view_ban_details' }]; case UserStatus.INACTIVE: return [{ label: 'Reactivar Cuenta', action: 'reactivate_account' }]; default: return []; } } } ``` --- ### 5. Prevención de Registro con Email Baneado **Problema:** Usuario baneado intenta crear nueva cuenta con mismo email **Solución: Validar email contra tabla `banned_emails` en registro** ```typescript // apps/backend/src/modules/auth/services/auth.service.ts async registerByInvitation(token: string, password: string): Promise { const invitation = await this.validateInvitationToken(token); // Verificar si el email está baneado const isBanned = await this.bannedEmailRepository.findOne({ where: { email: invitation.email }, }); if (isBanned) { throw new ForbiddenException({ statusCode: 403, message: 'Este email está bloqueado y no puede crear una cuenta.', errorCode: 'EMAIL_BANNED', contactSupport: true, bannedReason: isBanned.reason, }); } // Continuar con registro... } ``` ```sql -- apps/database/ddl/schemas/auth_management/tables/banned-emails.sql CREATE TABLE auth_management.banned_emails ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email VARCHAR(255) UNIQUE NOT NULL, reason TEXT NOT NULL, banned_by UUID REFERENCES auth_management.profiles(id), banned_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_banned_emails_email ON auth_management.banned_emails(email); ``` --- ## ✅ Criterios de Aceptación ### AC-001: ENUM Implementado - [ ] ENUM `user_status` existe en schema `auth_management` - [ ] ENUM tiene exactamente 5 valores: `active`, `inactive`, `suspended`, `banned`, `pending` - [ ] Columna `profiles.status` usa el ENUM con default `'pending'` - [ ] Columna `user_constructoras.status` usa el ENUM con default `'active'` ### AC-002: Transiciones Validadas - [ ] Solo transiciones permitidas pueden ejecutarse (validación en service) - [ ] Transiciones prohibidas lanzan exception con mensaje claro - [ ] Trigger audita TODOS los cambios de estado automáticamente - [ ] Razón es obligatoria para `suspended` y `banned` - [ ] Evidencia es obligatoria para `banned` ### AC-003: Middleware Activo - [ ] `UserStatusMiddleware` aplicado globalmente en backend - [ ] Middleware valida estado en CADA request autenticado - [ ] Excepciones configuradas para paths específicos (`/auth/reactivate`, etc.) - [ ] Frontend redirige según status (pending → verify-email, banned → account-banned) - [ ] Usuario suspendido no puede acceder ni vía API ni vía UI ### AC-004: Notificaciones Enviadas - [ ] Usuario recibe notificación push al cambiar status - [ ] Usuario recibe email con razón y próximos pasos - [ ] Notificación visible en UI con icono correcto - [ ] Email incluye información de contacto (soporte, legal) ### AC-005: Admin Panel Funcional - [ ] Director puede suspender usuarios (excepto otros directores) - [ ] Director puede banear usuarios permanentemente - [ ] Razón es campo obligatorio (min 20 caracteres para suspend, 50 para ban) - [ ] Evidencia es obligatoria para baneo - [ ] Historial de cambios visible en perfil de usuario - [ ] Director puede levantar suspensión con justificación ### AC-006: Usuario Puede Desactivar/Reactivar - [ ] Botón "Desactivar cuenta" visible en `/settings/account` - [ ] Modal de confirmación requiere contraseña - [ ] Reactivación simple desde login con botón "Reactivar cuenta" - [ ] Rate limiting: máximo 3 reactivaciones por día - [ ] Email de confirmación enviado al desactivar ### AC-007: Multi-tenancy Soportado - [ ] Usuario puede tener diferentes estados en diferentes constructoras - [ ] Login muestra solo constructoras donde status = 'active' - [ ] Suspensión en una constructora no afecta otras constructoras - [ ] Baneo global afecta TODAS las constructoras ### AC-008: Email Baneado Bloqueado - [ ] Tabla `banned_emails` creada - [ ] Registro valida email contra `banned_emails` - [ ] Email baneado no puede crear nueva cuenta - [ ] Error claro al intentar registro con email baneado --- ## 🧪 Testing ### Test Suite: User Status Management #### Test Case 1: Usuario pending no puede acceder ```typescript // apps/backend/src/modules/auth/tests/user-status.spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, HttpStatus } from '@nestjs/common'; import * as request from 'supertest'; describe('UserStatusMiddleware - Pending Users', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('should block pending user from accessing protected endpoints', async () => { // Arrange: Crear usuario con status pending const user = await createTestUser({ email: 'pending@test.com', status: UserStatus.PENDING, }); const token = await generateAuthToken(user); // Act: Intentar acceder a dashboard const response = await request(app.getHttpServer()) .get('/dashboard') .set('Authorization', `Bearer ${token}`) .expect(HttpStatus.FORBIDDEN); // Assert expect(response.body.message).toContain('verificar tu email'); expect(response.body.errorCode).toBe('EMAIL_NOT_VERIFIED'); }); it('should allow pending user to access /auth/resend-verification', async () => { const user = await createTestUser({ status: UserStatus.PENDING }); const token = await generateAuthToken(user); // Endpoint de excepción debe funcionar const response = await request(app.getHttpServer()) .post('/auth/resend-verification') .set('Authorization', `Bearer ${token}`) .expect(HttpStatus.OK); expect(response.body.message).toContain('Email enviado'); }); }); ``` --- #### Test Case 2: Director puede suspender usuario ```typescript describe('UserManagementService - Suspend User', () => { it('should allow director to suspend resident with reason', async () => { // Arrange const director = await createTestUser({ role: ConstructionRole.DIRECTOR, constructoraId: 'constructora-a', }); const resident = await createTestUser({ role: ConstructionRole.RESIDENT, status: UserStatus.ACTIVE, constructoraId: 'constructora-a', }); await loginAs(director); // Act const response = await request(app.getHttpServer()) .post(`/admin/users/${resident.id}/suspend`) .send({ reason: 'Registró asistencias falsas de empleados', durationDays: 14, evidence: ['https://s3.aws.com/screenshot1.png'], }) .expect(HttpStatus.OK); // Assert expect(response.body.message).toContain('suspendido exitosamente'); // Verificar estado en DB const updatedResident = await getUserById(resident.id); expect(updatedResident.status).toBe(UserStatus.SUSPENDED); expect(updatedResident.suspendedReason).toBe('Registró asistencias falsas de empleados'); // Verificar auditoría const auditLog = await getLatestAuditLog(resident.id, 'user_status'); expect(auditLog.action).toBe('suspend'); expect(auditLog.performedBy).toBe(director.id); expect(auditLog.details.reason).toBe('Registró asistencias falsas de empleados'); expect(auditLog.priority).toBe('high'); }); it('should NOT allow director to suspend another director', async () => { const director1 = await createTestUser({ role: ConstructionRole.DIRECTOR }); const director2 = await createTestUser({ role: ConstructionRole.DIRECTOR }); await loginAs(director1); const response = await request(app.getHttpServer()) .post(`/admin/users/${director2.id}/suspend`) .send({ reason: 'Test', durationDays: 7 }) .expect(HttpStatus.FORBIDDEN); expect(response.body.message).toContain('No puedes suspender a otro director'); }); it('should require reason with minimum length', async () => { const director = await createTestUser({ role: ConstructionRole.DIRECTOR }); const resident = await createTestUser({ role: ConstructionRole.RESIDENT }); await loginAs(director); const response = await request(app.getHttpServer()) .post(`/admin/users/${resident.id}/suspend`) .send({ reason: 'Test', // Muy corto (< 20 caracteres) durationDays: 7, }) .expect(HttpStatus.BAD_REQUEST); expect(response.body.message).toContain('Razón debe tener mínimo 20 caracteres'); }); }); ``` --- #### Test Case 3: Usuario puede desactivar y reactivar ```typescript describe('UserAccountService - Deactivate/Reactivate', () => { it('should allow user to deactivate their own account', async () => { // Arrange const user = await createTestUser({ email: 'engineer@test.com', password: 'password123', status: UserStatus.ACTIVE, }); await loginAs(user); // Act: Desactivar const deactivateResponse = await request(app.getHttpServer()) .post('/auth/deactivate') .send({ password: 'password123' }) .expect(HttpStatus.OK); expect(deactivateResponse.body.message).toContain('desactivada'); // Assert: Verificar estado let updatedUser = await getUserById(user.id); expect(updatedUser.status).toBe(UserStatus.INACTIVE); // Assert: Verificar auditoría const auditLog = await getLatestAuditLog(user.id, 'user_status'); expect(auditLog.action).toBe('deactivate'); expect(auditLog.details.deactivatedBy).toBe('self'); }); it('should allow user to reactivate their account', async () => { // Arrange const user = await createTestUser({ status: UserStatus.INACTIVE }); await loginAs(user); // Act: Reactivar const reactivateResponse = await request(app.getHttpServer()) .post('/auth/reactivate') .expect(HttpStatus.OK); expect(reactivateResponse.body.message).toContain('reactivada'); // Assert const updatedUser = await getUserById(user.id); expect(updatedUser.status).toBe(UserStatus.ACTIVE); }); it('should enforce rate limiting on reactivation (max 3/day)', async () => { const user = await createTestUser({ status: UserStatus.INACTIVE }); await loginAs(user); // Reactivar 3 veces (máximo permitido) for (let i = 0; i < 3; i++) { await request(app.getHttpServer()) .post('/auth/reactivate') .expect(HttpStatus.OK); await request(app.getHttpServer()) .post('/auth/deactivate') .send({ password: user.password }) .expect(HttpStatus.OK); } // Intentar 4ta reactivación (debe fallar) const response = await request(app.getHttpServer()) .post('/auth/reactivate') .expect(HttpStatus.TOO_MANY_REQUESTS); expect(response.body.errorCode).toBe('TOO_MANY_REACTIVATIONS'); expect(response.body.message).toContain('límite de reactivaciones'); }); }); ``` --- #### Test Case 4: Banned user cannot login ```typescript describe('AuthService - Banned User', () => { it('should block banned user from logging in', async () => { // Arrange const user = await createTestUser({ email: 'banned@test.com', password: 'password123', status: UserStatus.BANNED, bannedReason: 'Fraude financiero', }); // Act const response = await request(app.getHttpServer()) .post('/auth/login') .send({ email: 'banned@test.com', password: 'password123', }) .expect(HttpStatus.UNAUTHORIZED); // Assert expect(response.body.message).toContain('baneada permanentemente'); expect(response.body.errorCode).toBe('ACCOUNT_BANNED'); expect(response.body.contactSupport).toBe(true); }); it('should block banned email from creating new account', async () => { // Arrange: Crear email baneado await createBannedEmail({ email: 'fraud@test.com', reason: 'Usuario anterior cometió fraude', }); // Act: Intentar registro con email baneado const invitationToken = await createInvitation('fraud@test.com'); const response = await request(app.getHttpServer()) .post('/auth/register-by-invitation') .send({ token: invitationToken, password: 'newPassword123', }) .expect(HttpStatus.FORBIDDEN); // Assert expect(response.body.errorCode).toBe('EMAIL_BANNED'); expect(response.body.message).toContain('bloqueado'); }); }); ``` --- #### Test Case 5: Multi-tenancy status validation ```typescript describe('Multi-tenancy Status', () => { it('should allow user active in constructora A but suspended in B', async () => { // Arrange const user = await createTestUser({ email: 'multi@test.com' }); await assignUserToConstructora(user.id, 'constructora-a', { status: UserStatus.ACTIVE, role: ConstructionRole.DIRECTOR, }); await assignUserToConstructora(user.id, 'constructora-b', { status: UserStatus.SUSPENDED, role: ConstructionRole.ENGINEER, }); // Act: Login const loginResponse = await request(app.getHttpServer()) .post('/auth/login') .send({ email: 'multi@test.com', password: 'password' }) .expect(HttpStatus.OK); // Assert: Solo debe ver constructora A (activa) expect(loginResponse.body.constructoras).toHaveLength(1); expect(loginResponse.body.constructoras[0].id).toBe('constructora-a'); // Act: Acceder a constructora A (debe funcionar) const tokenA = loginResponse.body.accessToken; await request(app.getHttpServer()) .get('/dashboard') .set('Authorization', `Bearer ${tokenA}`) .expect(HttpStatus.OK); // Act: Intentar cambiar a constructora B (debe fallar) const switchResponse = await request(app.getHttpServer()) .post('/auth/switch-constructora') .set('Authorization', `Bearer ${tokenA}`) .send({ constructoraId: 'constructora-b' }) .expect(HttpStatus.FORBIDDEN); expect(switchResponse.body.message).toContain('suspended'); }); }); ``` --- #### Test Case 6: Trigger audits all status changes ```typescript describe('Audit Trigger', () => { it('should automatically audit status changes via trigger', async () => { const user = await createTestUser({ status: UserStatus.ACTIVE }); // Cambiar estado directamente en DB (sin pasar por service) await query(` UPDATE auth_management.profiles SET status = 'suspended' WHERE id = $1 `, [user.id]); // Verificar que trigger creó audit log const auditLogs = await query(` SELECT * FROM audit_logging.audit_logs WHERE resource_id = $1 AND resource_type = 'user_status' ORDER BY created_at DESC LIMIT 1 `, [user.id]); expect(auditLogs.rows).toHaveLength(1); expect(auditLogs.rows[0].action).toBe('suspend'); expect(auditLogs.rows[0].details.old_status).toBe('active'); expect(auditLogs.rows[0].details.new_status).toBe('suspended'); }); }); ``` --- ## 📚 Referencias Adicionales ### Documentos Relacionados - 📄 [RF-AUTH-001: Sistema de Roles de Construcción](./RF-AUTH-001-roles-construccion.md) - Estados interactúan con roles - 📄 [RF-AUTH-003: Multi-tenancy por Constructora](./RF-AUTH-003-multi-tenancy.md) *(Pendiente)* - Estados por constructora - 📄 [US-FUND-001: Autenticación Básica JWT](../historias-usuario/US-FUND-001-autenticacion-basica-jwt.md) - Login valida estado - 📄 [US-FUND-005: Sistema de Sesiones](../historias-usuario/US-FUND-005-sistema-sesiones.md) *(Pendiente)* - Cerrar sesiones al suspender ### Regulaciones y Compliance - [GDPR Article 17: Right to Erasure](https://gdpr-info.eu/art-17-gdpr/) - Derecho al olvido - [GDPR Article 20: Right to Data Portability](https://gdpr-info.eu/art-20-gdpr/) - Exportar datos personales - [Ley Federal de Protección de Datos Personales (México)](https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf) - Protección de datos en México ### Recursos Técnicos - [PostgreSQL ENUM Types](https://www.postgresql.org/docs/current/datatype-enum.html) - [NestJS Guards](https://docs.nestjs.com/guards) - [NestJS Middleware](https://docs.nestjs.com/middleware) --- ## 📅 Historial de Cambios | Versión | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-11-17 | Tech Team | Creación inicial adaptada de GAMILIT con contexto de construcción | --- **Documento:** `MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md` **Ruta absoluta:** `[RUTA-LEGACY-ELIMINADA]/docs/01-fase-alcance-inicial/MAI-001-fundamentos/requerimientos/RF-AUTH-002-estados-cuenta.md` **Generado:** 2025-11-17 **Mantenedores:** @tech-lead @backend-team @frontend-team