5b899256b1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
262 lines
17 KiB
Markdown
262 lines
17 KiB
Markdown
# 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`.
|
|
|
|
## Fix v1.8.1 (2026-09-10) — "Agregar dependencia" sin feedback
|
|
|
|
- El usuario reporto que el boton "Agregar dependencia" no hacia nada. Investigado con curl y
|
|
Playwright: el mecanismo funciona bien cuando hay 2+ actividades en el proyecto; el problema
|
|
real era que con una sola actividad (o todas ya agregadas) el combo queda vacio, no se
|
|
selecciona nada, y el boton no daba ninguna señal de por que. Fix: mensaje explicando la
|
|
situacion en vez del selector, boton deshabilitado si no hay seleccion. Solo frontend, PID
|
|
confirmado, bundle nuevo 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`.
|