erp-construccion/docs/02-definicion-modulos/MAI-001-fundamentos/historias-usuario/US-FUND-001-autenticacion-basica-jwt.md

568 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# US-FUND-001: Autenticación básica con JWT para Construcción
**Épica:** MAI-001 - Fundamentos
**Sprint:** Sprint 1 (Semanas 2-3)
**Story Points:** 8 SP
**Presupuesto:** $2,900 MXN
**Prioridad:** P0 - Crítica (Alcance Inicial)
**Estado:** 🚧 Planificado
**Reutilización GAMILIT:** 95% (adaptación mínima)
---
## 📝 Descripción
Como **cualquier usuario del sistema (director, ingeniero, residente, etc.)**, quiero poder **registrarme, iniciar sesión con selector de constructora, y recuperar mi contraseña** para **acceder de forma segura a la plataforma y mis obras asignadas**.
**Contexto del Alcance Inicial:**
En el MVP se implementó un sistema de autenticación basado en GAMILIT con JWT que soporta 7 roles fijos específicos de construcción. El sistema incluye:
- Multi-tenancy por constructora
- Selector de constructora al login
- Invitación de usuarios por constructora
- Rol por defecto: `resident`
**Diferencias vs GAMILIT:**
- GAMILIT: Auto-registro abierto → Inmobiliario: Registro por invitación
- GAMILIT: 1 organización → Inmobiliario: Múltiples constructoras
- GAMILIT: 3 roles → Inmobiliario: 7 roles
---
## ✅ Criterios de Aceptación
- [ ] **CA-01:** El sistema permite registrar nuevos usuarios por invitación (email único)
- [ ] **CA-02:** Al registrarse por invitación, el usuario recibe rol especificado en la invitación (default: `resident`)
- [ ] **CA-03:** Las contraseñas se almacenan hasheadas con bcrypt (min. 10 rounds)
- [ ] **CA-04:** El login incluye selector de constructora (si usuario pertenece a múltiples)
- [ ] **CA-05:** El login genera un JWT token válido por 24 horas
- [ ] **CA-06:** El JWT incluye: userId, role, constructoraId (activa), email
- [ ] **CA-07:** Existe endpoint de recuperación de contraseña que envía email con token temporal
- [ ] **CA-08:** El token de recuperación expira en 1 hora
- [ ] **CA-09:** El sistema permite cerrar sesión (invalidación de token en frontend)
- [ ] **CA-10:** Las contraseñas deben tener mínimo 8 caracteres (al menos 1 número)
- [ ] **CA-11:** Se retorna mensaje de error apropiado para credenciales inválidas
- [ ] **CA-12:** Usuario puede cambiar de constructora activa sin volver a loggearse
---
## 🎯 Especificaciones Técnicas
### Backend (Node.js + Express + TypeScript)
**Endpoints:**
```
POST /api/auth/register-by-invitation
- Body: { invitationToken, password, firstName, lastName }
- Response: { user, accessToken, constructora }
- Note: Valida token de invitación antes de crear usuario
POST /api/auth/login
- Body: { email, password, constructoraId? }
- Response: { user, accessToken, constructoras[] }
- Note: Si usuario tiene múltiples constructoras, retorna lista para selector
POST /api/auth/switch-constructora
- Body: { constructoraId }
- Headers: Authorization: Bearer {token}
- Response: { accessToken } (nuevo token con constructora actualizada)
POST /api/auth/forgot-password
- Body: { email }
- Response: { message: "Email sent" }
POST /api/auth/reset-password
- Body: { token, newPassword }
- Response: { message: "Password updated" }
GET /api/auth/me
- Headers: Authorization: Bearer {token}
- Response: { user, constructora, role }
```
**Servicios:**
- **AuthService:** Lógica de autenticación (register, login, validateUser)
- **JwtService:** Generación y validación de tokens JWT
- **MailService:** Envío de emails de recuperación e invitación
- **ConstructoraService:** Gestión de relación usuario-constructora
**Entidades:**
```typescript
// apps/backend/src/modules/auth/entities/user.entity.ts
@Entity('users')
class User {
id: string (UUID)
email: string (unique)
password: string (hashed)
first_name: string
last_name: string
is_active: boolean
email_verified: boolean
createdAt: Date
updatedAt: Date
}
// apps/backend/src/modules/auth/entities/user-constructora.entity.ts
@Entity('user_constructoras')
class UserConstructora {
id: string (UUID)
user_id: string (FK to users)
constructora_id: string (FK to constructoras)
role: ConstructionRole ('director' | 'engineer' | 'resident' | 'purchases' | 'finance' | 'hr' | 'post_sales')
is_primary: boolean (constructora por defecto)
active: boolean
created_at: Date
}
// apps/backend/src/modules/auth/entities/invitation.entity.ts
@Entity('invitations')
class Invitation {
id: string (UUID)
constructora_id: string (FK)
email: string
role: ConstructionRole
token: string (unique)
expires_at: Date
used: boolean
invited_by: string (FK to users)
created_at: Date
}
// apps/backend/src/modules/auth/entities/password-reset-token.entity.ts
@Entity('password_reset_tokens')
class PasswordResetToken {
id: string (UUID)
userId: string (FK to users)
token: string
expiresAt: Date
used: boolean
}
```
**Guards:**
- **JwtAuthGuard:** Protege rutas que requieren autenticación
- **RolesGuard:** Valida roles específicos (7 roles de construcción)
- **ConstructoraGuard:** Valida que usuario tenga acceso a la constructora
**JWT Payload:**
```typescript
interface JwtPayload {
sub: string; // userId
email: string;
role: ConstructionRole;
constructoraId: string;
iat: number;
exp: number;
}
```
---
### Frontend (React + Vite + TypeScript)
**Componentes:**
- `LoginForm.tsx`: Formulario de inicio de sesión con selector de constructora
- `ConstructoraSelector.tsx`: Selector de constructora (si usuario tiene múltiples)
- `RegisterByInvitationForm.tsx`: Formulario de registro por invitación
- `ForgotPasswordForm.tsx`: Solicitud de recuperación
- `ResetPasswordForm.tsx`: Establecer nueva contraseña
- `SwitchConstructoraModal.tsx`: Modal para cambiar constructora activa
**Estado (Zustand):**
```typescript
// apps/frontend/src/stores/authStore.ts
interface AuthStore {
user: User | null;
token: string | null;
constructora: Constructora | null;
constructoras: Constructora[];
isAuthenticated: boolean;
// Actions
login: (email, password, constructoraId?) => Promise<void>;
registerByInvitation: (token, data) => Promise<void>;
logout: () => void;
switchConstructora: (constructoraId) => Promise<void>;
forgotPassword: (email) => Promise<void>;
resetPassword: (token, newPassword) => Promise<void>;
fetchMe: () => Promise<void>;
}
```
**Rutas:**
```typescript
// apps/frontend/src/routes/auth.routes.tsx
const authRoutes = [
{ path: '/login', element: <LoginPage /> },
{ path: '/register/:invitationToken', element: <RegisterPage /> },
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
{ path: '/reset-password/:token', element: <ResetPasswordPage /> },
];
```
**Almacenamiento:**
- Token JWT guardado en `localStorage` (key: 'auth_token')
- Constructora activa en `localStorage` (key: 'active_constructora')
- Auto-login si existe token válido al cargar la app
---
### Seguridad
**Passwords:**
- Hasheadas con bcrypt (10 rounds)
- Validación: min 8 caracteres, 1 número, 1 mayúscula (recomendado)
- No se almacena ni loguea password en texto plano
**JWT:**
- Firmado con secret key (desde .env: JWT_SECRET)
- Expiración: 24 horas
- Header: `Authorization: Bearer {token}`
**Tokens de recuperación:**
- Generados con crypto.randomBytes(32)
- Un solo uso (flag `used`)
- Expiración: 1 hora
- Invalidados después de uso
**Invitaciones:**
- Token único por invitación
- Expiración configurable (default: 7 días)
- Solo 1 uso
- Vinculado a email específico
**Rate Limiting:**
- Login: 5 intentos por minuto por IP
- Forgot password: 3 intentos por hora por IP
- Register: 10 por hora por IP
---
## 📋 Dependencias
**Antes:**
- Ninguna (primera historia del proyecto)
- Infraestructura base migrada de GAMILIT (Sprint 0)
**Después:**
- US-FUND-002 (Perfiles de usuario - requiere autenticación)
- US-FUND-003 (Dashboard por rol - requiere autenticación)
- US-FUND-005 (Sistema de sesiones - extiende esta funcionalidad)
---
## 📐 Definición de Hecho (DoD)
- [ ] Código implementado y revisado (code review aprobado)
- [ ] Tests unitarios para AuthService (>80% coverage)
- [ ] Tests E2E para flujos de autenticación
- [ ] Validación de seguridad (password hashing, JWT signing)
- [ ] Documentación de API en Swagger/OpenAPI
- [ ] Probado en ambiente de desarrollo
- [ ] Sin warnings de seguridad en npm audit
- [ ] Logs de auditoría configurados
---
## 🎨 Mockups/Wireframes
### Flujo de Login
```
┌────────────────────────────────────────────┐
│ Login - Sistema de Obra │
├────────────────────────────────────────────┤
│ │
│ Email: │
│ [____________________________________] │
│ │
│ Contraseña: │
│ [____________________________________] │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ [🏢] Constructora (opcional) │ │
│ │ > ABC Constructora SA de CV │ │
│ │ XYZ Edificaciones │ │
│ └──────────────────────────────────────┘ │
│ │
│ [ ] Recordarme │
│ │
│ [ Iniciar Sesión ] │
│ │
│ ¿Olvidaste tu contraseña? │
│ ¿Tienes una invitación? Regístrate │
│ │
└────────────────────────────────────────────┘
```
### Flujo de Selector de Constructora (después de login)
```
┌────────────────────────────────────────────┐
│ Selecciona tu Constructora │
├────────────────────────────────────────────┤
│ │
│ Tienes acceso a múltiples constructoras. │
│ Selecciona con cuál deseas trabajar hoy: │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 🏢 ABC Constructora SA de CV │ │
│ │ Rol: Ingeniero │ │
│ │ 5 obras activas │ │
│ │ [Seleccionar] ───┼─┤
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 🏢 XYZ Edificaciones │ │
│ │ Rol: Residente │ │
│ │ 2 obras activas │ │
│ │ [Seleccionar] │ │
│ └──────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────┘
```
---
## 🧪 Tareas de Implementación
### Backend (Estimado: 16h, GAMILIT: 17h)
**Total Backend:** ~15h (~3.75 SP) - Ahorro 2h por reutilización
- [ ] **Tarea B.1:** Migración de auth desde GAMILIT - Real: 2h
- [x] Copiar AuthService de GAMILIT
- [x] Copiar JwtStrategy
- [x] Adaptar DTOs para construcción
- [x] Configurar JwtModule
- [ ] **Tarea B.2:** Multi-tenancy y constructoras - Estimado: 4h
- [ ] Crear ConstructoraService
- [ ] Endpoints de gestión de constructoras
- [ ] Relación user ← user_constructoras → constructoras
- [ ] Selector de constructora en login
- [ ] **Tarea B.3:** Sistema de invitaciones - Estimado: 3h
- [ ] Crear InvitationService
- [ ] Endpoint POST /invitations/create (solo director)
- [ ] Endpoint POST /auth/register-by-invitation
- [ ] Envío de email con link de invitación
- [ ] **Tarea B.4:** Recuperación de contraseña - Estimado: 2h
- [x] Migrar MailService de GAMILIT
- [ ] POST /auth/forgot-password
- [ ] POST /auth/reset-password
- [ ] Templates de email
- [ ] **Tarea B.5:** Endpoints adicionales - Estimado: 2h
- [ ] POST /auth/switch-constructora
- [ ] GET /auth/me (con constructora activa)
- [ ] Logs de auditoría para cambios de constructora
- [ ] **Tarea B.6:** Documentación Swagger - Estimado: 2h
- [ ] Documentar todos los endpoints de auth
- [ ] Ejemplos de request/response
- [ ] Schemas de validación
---
### Frontend (Estimado: 8h, GAMILIT: 9h)
**Total Frontend:** ~7h (~1.75 SP) - Ahorro 2h
- [ ] **Tarea F.1:** Migración de componentes auth - Real: 2h
- [x] Copiar LoginForm de GAMILIT
- [x] Copiar RegisterForm
- [x] Copiar ForgotPasswordForm
- [x] Copiar ResetPasswordForm
- [ ] **Tarea F.2:** Selector de constructora - Estimado: 3h
- [ ] Componente ConstructoraSelector
- [ ] Modal de selección post-login
- [ ] Switcher en navbar (cambiar constructora activa)
- [ ] Persistir selección en localStorage
- [ ] **Tarea F.3:** Registro por invitación - Estimado: 2h
- [ ] Página /register/:invitationToken
- [ ] Validación de token
- [ ] Formulario de registro con datos de invitación
- [ ] **Tarea F.4:** AuthStore con Zustand - Real: 0h (migrado)
- [x] Copiar authStore de GAMILIT
- [ ] Agregar: constructora, constructoras, switchConstructora
- [ ] Hook useAuth
---
### Testing (Estimado: 6h, GAMILIT: 5.5h)
**Total Testing:** ~6h (~1.5 SP) - Similar a GAMILIT
- [ ] **Tarea T.1:** Tests unitarios backend - Estimado: 3h
- [ ] Tests de AuthService (login, register, JWT)
- [ ] Tests de ConstructoraService
- [ ] Tests de InvitationService
- [ ] Tests de guards (JwtAuthGuard, ConstructoraGuard)
- [ ] **Tarea T.2:** Tests E2E - Estimado: 2h
- [ ] Login con constructora única
- [ ] Login con múltiples constructoras
- [ ] Registro por invitación
- [ ] Recuperación de contraseña
- [ ] Cambio de constructora activa
- [ ] **Tarea T.3:** Tests frontend - Estimado: 1h
- [ ] Tests de componentes de formularios
- [ ] Tests de AuthStore
---
### Deployment (Estimado: 2h, GAMILIT: 2h)
**Total Deployment:** ~2h (~0.5 SP) - Similar
- [ ] **Tarea D.1:** Variables de entorno - Estimado: 1h
- [ ] JWT_SECRET configurado
- [ ] SMTP configurado (envío de emails)
- [ ] Frontend: API_URL configurado
- [ ] **Tarea D.2:** Deploy y validación - Estimado: 1h
- [ ] Deploy a staging
- [ ] Smoke tests de autenticación
- [ ] Validación de seguridad (bcrypt, JWT)
---
## 📊 Resumen de Horas
| Categoría | Estimado | Real | Varianza | Ahorro vs GAMILIT |
|-----------|----------|------|----------|-------------------|
| Backend | 15h | TBD | - | -2h (13%) |
| Frontend | 7h | TBD | - | -2h (22%) |
| Testing | 6h | TBD | - | +0.5h (0%) |
| Deployment | 2h | TBD | - | 0h (0%) |
| **TOTAL** | **30h** | **TBD** | **-** | **-3.5h (~12%)** |
**Validación:** 8 SP × 4h/SP = 32 horas estimadas (vs 30h optimizado) ✅
**Ahorro total:** ~3.5 horas gracias a reutilización de GAMILIT
---
## 📅 Cronograma Real
**Sprint:** Sprint 1 (Semanas 2-3)
**Fecha Inicio:** TBD
**Fecha Fin:** TBD
**Estado:** 🚧 Planificado
**Notas:**
- Multi-tenancy es la diferencia principal vs GAMILIT
- Selector de constructora requiere UX cuidadosa
- Invitaciones reemplazan auto-registro abierto
---
## 🧪 Testing
### Tests Unitarios (Backend)
```typescript
describe('AuthService', () => {
it('should hash password on register by invitation', async () => {
const invitation = await createInvitation({ email: 'test@obra.com', role: 'resident' });
const result = await authService.registerByInvitation(invitation.token, {
password: 'SecurePass123',
firstName: 'Juan',
lastName: 'Pérez'
});
expect(result.user.password).not.toBe('SecurePass123');
expect(await bcrypt.compare('SecurePass123', result.user.password)).toBe(true);
});
it('should generate JWT with constructora on login', async () => {
const user = await createUser({ email: 'test@obra.com' });
const constructora = await assignUserToConstructora(user.id, { role: 'engineer' });
const result = await authService.login('test@obra.com', 'password', constructora.id);
const decoded = jwt.verify(result.accessToken, process.env.JWT_SECRET);
expect(decoded.constructoraId).toBe(constructora.id);
expect(decoded.role).toBe('engineer');
});
it('should allow switching constructora', async () => {
const user = await createUser();
await assignUserToConstructora(user.id, { constructoraId: 'A', role: 'engineer' });
await assignUserToConstructora(user.id, { constructoraId: 'B', role: 'resident' });
const newToken = await authService.switchConstructora(user.id, 'B');
const decoded = jwt.verify(newToken, process.env.JWT_SECRET);
expect(decoded.constructoraId).toBe('B');
expect(decoded.role).toBe('resident');
});
});
```
### Tests E2E
```typescript
describe('Auth API E2E', () => {
it('POST /auth/login - success with constructora selector', async () => {
const user = await createUser({ email: 'multi@obra.com' });
await assignUserToConstructora(user.id, { constructoraId: 'A' });
await assignUserToConstructora(user.id, { constructoraId: 'B' });
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'multi@obra.com', password: 'password' });
expect(response.status).toBe(200);
expect(response.body.constructoras).toHaveLength(2);
expect(response.body.accessToken).toBeDefined();
});
it('POST /auth/register-by-invitation - success', async () => {
const invitation = await createInvitation({ email: 'new@obra.com', role: 'resident' });
const response = await request(app)
.post('/api/auth/register-by-invitation')
.send({
invitationToken: invitation.token,
password: 'SecurePass123',
firstName: 'Juan',
lastName: 'Pérez'
});
expect(response.status).toBe(201);
expect(response.body.user.email).toBe('new@obra.com');
expect(response.body.accessToken).toBeDefined();
});
});
```
---
## 🎯 Estimación
**Desglose de Esfuerzo (8 SP = ~3-4 días):**
- Backend: multi-tenancy y invitaciones: 1.5 días
- Frontend: selector de constructora: 1 día
- Testing: 0.75 días
- Ajustes y documentación: 0.75 días
**Riesgos:**
- Selector de constructora puede requerir iteraciones de UX
- Multi-tenancy en RLS requiere validaciones exhaustivas
**Mitigaciones:**
- Mockups de selector antes de implementar
- Tests E2E de multi-tenancy desde día 1
---
**Creado:** 2025-11-17
**Actualizado:** 2025-11-17
**Responsable:** Equipo Backend + Frontend
**Sprint:** Sprint 1 (Semanas 2-3)
**Épica:** MAI-001 - Fundamentos