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
@@ -0,0 +1,29 @@
-- Registro con verificacion de correo + aprobacion, rol superadmin, config SMTP. Idempotente.
SET NAMES utf8mb4;
INSERT IGNORE INTO roles (code, name) VALUES ('superadmin', 'Super Administrador');
ALTER TABLE users
ADD COLUMN IF NOT EXISTS status ENUM('pending_verification','pending_approval','active')
NOT NULL DEFAULT 'active',
ADD COLUMN IF NOT EXISTS email_verified_at DATETIME NULL,
ADD COLUMN IF NOT EXISTS verification_token VARCHAR(128) NULL,
ADD COLUMN IF NOT EXISTS verification_token_expires DATETIME NULL,
ADD COLUMN IF NOT EXISTS reset_token VARCHAR(128) NULL,
ADD COLUMN IF NOT EXISTS reset_token_expires DATETIME NULL;
CREATE INDEX IF NOT EXISTS idx_users_verification_token ON users(verification_token);
CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(reset_token);
CREATE TABLE IF NOT EXISTS system_settings (
`key` VARCHAR(60) PRIMARY KEY,
`value` TEXT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- El usuario mas antiguo del sistema (creado antes de este feature, via create-admin.js) pasa a
-- superadmin: es efectivamente el "primer usuario", solo que no vino del flujo de auto-registro.
UPDATE users u JOIN roles r ON r.code = 'superadmin'
SET u.role_id = r.id
WHERE u.id = (SELECT id FROM (SELECT MIN(id) AS id FROM users) x)
AND u.role_id <> r.id;