feat: autorregistro con verificacion de correo + aprobacion, superadmin y SMTP

- POST /api/auth/register: primer usuario del sistema -> superadmin con sesion
  directa (bootstrap, sin correo ni aprobacion); los siguientes quedan
  pending_verification y reciben un correo con enlace de un solo uso (24h).
- GET /api/auth/verify-email: pasa a pending_approval y notifica in-app a los
  admin/superadmin activos.
- admin/superadmin aprueban (POST /api/users/:id/approve, eligen rol y
  departamento) o rechazan (POST /api/users/:id/reject, borra y audita el
  email) desde Organizacion -> Usuarios (nuevo badge de estado + acciones).
- login() da mensaje especifico segun el status de la cuenta en vez del
  generico "credenciales invalidas".
- Recuperacion de clave (forgot/reset-password): respuesta siempre generica,
  token de 1 hora de un solo uso.
- Nuevo modulo settings: SMTP configurable SOLO por superadmin desde
  Configuracion -> Correo (SMTP); clave cifrada AES-256-GCM (ENCRYPTION_KEY)
  en la tabla system_settings, nunca expuesta por la API. sendMail() degrada
  a modo dev (loguea en vez de enviar) si no hay SMTP configurado, y nunca
  deja que un fallo de envio tumbe al que lo llama.
- Migracion 002_registration_and_smtp.sql: rol superadmin, columnas de ciclo
  de vida en users (status, tokens), tabla system_settings, y promueve al
  usuario mas antiguo del sistema a superadmin.
- Fix: React.StrictMode disparaba el efecto de VerifyEmailPage dos veces; la
  segunda llamada (token ya consumido) mostraba "enlace invalido" pese a que
  la verificacion ya habia funcionado. Se agrega una guarda por token.
- Fix (deuda tecnica v1.0.0): se reordena el montaje de rutas en app.js —
  subtasksRoutes (mount generico en /api) va al final, para no interceptar
  con su requireAuth interno una request sin token dirigida a un router mas
  especifico registrado despues.
- Documentacion actualizada: AGENTS.md, SPEC.md, API.md, BASE_DE_DATOS.md,
  CONFIGURACION.md, ESTADO.md, CHANGELOG.md. Version 1.1.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-05 23:28:46 -05:00
parent 556bbc7314
commit 7557dffc52
49 changed files with 1063 additions and 84 deletions
+17 -25
View File
@@ -20,8 +20,9 @@
| Tabla | Contenido |
|---|---|
| `roles` | catálogo fijo de 5 roles (admin, it_manager, project_lead, collaborator, viewer) |
| `users` | usuarios, con rol y departamento |
| `roles` | catálogo fijo de 6 roles (superadmin, admin, it_manager, project_lead, collaborator, viewer) |
| `users` | usuarios, con rol, departamento y ciclo de vida de registro (`status`, tokens) |
| `system_settings` | clave/valor de configuración del sistema (hoy: SMTP, clave cifrada) |
| `departments` | departamentos/áreas, jerarquía simple vía `parent_id` |
| `project_types` | catálogo de tipos de proyecto |
| `priorities` | catálogo de prioridades (baja/media/alta/crítica) |
@@ -58,7 +59,14 @@
editables directo por el cliente: actividad con subtareas = % de subtareas hechas; actividad sin
subtareas = manual (0-100); proyecto = promedio del avance de sus actividades.
- `users.password_hash` con bcrypt, nunca texto plano ni reversible.
- Borrado lógico donde aplique (`users.is_active`); nunca DELETE físico de usuarios con historial.
- `users.status` (`pending_verification`/`pending_approval`/`active`) es el ciclo de vida del
autorregistro, independiente de `is_active` (que sigue siendo el interruptor manual del admin
para desactivar a alguien ya activo). Un usuario `pending_approval` rechazado se **borra**
físicamente (no hay estado "rechazado" — se audita el email en `audit_log` antes de borrar).
- `system_settings.value` para `smtp_pass_enc` va **cifrado** (AES-256-GCM, `ENCRYPTION_KEY` del
`.env`) — nunca en texto plano, nunca se devuelve por la API (solo `configured: true/false`).
- Borrado lógico donde aplique (`users.is_active`); nunca DELETE físico de usuarios **activos** con
historial (los `pending_approval` rechazados sí se borran, ver arriba).
- Empresa asumida única (no hay tabla `companies` en el MVP).
- Producción siempre usa `192.168.222.32`. Desarrollo siempre usa `192.168.222.20`. No mezclar.
@@ -105,36 +113,20 @@ node src/scripts/seed-db.js
---
## Setup de base de datos en producción (primera vez) — pendiente, no ejecutado aún
## Setup de base de datos en producción — ejecutado el 2026-09-06
El servidor `.32` no tiene SSH accesible; el acceso admin es solo desde la máquina de desarrollo.
Credenciales de acceso admin: ver `SERVIDORES.md` (project-md-templates/, no versionado).
BD `project_linkedcorp` creada en `.32` con `crear-db-prod.sh` (grant a `192.168.222.3`). Detalle
del proceso y estado real en `PRODUCCION.md`.
```bash
mysql -h 192.168.222.32 -u mgonzalez -p --ssl=0
```
```sql
CREATE DATABASE IF NOT EXISTS project_linkedcorp
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'project_linkedcorp'@'192.168.222.3'
IDENTIFIED BY '[DB_PASSWORD_PROD]';
GRANT ALL PRIVILEGES ON project_linkedcorp.* TO 'project_linkedcorp'@'192.168.222.3';
FLUSH PRIVILEGES;
```
Migración en el servidor de aplicaciones (`.3`), cuando exista el deploy:
Migración en el servidor de aplicaciones (`.3`):
```bash
ssh deploy@192.168.222.3 -p 2203
cd /opt/apps/project.linkedcorp.net/source/backend
set -a && source /opt/apps/project.linkedcorp.net/backend/.env && set +a
node src/scripts/migrate-db.js
node src/scripts/seed-db.js # solo catálogos/roles; NO recrear usuarios de prueba de dev
node src/scripts/seed-db.js # solo catálogos/roles; sin datos de ejemplo en produccion
node src/scripts/create-admin.js "Nombre" correo@dominio # solo la primera vez, bootstrap manual
```
---