--- asset_id: MIN-EL-Jesus-20260701-v01 version: v01 tipo: MIN — Minuta del día owner: Jesús sherpa: Jess ratificador: Jesús fecha: 2026-07-01 intellibank: IB-EL-EmpowerLabs subbank: EQ-EL-Equipo proyecto: api-nodejs-mongo · recuperación de contraseña + login (puerto PHP→Node) --- # Minuta del día — Jesús · 2026-07-01 ## Qué se hizo - **Recuperación de contraseña — servicio + endpoint (`api-nodejs-mongo`):** - Portado `recoverPass()` de PHP (`controllers/general/tools.php` + rch) a `services/user/PasswordRecoveryService.js` - Nuevo endpoint `POST /api/user/recover-password` (`routes/user/index.js`) — body `{ email }` - Genera password temporal `#~_{]`, actualiza `colaborador.password` (pool1/MySQL) y dispara el correo contra el mismo endpoint PHP existente (`genniux.net/api_rch/rch/colab/send/email`, template 4497486) - Agregada `replaceSpecialsCharacters3` a `utils/strings.js` (faltaba portar; ya existían las variantes 1 y 2) — validada carácter por carácter contra el PHP original vía `php -r` - **Refactor — catálogo de dominios centralizado:** - Nuevo `utils/domains.js`: `APP_DOMAINS` (origin → nombre de app, usado por `getAppActive`) y `PLAYBOOK_DOMAINS`/`PLAYBOOK_DOMAINS_LOCAL` (host → id de playbook, usado por `getDomain`) - Eliminada la duplicación de esos literales entre `services/user/PasswordRecoveryService.js` y `models/playbook/helpers.js` — ambos ahora importan del catálogo único - Reexportado desde `utils/tools.js` siguiendo el mismo patrón que `strings`/`tokens`/`validation` - **Login — servicio + endpoint (`api-nodejs-mongo`):** - Portado `loginUser()` de `data/resources/php/admin.php`, junto con los efectos de `updateUser()` que usa (`ultimo_acceso`, consumo de `accessToken`) — nuevo `services/user/AuthService.js` - Nuevo endpoint `POST /api/user/login` — body `{ by?, valBy?, valToken?, wikiname?, email?, password }` - Login por email o wikiname, con reintento automático por wikiname si falla por email (igual que el PHP) - Soporta login alterno vía `accessToken` usado como password (se consume tras el uso) - **Fix de seguridad silencioso:** el PHP original arma el SQL por interpolación directa de `$wikiname`/`$email` pese a usar `PDO::prepare()` (nunca hace bind) — vulnerable a inyección SQL. Portado con placeholders parametrizados; mismo comportamiento funcional, sin la vulnerabilidad - Extraído `hashLegacyPassword` (`sha1(md5(salt+pass))`) a `utils/passwordHash.js` — compartido entre login y recover-password (antes duplicado). Validado byte a byte contra PHP real - **Recuperación de contraseña — token de validación:** - Agregada propiedad `valToken` a `recoverPassword(email, dominioMP, valToken = true)` y al body de `POST /api/user/recover-password` — default `true` - Si `valToken` es `true`: genera un token temporal reusando el servicio de `utils/tokens.js` (mismo que usa `POST /api/general/temp-data`) y la url del correo queda `${dominioMP}/validate/:token` en vez de `${dominioMP}/login` - Si `valToken` es `false`: se mantiene el comportamiento anterior (`/login` directo) - Iteración sobre el token: `addTempToken(data, expires = 5)` ahora acepta minutos de expiración como segundo parámetro (antes fijo en 5 min); el flujo de recuperación lo llama con `10` minutos y guarda `{ user: { email, wikiname }, password: userPass }` como payload del token - **Recuperación de contraseña — envío directo por Mailjet + password "fácil":** - Agregada propiedad `valApi` a `recoverPassword(...)` — si `true` sigue usando la API PHP existente para enviar el correo; si `false` (default) envía directo por Mailjet desde el proyecto - Nuevo `services/email/MailjetService.js` — puerto de `sendEmail()` de `controllers/empresa/colaborador.php` (tabla de templates por `type_msg`, mismo `From`/Bcc condicional); nuevo `config/mailjet.js` (cliente lazy, mismo patrón que `config/stripe.js`) - Instalado `node-mailjet@^6`; agregadas `MJ_APIKEY_PUBLIC`/`MJ_APIKEY_PRIVATE` a `.env`/`.env-example` — reusé las credenciales reales ya en uso para este mismo flujo en el PHP legacy (misma cuenta `apps@genniux.com`), confirmado con el usuario antes de escribirlas a `.env` - Agregada propiedad `valPass` — si `true` password con símbolos (como antes); si `false` (default) password "fácil" sin símbolos: prefijo corto de la app (`utils/domains.js` → `APP_ABBR`, ej. `MPB`/`REB`/`TRH`/`MSH`) + 3-4 alfanuméricos aleatorios (`crypto.randomInt`) - Agregados a `APP_DOMAINS`/`APP_ABBR`: `tribusrrhh.com` y `rebelocityclub.com` (+ subdominios `libreria.`/`rally.`) y los hosts locales que le faltaban vs. `PLAYBOOK_DOMAINS_LOCAL` (`localhost:4167/3167`→TribusRRHH, `localhost:3169/4269/4169`→Rebelocity, `localhost:8080`/`localhost`→MasterPlaybooks) - Mejora de seguridad en `userPass` (rama `valPass=true`): los símbolos especiales ya no son fijos (`#~_...{]`) — ahora `randomSpecialChars()` los toma al azar de un pool con `crypto.randomInt` (RNG criptográfico, reemplaza `Math.random()`) - **Actualización de contraseña — nuevo endpoint (`api-nodejs-mongo`):** - Nuevo método `AuthService.updatePassword({ wikiname, oldpassword, newpassword })` — puerto de `updPassword()` de `admin.php`, con placeholders parametrizados - Nuevo endpoint `POST /api/user/password/upd` — distinto de recover-password: aquí el usuario ya conoce su password actual - Renombrado `recover-password` → `password/recover` (consistencia con `password/upd`) - **Documentación para Alex/Gustavo (integración nativa en Intellibanks):** - Creado `data/resources/user/auth-endpoints.md` — tablas (`colaborador`/`puesto`/`perfil`), los 5 endpoints de `/api/user`, y las reglas de "registro con correo" (pendiente, para que Gustavo lo implemente sin tener que reversear el PHP) - A pedido del usuario: sin referencias a PHP en el doc (para no confundir a Alex/Gustavo, que solo consumen el sistema Node), con frontmatter WORX (`asset_id: DC-EL-AuthEndpoints-v01`) pero sin las secciones de "Estructura" ni "Notas para Intellibanks" (se quitaron por ser ruido/confusas para el propósito del doc) - Agregada URL base (prod `https://genniux.net/api-nodejs/` / local `http://localhost:3000/`) al doc y al `README.md` del proyecto; actualizada la sección "Usuario" del README (antes stub genérico) con los 5 endpoints reales - **`AppDomainService` — image por app + separación de la lógica de dominios:** - `getAppActive()` ahora resuelve también `image` (logo de la app), consultando la tabla `dominios` (columnas `logo`/`icono`, buscada por `nombre = dominioMP`) — dato real que ya vive en la DB, no había que inventar URLs - A pedido del usuario: se separó `getAppActive()` de `PasswordRecoveryService.js` a un servicio propio, `services/domains/AppDomainService.js` — mantiene `PasswordRecoveryService` enfocado solo en la lógica de password/email, y deja la resolución de app/imagen reusable para otros flujos futuros - `image` cae a un default (`DEFAULT_IMAGE`, asset fijo en Mailjet) cuando no hay fila en `dominios` o `logo`/`icono` vienen vacíos — así el correo de recuperación nunca sale sin imagen - Ajustado `RESET_EMAIL_TEMPLATE` a `8164910` (antes `4497486`) — template de Mailjet actualizado para el flujo de recuperación ## Decisiones - Los catálogos de dominios/apps conocidos (`APP_DOMAINS`, `PLAYBOOK_DOMAINS`) viven en un solo lugar (`utils/domains.js`) — evita que futuros cambios de subdominio requieran tocar múltiples archivos - El hash de password legacy (`sha1(md5(salt+pass))`) se centraliza en `utils/passwordHash.js` — cualquier flujo nuevo que toque `colaborador.password` debe reusar esta función, no reimplementar el hash - Los bugs/dead-code del PHP original que no aportan (ej. branch con variable indefinida `$eventQuery` en `recoverPass`) no se replican — en el modelo de promesas de `mysql2` esos branches son inalcanzables de cualquier forma - No se portó el resto de `updateUser()` (casos `type`, `puestoEmpresa`, `pass`, `token`) — fuera de alcance hasta que se pida `addUser`/`updPassword` - `expires` en `addTempToken` queda en minutos (no ms) para que quien lo llame no tenga que hacer la conversión — el recover-password usa 10 min en vez del default de 5 - El correo de recuperación ahora sale por Mailjet directo por default (`valApi=false`) — la API PHP queda como alternativa vía flag, no como default - Password "fácil" (`valPass=false`) queda como default para recover-password — la retroalimentación fue que los símbolos especiales fijos del esquema original eran excesivos para una password que de todos modos se espera que el usuario cambie después - El doc para Alex/Gustavo vive en el repo de código (`data/resources/user/`), no en el vault de Intellibanks — sigue el formato de los demás docs técnicos del repo (`data/resources/stripe/`, `data/resources/points/`), con frontmatter WORX agregado solo a este archivo, no a los demás docs existentes del repo - La resolución de app/imagen por dominio queda como servicio aparte (`AppDomainService`) en vez de vivir dentro de `PasswordRecoveryService` — cualquier otro flujo que necesite "qué app/logo corresponde a este origin" lo reusa sin duplicar ## NEXTs - [x] Probar `POST /api/user/password/recover` end-to-end (Mailjet y API PHP) — probado, OK - [x] Probar `POST /api/user/login` end-to-end (login por email, por wikiname, y vía accessToken) — probado, OK - [x] Probar `POST /api/user/password/upd` end-to-end — probado, OK - [x] `/validate/:token` — no es endpoint de backend, es page del frontend que consume el token generado por `recover-password`; ya probada, OK - [ ] Registro con correo (`addUserValid` de `admin.php`) — Gustavo lo está trabajando; reglas documentadas en `data/resources/user/auth-endpoints.md` §6