5b899256b1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
17 KiB
17 KiB
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 conserve) - Backend:
/opt/apps/project.linkedcorp.net/backend/(node_modules +.envde produccion) - Source (checkout de Gitea, WorkingDirectory de ambos servicios):
/opt/apps/project.linkedcorp.net/source/ - Storage:
/opt/apps/project.linkedcorp.net/storage/(adjuntos —STORAGE_ROOTen.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, WorkingDirectorysource/frontend... revisar nota abajo) - Backend:
project-linkedcorp-backend(node src/server.js, WorkingDirectorysource/backend,EnvironmentFile=backend/.env)
Nota real: el servicio frontend corre con
WorkingDirectory=/opt/apps/project.linkedcorp.net/frontend(nosource/frontend) sirviendofrontend/dist/con elserveinstalado ahi mismo (node_modules minimo, solo el paqueteserve). Eldist/se copia desdesource/frontend/disten 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 desde192.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_HOSTactivo en runtime (backend/.envde 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) sobreproject_linkedcorpen.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
.mdde/opt/apps/project.linkedcorp.net/source(paso de higiene del checklist deDEPLOY_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.sqlaplicada sobre.32: rolsuperadmin, columnas de ciclo de vida enusers, tablasystem_settings.mgonzalez@linkedcorp.net(id=1) quedo promovido asuperadminautomaticamente. ENCRYPTION_KEYgenerado y agregado al.envde produccion (cifra la clave SMTP en BD).- Ambos servicios reconstruidos (backend con
nodemailernuevo, frontend recompilado) y reiniciados; PID confirmado distinto en los dos. - Verificado end-to-end contra
https://project.linkedcorp.net: login reportaroleCode: superadmin,GET /api/settings/smtprespondeconfigured: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-backenden 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 (nodemailerya 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 (
journalctlsin errores) yGET /api/settings/smtpsin token responde401como 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"ensystem_settings(confirmado por hexdump del valor en la BD de produccion). - Fix:
updateSmtpConfig/testSmtpConfigahora hacentrim()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 quetrim()no lo tocaba y el superadmin siguio viendo el mismoENOTFOUNDtras 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
PasswordInputreutilizable. - 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.sqlaplicada sobre.32: columnasdeleted_at/deleted_byenprojects. 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
collaboratorsin 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/projectssin 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) elreturn 301 https://...estaba directo en el bloqueserver{}, que en nginx se ejecuta en la fase de server rewrite antes de evaluarlocation— por eso el reto ACME/.well-known/acme-challenge/de certbot siempre terminaba redirigido a HTTPS, y ahi fallaba contra NPM (sin certificado asignado paramail.linkedcorp.neten el 443, SNI rechazado). Fix: mover el redirect dentro delocation / { return 301 ...; }para que la ubicacion mas especifica del challenge tenga prioridad. Certificado renovado concertbot 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 de192.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 (elfilter.logde pfSense solo retenia horas de historial — se amplio a ~25 dias por si se repite). - Verificado end-to-end: envio real via
testSmtpConfigdesde 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):
fail2banconmaxretry5->3,bantime1h->24h con escalado a 7 dias para reincidentes, jailrecidivenuevo. Cuentasadmin@/postmaster@linkedcorp.netidentificadas 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 tenianMAIL_TLS_REJECT_UNAUTHORIZED=falsecomo workaround del mismo certificado vencido — revertido atruey 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'enproject_members), independiente del rol global de quien se promueve — nuevoPUT /api/projects/:id/members/:userId/role. Verificado con curl: uncollaboratorsin 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; elownerno se puede tocar por esta via. - Menu "Configuracion" oculto y bloqueado por ruta para todo el que no sea superadmin (verificado
con Playwright: un
adminde 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 solosuperadminpara no dejar el backend mas permisivo que la UI (verificado con curl:adminrecibe 403 al crear un catalogo, lectura sigue en 200). - Sin migracion nueva (la columna
role_in_projectya 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.sqlaplicada sobre.32: agrega el statusinvitedal enum deusers. - 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 publicosGET /api/auth/inviteyPOST /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/invitedevuelve el email ->POST /api/auth/accept-invitecrea 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/invitesin token responde400de validacion, no401— confirma que quedo publico como se espera;/aceptar-invitacioncarga con200).
Actualizacion v1.8.0 (2026-09-10) — renombrar Subtareas a Tareas
- Migracion
005_rename_subtasks_to_tasks.sqlaplicada sobre.32. Nota real: la primera version de esta migracion usabaRENAME TABLE subtasks TO tasks, pero fallo en produccion con "Table 'tasks' already exists" porque001_initial.sql(ya actualizado para creartasksdirecto en instalaciones nuevas) crea esa tabla vacia en el mismo run, antes de que corriera la 005 — sin perdida de datos (subtasksen produccion tenia 0 filas). Corregida para copiar filas desubtasksatasks(con id explicito) y borrarsubtasks, ajustando elAUTO_INCREMENTdespues; probada con datos simulados en dev antes de reintentar en produccion. - Modulo backend
subtasks/→tasks/, rutas/api/activities/:id/tasksy/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
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 (
.envde produccion vive solo enbackend/.envdel servidor). - No reemplazar
storage/nilogs/durante un deploy. - Documentar cambios de version y estado real en este archivo y en
CHANGELOG.md.