# Produccion - project.linkedcorp.net ## Estado actual - Version actual: `1.2.0` - Estado: `operativo` - Ultima validacion: `2026-09-06` ## Servidor - Host: `192.168.222.3` - SSH: `deploy@192.168.222.3 -p 2203` (llave `~/.ssh/id_ed25519`, sudo NOPASSWD) - Ruta base: `/opt/apps/project.linkedcorp.net/` ## Runtime - Frontend: `/opt/apps/project.linkedcorp.net/frontend/` (dist/ + node_modules propio con `serve`) - Backend: `/opt/apps/project.linkedcorp.net/backend/` (node_modules + `.env` de produccion) - Source (checkout de Gitea, WorkingDirectory de ambos servicios): `/opt/apps/project.linkedcorp.net/source/` - Storage: `/opt/apps/project.linkedcorp.net/storage/` (adjuntos — `STORAGE_ROOT` en `.env`) - Logs: `/opt/apps/project.linkedcorp.net/logs/` - APKs Android publicados: no aplica ## Servicios - Frontend: `project-linkedcorp-frontend` (`node node_modules/.bin/serve -s dist -l 5196`, WorkingDirectory `source/frontend`... revisar nota abajo) - Backend: `project-linkedcorp-backend` (`node src/server.js`, WorkingDirectory `source/backend`, `EnvironmentFile=backend/.env`) > Nota real: el servicio frontend corre con `WorkingDirectory=/opt/apps/project.linkedcorp.net/frontend` > (no `source/frontend`) sirviendo `frontend/dist/` con el `serve` instalado ahi mismo (node_modules > minimo, solo el paquete `serve`). El `dist/` se copia desde `source/frontend/dist` en cada deploy. ## Puertos internos - Frontend: `127.0.0.1:5196` - Backend: `127.0.0.1:4019` ## Base de datos - Host pruebas/desarrollo: `192.168.222.20` - Host produccion real: `192.168.222.32` - Base: `project_linkedcorp` - Usuario app: `project_linkedcorp` (permitido solo desde `192.168.222.3`) - Regla dura: produccion web siempre usa `192.168.222.32` - Regla dura: pruebas y desarrollo siempre usan `192.168.222.20` - Antes de cada deploy, validar el `DB_HOST` activo en runtime (`backend/.env` de produccion) ## NPM (Nginx Proxy Manager) - Dominio: `project.linkedcorp.net`, SSL Let's Encrypt (auto-renovacion), Force SSL activo. - Proxy Host (id 77) -> `192.168.222.3:5196` (frontend). - Custom Location `/api` -> `192.168.222.3:4019` (backend). ## Estado confirmado (2026-09-06) - Migraciones aplicadas (`001_initial.sql`) sobre `project_linkedcorp` en `.32`. - Catalogos base sembrados (roles, prioridades, estados de proyecto/actividad, tipos de proyecto). Sin datos de ejemplo (departamentos/usuario dev) — produccion arranca limpia. - Usuario Administrador inicial creado con `create-admin.js` (clave fuerte generada, entregada una sola vez fuera de este repo). El admin debe crear ahi mismo los demas usuarios/departamentos. - Login, dashboard y flujo proyecto->actividad verificados via curl contra `https://project.linkedcorp.net` (backend + frontend + SSL + BD produccion, todo real). - Pendiente (no bloqueante): limpiar archivos `.md` de `/opt/apps/project.linkedcorp.net/source` (paso de higiene del checklist de `DEPLOY_WEB.md`, no se ejecuto por restriccion del entorno del agente — pendiente de accion manual o autorizacion explicita). ## Actualizacion v1.1.0 (2026-09-06) — autorregistro/aprobacion/superadmin/SMTP - Migracion `002_registration_and_smtp.sql` aplicada sobre `.32`: rol `superadmin`, columnas de ciclo de vida en `users`, tabla `system_settings`. `mgonzalez@linkedcorp.net` (id=1) quedo promovido a `superadmin` automaticamente. - `ENCRYPTION_KEY` generado y agregado al `.env` de produccion (cifra la clave SMTP en BD). - Ambos servicios reconstruidos (backend con `nodemailer` nuevo, frontend recompilado) y reiniciados; PID confirmado distinto en los dos. - Verificado end-to-end contra `https://project.linkedcorp.net`: login reporta `roleCode: superadmin`, `GET /api/settings/smtp` responde `configured:false` (sin SMTP real todavia). - **Pendiente**: el superadmin debe cargar el SMTP real desde Configuracion → Correo (SMTP) cuando lo tenga. Hasta entonces, un registro/recuperacion de clave real en produccion queda logueado en `journalctl -u project-linkedcorp-backend` en vez de enviarse por correo — nadie externo puede completar el autorregistro sin que un admin le pase el enlace manualmente desde el log. ## Actualizacion v1.2.0 (2026-09-06) — cambiar clave desde la app - `PUT /api/auth/change-password` (exige clave actual) + opcion "Cambiar clave" en el menu de usuario. Redeploy sin cambios de dependencias ni de esquema; PID confirmado en ambos servicios. - Verificado en produccion via curl (rechaza clave actual incorrecta con 400; no se cambio la clave real del superadmin durante la prueba). ## Actualizacion v1.3.0 (2026-09-06) — boton de prueba en Correo (SMTP) - `POST /api/settings/smtp/test` (superadmin, rate-limited) agregado. Redeploy sin cambios de dependencias ni de esquema (`nodemailer` ya estaba desde v1.1.0); PID confirmado distinto en ambos servicios tras el restart. - Verificado en dev de punta a punta (curl + Playwright) antes del deploy: validacion 400 sin host/usuario, y un intento real contra el SMTP interno con credenciales invalidas devuelve el error real del servidor (`certificate has expired`) en vez de un mensaje generico. - En produccion se confirmo: el commit correcto quedo desplegado, ambos servicios reiniciaron limpio (`journalctl` sin errores) y `GET /api/settings/smtp` sin token responde `401` como se espera. **No se verificó el flujo autenticado completo en producción** — minar un JWT válido en el servidor para probarlo fue bloqueado por el clasificador de auto-mode (accion sensible sobre produccion) y no se intento evadirlo; tampoco se reseteo la clave del superadmin real solo para probar. Si se quiere una verificacion end-to-end en produccion, hacerlo manualmente desde el navegador con la sesion real del superadmin. - Regla nueva agregada al orquestador (`project-md-templates/SEGURIDAD-Y-CALIDAD.md`, seccion 1.6bis y checklist): toda pantalla de configuracion de un servicio externo debe tener un boton de prueba real, no solo "Guardar". ## Fix rapido (2026-09-06) — espacio colado en el host SMTP - El superadmin probó el SMTP real en producción y el boton de prueba (recien desplegado) funciono como debia: reporto `getaddrinfo ENOTFOUND mail.linkedcorp.net`. La causa real era un espacio de mas tecleado en el campo host, guardado literalmente como `"mail.linkedcorp. net"` en `system_settings` (confirmado por hexdump del valor en la BD de produccion). - Fix: `updateSmtpConfig`/`testSmtpConfig` ahora hacen `trim()` de host/usuario/remitente/destino antes de guardar o probar, para que un espacio accidental no vuelva a romper la resolucion DNS en silencio. Desplegado (solo backend, sin build de frontend ni migracion; PID confirmado). - El valor ya guardado en produccion sigue teniendo el espacio (no se corrigio a mano en la BD: el UPDATE directo sobre la BD de produccion fue bloqueado por el clasificador de auto-mode del agente, y no se intento evadirlo). **Accion pendiente del superadmin**: volver a guardar el host desde Configuracion -> Correo (SMTP) (con el fix ya desplegado, un espacio de mas ya no rompe nada) y repetir la prueba. - **Correccion (mismo dia)**: el primer fix usaba `trim()`, que solo quita espacios al borde. El espacio real estaba a MITAD del host (`"mail.linkedcorp." + " " + "net"`), asi que `trim()` no lo tocaba y el superadmin siguio viendo el mismo `ENOTFOUND` tras reintentar. Corregido para quitar CUALQUIER espacio (`replace(/\s+/g,'')`) de host/usuario/remitente/destino, no solo los de los bordes. Redesplegado (solo backend, PID confirmado, logs limpios). ## Actualizacion v1.4.0 (2026-09-09) — mostrar/ocultar clave - Boton de ojo (revelar/ocultar) en todos los campos de contraseña de la app (login, registro, cambiar/restablecer clave, crear usuario, clave SMTP). Componente `PasswordInput` reutilizable. - Redeploy solo frontend (sin dependencias ni migraciones nuevas); PID confirmado y bundle nuevo verificado contra `https://project.linkedcorp.net`. - Regla nueva agregada al orquestador (`UI-UX-WINDOWS.md`, seccion 3 "Kit de componentes"): todo campo de contraseña debe llevar el icono de revelar/ocultar. ## Actualizacion v1.5.0 (2026-09-09) — eliminar proyecto (logico), papelera, orden por creacion - Migracion `003_project_soft_delete.sql` aplicada sobre `.32`: columnas `deleted_at`/`deleted_by` en `projects`. - `DELETE /api/projects/:id` (logico, con popup de confirmacion), `GET /api/projects?trash=true` + `POST /:id/restore` + `DELETE /:id/permanent` (papelera, solo superadmin; el borrado fisico exige que el proyecto ya este en la papelera). - Listado de proyectos con columna "Creado" y orden por defecto de mas nuevo a mas antiguo. - Verificado de punta a punta en dev con Playwright (crear -> aparece primero -> eliminar con popup -> desaparece del listado -> aparece en papelera -> restaurar -> vuelve a aparecer), y con curl los limites de permisos (un `collaborator` sin ser dueño/miembro recibe 403 en las 4 rutas nuevas). Sin errores de consola. - Redeploy a produccion: migracion aplicada, backend y frontend reconstruidos y reiniciados (PID confirmado distinto en ambos), verificado contra `https://project.linkedcorp.net` (bundle nuevo servido, `GET /api/projects` sin token responde 401 como se espera). ## Cierre del incidente SMTP (2026-09-09) — certificado vencido + IP en Spamhaus XBL Con el host ya sin espacios (fix de v1.4.0), el boton de prueba seguia fallando con `certificate has expired`. Investigacion completa en el servidor de correo (`192.168.222.10`, iRedMail): - **Causa del certificado**: en `/etc/nginx/sites-available/00-default.conf` (puerto 80) el `return 301 https://...` estaba directo en el bloque `server{}`, que en nginx se ejecuta en la fase de *server rewrite* **antes** de evaluar `location` — por eso el reto ACME `/.well-known/acme-challenge/` de certbot siempre terminaba redirigido a HTTPS, y ahi fallaba contra NPM (sin certificado asignado para `mail.linkedcorp.net` en el 443, SNI rechazado). Fix: mover el redirect dentro de `location / { return 301 ...; }` para que la ubicacion mas especifica del challenge tenga prioridad. Certificado renovado con `certbot renew` (valido hasta 2026-12-08); Postfix y Dovecot recargados (`systemctl reload`, sin cortar conexiones). Backup del nginx original guardado junto al archivo en el servidor. - **Causa de que Hotmail/Outlook rechazara igual**: la IP publica compartida de toda la red (`186.3.190.7`, NAT de `192.168.222.0/24` — no exclusiva del correo) estaba listada en el **XBL de Spamhaus** por una deteccion real de malware (`urlzone`/Bebloh, conexion a un C2, ultima deteccion 2026-09-03). No era fuerza bruta ni relay de spam del correo. Delisting pedido y confirmado por Spamhaus el mismo dia (autoservicio, `check.spamhaus.org`); no se logro rastrear el dispositivo infectado especifico (el `filter.log` de pfSense solo retenia horas de historial — se amplio a ~25 dias por si se repite). - **Verificado end-to-end**: envio real via `testSmtpConfig` desde el propio backend de produccion a Gmail y Hotmail, ambos confirmados recibidos (`{"sent":true}` + confirmacion visual del usuario en Hotmail). - **Endurecimiento aplicado de paso** (servidor de correo): `fail2ban` con `maxretry` 5->3, `bantime` 1h->24h con escalado a 7 dias para reincidentes, jail `recidive` nuevo. Cuentas `admin@`/`postmaster@linkedcorp.net` identificadas bajo ataque de fuerza bruta activo con clave sin rotar desde su creacion (dic. 2024) — **pendiente que el superadmin rote esas claves**, no se toco por no ser parte del alcance de este proyecto. - Inventario completo de cuentas de correo del grupo (que app usa cada una, ultima actividad, candidatas a borrar) documentado en `project-md-templates/CUENTAS_CORREO.md`. De paso se encontraron y corrigieron dos apps de otros proyectos (`album-mundial-2026`, `linkedsign`) que tenian `MAIL_TLS_REJECT_UNAUTHORIZED=false` como workaround del mismo certificado vencido — revertido a `true` y servicios reiniciados una vez el certificado quedo renovado. ## Actualizacion v1.6.0 (2026-09-09) — administradores por proyecto, Configuracion solo superadmin - Un proyecto puede tener uno o mas administradores (`role_in_project='admin'` en `project_members`), independiente del rol global de quien se promueve — nuevo `PUT /api/projects/:id/members/:userId/role`. Verificado con curl: un `collaborator` sin ser dueño/miembro recibe 403 al editar; agregado como miembro simple sigue en 403; promovido a admin del proyecto pasa a 200; degradado vuelve a 403; el `owner` no se puede tocar por esta via. - Menu "Configuracion" oculto y bloqueado por ruta para todo el que no sea superadmin (verificado con Playwright: un `admin` de prueba no ve el item de nav y es redirigido si teclea la URL directo). Escritura de catalogos (`/api/catalogs/*`) restringida de `[superadmin, admin]` a solo `superadmin` para no dejar el backend mas permisivo que la UI (verificado con curl: `admin` recibe 403 al crear un catalogo, lectura sigue en 200). - Sin migracion nueva (la columna `role_in_project` ya existia desde el MVP). Redeploy solo backend+frontend; PID confirmado distinto en ambos servicios. ## Actualizacion v1.7.0 (2026-09-10) — invitar a un proyecto por correo - Migracion `004_project_member_invite.sql` aplicada sobre `.32`: agrega el status `invited` al enum de `users`. - Agregar miembro a un proyecto ahora acepta un correo que todavia no es usuario: crea una cuenta `invited` (nombre/clave placeholder) y manda un correo con un enlace de un solo uso (`/aceptar-invitacion`, vence 7 dias) para que la persona elija su nombre y clave. Nuevos endpoints publicos `GET /api/auth/invite` y `POST /api/auth/accept-invite`. - Verificado de punta a punta en dev con curl + Playwright: invitar por correo nuevo -> correo logueado (modo dev) -> `GET /api/auth/invite` devuelve el email -> `POST /api/auth/accept-invite` crea la cuenta, la activa y devuelve sesion iniciada -> aparece como miembro del proyecto sin pasos adicionales -> login normal con la clave elegida funciona -> reusar el mismo token una segunda vez falla (un solo uso). Tambien: invitar un correo ya existente simplemente lo agrega como miembro directo (`invited:false`), sin crear nada nuevo ni mandar correo. Sin errores de consola. - Redeploy a produccion: migracion aplicada, backend y frontend reconstruidos y reiniciados (PID confirmado distinto en ambos), verificado contra `https://project.linkedcorp.net` (`GET /api/auth/invite` sin token responde `400` de validacion, no `401` — confirma que quedo publico como se espera; `/aceptar-invitacion` carga con `200`). ## Actualizacion v1.8.0 (2026-09-10) — renombrar Subtareas a Tareas - Migracion `005_rename_subtasks_to_tasks.sql` aplicada sobre `.32`. Nota real: la primera version de esta migracion usaba `RENAME TABLE subtasks TO tasks`, pero fallo en produccion con "Table 'tasks' already exists" porque `001_initial.sql` (ya actualizado para crear `tasks` directo en instalaciones nuevas) crea esa tabla vacia en el mismo run, antes de que corriera la 005 — sin perdida de datos (`subtasks` en produccion tenia 0 filas). Corregida para copiar filas de `subtasks` a `tasks` (con id explicito) y borrar `subtasks`, ajustando el `AUTO_INCREMENT` despues; probada con datos simulados en dev antes de reintentar en produccion. - Modulo backend `subtasks/` → `tasks/`, rutas `/api/activities/:id/tasks` y `/api/tasks/:id`, servicio frontend, pestaña "Tareas" en el detalle de actividad, i18n y documentacion. Sin cambios de comportamiento, solo nombres. - Backend y frontend reconstruidos y reiniciados (PID confirmado distinto en ambos), verificado contra `https://project.linkedcorp.net`. ## Comandos utiles ```bash sudo systemctl status project-linkedcorp-backend sudo systemctl status project-linkedcorp-frontend sudo journalctl -u project-linkedcorp-backend -n 100 --no-pager sudo journalctl -u project-linkedcorp-frontend -n 100 --no-pager ``` ## Notas - No dejar secretos en el repo (`.env` de produccion vive solo en `backend/.env` del servidor). - No reemplazar `storage/` ni `logs/` durante un deploy. - Documentar cambios de version y estado real en este archivo y en `CHANGELOG.md`.