bdc62403f4
El usuario solicita el cambio desde Mi cuenta (correo nuevo + clave actual). Lo aprueba un admin/superadmin (colaboradores), solo un superadmin (admins) o el propio superadmin mediante enlace a su correo actual. Tras aprobar se envia confirmacion al correo nuevo y aviso al actual; al confirmar se aplica el cambio y se cierran todas las sesiones. Todo el proceso queda en auditoria. Migracion 035. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
247 lines
17 KiB
Markdown
247 lines
17 KiB
Markdown
# Base de Datos - project.linkedcorp.net
|
|
|
|
## Motor
|
|
|
|
- Motor: MariaDB
|
|
- Base principal: `project_linkedcorp`
|
|
- Host desarrollo: `192.168.222.20`
|
|
- Host producción: `192.168.222.32`
|
|
- Puerto: `3306`
|
|
- Charset: `utf8mb4`
|
|
- Collation: `utf8mb4_unicode_ci`
|
|
|
|
Migración `034_meetings.sql` (v4.4.0): pendiente de aplicar tanto en desarrollo como en
|
|
producción. Crea tablas e índices de reuniones y amplía el tipo de entidad de notificaciones.
|
|
`DATETIME` se guarda como hora civil de `America/Guayaquil`; la conexión usa `dateStrings: true`.
|
|
La recurrencia se almacena como JSON de texto y se expande al consultar, sin filas por ocurrencia.
|
|
|
|
## Fuente de verdad
|
|
|
|
- La base ya existe: sí (creada en dev el 2026-09-03, usuario `project_linkedcorp`).
|
|
- Archivo SQL base: `backend/database/migrations/` (SQL idempotente, numerado, `CREATE TABLE IF NOT EXISTS`).
|
|
- Seeds: `backend/database/seeds/` (roles, catálogos, usuario admin de dev).
|
|
|
|
## Tablas principales (MVP)
|
|
|
|
| Tabla | Contenido |
|
|
|---|---|
|
|
| `roles` | catálogo fijo de 6 roles (superadmin, admin, it_manager, project_lead, collaborator, viewer) |
|
|
| `users` | usuarios, con rol, departamento y ciclo de vida de registro (`status`, tokens) |
|
|
| `system_settings` | clave/valor de configuración del sistema (hoy: SMTP, clave cifrada) |
|
|
| `departments` | departamentos/áreas, jerarquía simple vía `parent_id` |
|
|
| `project_types` | catálogo de tipos de proyecto |
|
|
| `priorities` | catálogo de prioridades (baja/media/alta/crítica) |
|
|
| `project_statuses` | catálogo de estados de proyecto |
|
|
| `activity_statuses` | catálogo de estados de actividad |
|
|
| `projects` | proyectos |
|
|
| `project_members` | usuarios asignados a un proyecto (reemplaza "equipos" como módulo aparte) |
|
|
| `activities` | actividades de un proyecto |
|
|
| `activity_dependencies` | dependencias simples entre actividades ("depende de") |
|
|
| `tasks` | checklist liviano dentro de una actividad ("Tareas" en la UI; renombrada desde `subtasks` en v1.8.0) |
|
|
| `recurring_tasks` | recordatorios periódicos (ej. renovación de licencias), menú propio "Tareas recurrentes" |
|
|
| `meetings` | serie de reunión en hora civil America/Guayaquil; UID iCalendar, secuencia, versión y cancelación lógica |
|
|
| `meeting_participants` / `meeting_external_participants` | usuarios con RSVP y correos externos sin respuesta |
|
|
| `meeting_exceptions` | ocurrencias canceladas, sin materializar el resto de la serie |
|
|
| `meeting_reminders` | clave `(meeting_id, occurrence_start)` para deduplicar avisos |
|
|
| `comments` | comentarios sobre proyecto o actividad (`entity_type` + `entity_id`) |
|
|
| `attachments` | archivos adjuntos a proyecto o actividad |
|
|
| `notifications` | notificaciones in-app por usuario |
|
|
| `audit_log` | auditoría central (quién, qué, sobre qué, cuándo) |
|
|
| `error_logs` | errores de backend (excepciones 500) y frontend (JS/promesas sin catch), panel "Errores del sistema" en Configuración |
|
|
| `notes` | apuntes rápidos personales, menú propio "Notas"; editable solo por el dueño |
|
|
| `note_shares` | con quién está compartida (solo lectura) cada nota — `note_id, user_id` |
|
|
| `vault_entries` | repositorio de credenciales privado por usuario (menú "Contraseñas"), contraseña/notas cifradas |
|
|
| `devices` | inventario de equipos (menú "Equipos"), upsert por `deviceid` del agente GLPI; baja lógica con `status` |
|
|
| `device_cpus` / `device_memories` / `device_disks` / `device_volumes` / `device_networks` / `device_monitors` / `device_software` | componentes de cada equipo, reemplazados completos en cada reporte |
|
|
| `device_accounts` | cuentas que reporta el agente: sesión iniciada, usuarios locales y grupos locales (`kind`), reemplazadas en cada reporte (migración `027`) |
|
|
| `device_changes` | diff entre reportes (RAM, discos, software instalado/desinstalado) |
|
|
| `system_settings` (IA) | `ai_api_key_enc` (AES-256-GCM), `ai_model`, `ai_edit_version` (migración 031) |
|
|
| `ai_usage` | consumo por llamada a la IA (`user_id`, `action`, `model`, `input_tokens`, `output_tokens`, `cost_usd` estimado, `created_at`) (migración 032) |
|
|
| `user_modules` | acceso por módulo y usuario (`user_id`, `module`, `enabled`, `updated_by`); sin fila = habilitado (migración 030) |
|
|
| `device_viewers` | usuarios con acceso de solo lectura compartido al menú Equipos (`user_id`, `granted_by`) |
|
|
| `inventory_tokens` | tokens de ingesta del agente (solo hash sha256) |
|
|
|
|
## Relaciones clave
|
|
|
|
- `users.role_id -> roles.id`, `users.department_id -> departments.id`
|
|
- `departments.parent_id -> departments.id` (jerarquía opcional, ej. TI → Desarrollo)
|
|
- `projects.status_id -> project_statuses.id`, `projects.priority_id -> priorities.id`,
|
|
`projects.project_type_id -> project_types.id`, `projects.owner_user_id -> users.id`,
|
|
`projects.requesting_department_id / responsible_department_id -> departments.id`
|
|
- `project_members.project_id -> projects.id`, `project_members.user_id -> users.id`
|
|
- `activities.project_id -> projects.id`, `activities.status_id -> activity_statuses.id`,
|
|
`activities.priority_id -> priorities.id`, `activities.responsible_user_id -> users.id`
|
|
- `activity_dependencies.activity_id / depends_on_activity_id -> activities.id`
|
|
- `tasks.activity_id -> activities.id`, `tasks.responsible_user_id -> users.id` (nullable)
|
|
- `comments.entity_id` / `attachments.entity_id` apuntan a `projects.id` o `activities.id` según
|
|
`entity_type` (sin FK física cruzada — se valida en el servicio)
|
|
|
|
## Reglas de datos
|
|
|
|
- `projects.progress_pct` y `activities.progress_pct` son **calculados por el servicio**, no
|
|
editables directo por el cliente: actividad con tareas = % de tareas hechas; actividad sin
|
|
tareas = manual (0-100); proyecto = promedio del avance de sus actividades.
|
|
- `users.password_hash` con bcrypt, nunca texto plano ni reversible.
|
|
- `users.session_version` (migración `028`) comienza en 1 y aumenta al restablecer clave;
|
|
cada JWT de sesión debe tener la misma versión. Cambiar clave desde la aplicación conserva la sesión actual y revoca las demás.
|
|
- `user_sessions` (migración `033`) guarda cada sesión por `public_id` (el `sid` del JWT), usuario,
|
|
IP, agente, inicio, última actividad, vencimiento y revocación. Su FK a `users` usa `ON DELETE CASCADE`.
|
|
El backend limpia al iniciar sesión las filas vencidas o revocadas hace más de 30 días.
|
|
Aplicar `033_user_sessions.sql` antes de iniciar el backend nuevo: los JWT anteriores, sin `sid`, exigirán un nuevo inicio de sesión.
|
|
- Migración `029`: `edit_version` comienza en 1 en proyectos, comentarios, actividades, tareas,
|
|
departamentos, usuarios, catálogos, notas, tareas recurrentes y equipos. El `UPDATE` de edición
|
|
verifica la versión y la incrementa atómicamente; sin coincidencia responde 409. En
|
|
`system_settings`, las claves `company_edit_version` y `smtp_edit_version` serializan los
|
|
guardados de cada panel dentro de una transacción. Aplicar 029 antes de iniciar el backend nuevo.
|
|
- `users.status` (`pending_verification`/`pending_approval`/`active`/`invited`) es el ciclo de vida
|
|
de la cuenta, independiente de `is_active` (que sigue siendo el interruptor manual del admin
|
|
para desactivar a alguien ya activo). Un usuario `pending_approval` rechazado se **borra**
|
|
físicamente (no hay estado "rechazado" — se audita el email en `audit_log` antes de borrar).
|
|
- `invited`: cuenta placeholder creada al agregar por correo a alguien que aún no es usuario desde
|
|
Miembros de un proyecto (`name`=`email`, `password_hash` aleatorio inservible, reutiliza
|
|
`verification_token`/`verification_token_expires` — 7 días en vez de 24h). Al aceptar la
|
|
invitación (`POST /api/auth/accept-invite`) pasa directo a `active` **sin pasar por
|
|
`pending_approval`** — a diferencia del autorregistro público, aquí quien lo invitó ya lo vetó al
|
|
agregarlo a su proyecto. La fila de `project_members` se crea en el momento de invitar, no al
|
|
aceptar, así que en cuanto acepta ya aparece como miembro sin pasos adicionales.
|
|
- `recurring_tasks`: `project_id`/`responsible_user_id` son **opcionales** (NULL) — se puede crear
|
|
directo desde "Tareas recurrentes" o promoviendo una tarea puntual de una actividad
|
|
(`project_id` queda como referencia del proyecto de origen, la tarea puntual original NO se
|
|
borra). `last_reminder_sent_at` se limpia a NULL cada vez que se completa un ciclo
|
|
(`POST /:id/complete`, que suma `interval_value`/`interval_unit` a `next_due_date`) — es lo que
|
|
evita reenviar el correo dos veces en el mismo ciclo. El envío real lo hace un job en el propio
|
|
proceso del backend (`setInterval` cada 15 min, sin cron del SO ni cola nueva).
|
|
- `system_settings.value` para `smtp_pass_enc` va **cifrado** (AES-256-GCM, `ENCRYPTION_KEY` del
|
|
`.env`) — nunca en texto plano, nunca se devuelve por la API (solo `configured: true/false`).
|
|
- `system_settings`: `company_name`/`company_logo_filename`/`company_logo_mime` — marca configurable
|
|
(nombre y logo de la empresa, `GET /api/company` público, `PUT` solo superadmin), mismo patrón
|
|
clave/valor que SMTP, sin tabla nueva. El archivo del logo vive en `STORAGE_ROOT/branding/`, un
|
|
solo archivo a la vez (se borra el anterior al subir uno nuevo). Sin valor guardado = "LinkedCorp"
|
|
y logo estático embebido (comportamiento por defecto, no rompe instalaciones existentes).
|
|
- `activities.sort_order`/`tasks.sort_order`: orden manual (arrastrar con el mouse en la UI). `0` =
|
|
nunca reordenado a mano (la migración `012` lo inicializa con el `id` para preservar el orden
|
|
previo); el endpoint de reordenar siempre escribe posiciones `1..N`, nunca `0`, para que esa
|
|
migración siga siendo idempotente en deploys futuros sin pisar un orden real ya guardado.
|
|
- Borrado lógico donde aplique (`users.is_active`); nunca DELETE físico de usuarios **activos** con
|
|
historial (los `pending_approval` rechazados sí se borran, ver arriba).
|
|
- `projects.deleted_at`/`deleted_by`: eliminación lógica (quien puede editar el proyecto puede
|
|
"eliminarlo" — se oculta de listados/dashboard, sigue en BD). Borrado físico (`DELETE`, arrastra
|
|
actividades/comentarios/adjuntos por `ON DELETE CASCADE`) solo vía `permanentlyDeleteProject`,
|
|
solo superadmin, y solo sobre un proyecto que ya tiene `deleted_at` (dos pasos, nunca directo).
|
|
- `activities.deleted_at`/`deleted_by` y `tasks.deleted_at`/`deleted_by` (desde v2.5.0): antes se
|
|
borraban físico sin poder recuperar; ahora es lógica igual que `projects` — quien tiene permiso
|
|
de Escritura en el proyecto puede eliminarlas y restaurarlas (`POST .../restore`), sin borrado
|
|
físico ni endpoint de purga definitiva (no hizo falta, a diferencia de `projects`).
|
|
- `project_members.role_in_project`: `'owner'` (dueño, se asigna solo al crear el proyecto, nunca
|
|
se toca desde el endpoint de rol), `'write'` (permiso de Escritura: crear/editar/eliminar
|
|
(lógico) actividades y tareas del proyecto — nunca el proyecto en sí), `NULL` = Lectura (solo
|
|
ver). **Compartir un proyecto (agregar/quitar miembros, cambiar su nivel) es exclusivo del
|
|
dueño** (`projects/service.js#canManageMembers`) — ni un rol global de administración puede
|
|
hacerlo por él. `'admin'` es un valor legado (de un tercer nivel "administrador de proyecto"
|
|
que ya no existe) que se sigue respetando como Escritura en filas viejas, pero no se asigna
|
|
desde la UI. Los roles superadmin/admin/it_manager ven, editan y pueden borrar (lógico)
|
|
cualquier proyecto igual que antes (soporte/supervisión) — la única acción que queda exclusiva
|
|
del dueño, sin excepción ni para ellos, es compartir/dejar de compartir el proyecto.
|
|
- Empresa asumida única (no hay tabla `companies` en el MVP).
|
|
- Producción siempre usa `192.168.222.32`. Desarrollo siempre usa `192.168.222.20`. No mezclar.
|
|
- `error_logs`: sin límite de tiempo, pero se recorta solo a los 1000 registros más recientes en
|
|
cada inserción (evita crecimiento sin control si algo entra en loop de errores). Se puede vaciar
|
|
del todo desde el panel ("Limpiar registro"). `severity` (alta/media/baja) nunca se etiqueta a
|
|
mano: backend y errores de render de React = alta, errores JS sueltos = media, promesas
|
|
rechazadas sin manejar = baja.
|
|
- `vault_entries` (desde v2.7.0): `password_enc`/`notes_enc` guardan el resultado de `encrypt()`
|
|
(AES-256-GCM, `backend/src/utils/crypto.js`, mismo mecanismo ya usado para la clave SMTP) —
|
|
nunca texto plano. Privado por usuario, sin excepción (ni superadmin ve lo de otro). El acceso
|
|
al menú "Contraseñas" además pide un PIN de 4 dígitos (`users.vault_pin_hash`, bcrypt) — el PIN
|
|
es un candado de acceso, NO participa del cifrado (sería débil para eso). Cambiar/resetear el
|
|
PIN (incluso por correo, `vault_pin_reset_token`) nunca toca `vault_entries` — es un cambio de
|
|
candado, no una recuperación de datos. Cada vez que se revela/crea/edita/elimina una entrada, o
|
|
se desbloquea el candado, queda en `audit_log`. Compartir una entrada con otro usuario queda
|
|
para una fase 2 (no implementado).
|
|
- `vault_entries.icon_key` (desde v2.7.2): ícono elegido por el usuario para la tarjeta (catálogo
|
|
fijo, validado también en el backend — `vault/service.js#ICON_KEYS` debe coincidir siempre con
|
|
el del frontend). A propósito NO se usa ningún servicio externo de favicon (revelaría a
|
|
terceros qué URLs internas tiene guardadas cada usuario). `NULL` = sin elegir, el frontend
|
|
muestra la inicial del nombre.
|
|
|
|
## Riesgos o decisiones
|
|
|
|
- Sin triggers de BD para recalcular avances: toda la lógica vive en la capa `service/` del backend
|
|
(más simple de depurar y de extender en fase 2).
|
|
- `risk_level` es un campo simple en `projects` (bajo/medio/alto) — el módulo completo de Riesgos
|
|
(probabilidad/impacto/mitigación) queda para fase 2, sin necesitar migrar este campo.
|
|
|
|
## Archivos relacionados
|
|
|
|
- `backend/.env.example`
|
|
- `backend/database/migrations/*.sql`
|
|
- `backend/database/seeds/*.sql`
|
|
|
|
---
|
|
|
|
## Crear la base de datos en DESARROLLO (.20)
|
|
|
|
Ya ejecutado para este proyecto (2026-09-03):
|
|
|
|
```bash
|
|
bash /home/mgonzalez/desarrollo/project-md-templates/crear-db-dev.sh project_linkedcorp project_linkedcorp
|
|
```
|
|
|
|
BD `project_linkedcorp` y usuario `project_linkedcorp`@`192.168.222.40` creados en `192.168.222.20`.
|
|
Las credenciales están en `backend/.env` (no versionado).
|
|
|
|
Verificar:
|
|
|
|
```bash
|
|
mysql --defaults-extra-file=~/.config/mysql/dev20-admin.cnf -e "SHOW DATABASES LIKE 'project_linkedcorp';"
|
|
```
|
|
|
|
Aplicar migraciones/seeds (una vez creado `backend/src/scripts/migrate-db.js`):
|
|
|
|
```bash
|
|
cd backend
|
|
set -a && source .env && set +a
|
|
node src/scripts/migrate-db.js
|
|
node src/scripts/seed-db.js
|
|
```
|
|
|
|
---
|
|
|
|
## Setup de base de datos en producción — ejecutado el 2026-09-06
|
|
|
|
BD `project_linkedcorp` creada en `.32` con `crear-db-prod.sh` (grant a `192.168.222.3`). Detalle
|
|
del proceso y estado real en `PRODUCCION.md`.
|
|
|
|
Migración en el servidor de aplicaciones (`.3`):
|
|
|
|
```bash
|
|
ssh deploy@192.168.222.3 -p 2203
|
|
cd /opt/apps/project.linkedcorp.net/source/backend
|
|
set -a && source /opt/apps/project.linkedcorp.net/backend/.env && set +a
|
|
node src/scripts/migrate-db.js
|
|
node src/scripts/seed-db.js # solo catálogos/roles; sin datos de ejemplo en produccion
|
|
node src/scripts/create-admin.js "Nombre" correo@dominio # solo la primera vez, bootstrap manual
|
|
```
|
|
|
|
---
|
|
|
|
## Actualizaciones de esquema en producción
|
|
|
|
Método: archivos SQL idempotentes en `backend/database/migrations/00X-nombre.sql`
|
|
(`CREATE TABLE IF NOT EXISTS` / `information_schema` guards), aplicados con
|
|
`node src/scripts/migrate-db.js`. El runner lee todos los `.sql`, los ordena por nombre y los
|
|
ejecuta **todos en cada corrida**; no lleva tabla de historial. Por eso `033_user_sessions.sql`
|
|
usa `CREATE TABLE IF NOT EXISTS`. Siempre backup (`mysqldump --single-transaction`) antes de migrar
|
|
en producción.
|
|
|
|
- Equipos (desde v2.15.0, migración `023_devices.sql`): `devices.deviceid` UNIQUE; `assigned_user_id`
|
|
y `department_id` son datos manuales (el agente nunca los toca). Hijos con `ON DELETE CASCADE`.
|
|
`device_software.is_component` (migración `024`) marca actualizaciones/runtimes/drivers/paquetes del
|
|
sistema (según `system_category` del agente + nombre, ver `normalizer.js#isComponent`); las vistas de
|
|
software lo ocultan salvo que se marque la casilla. `inventory_tokens` guarda el hash (`token_hash`, valida la ingesta) y el token cifrado
|
|
(`token_enc`, migración `025`, AES-256-GCM con `ENCRYPTION_KEY`) para regenerar el instalador.
|
|
|
|
## Cambio de correo (migración 035, no aplicada)
|
|
|
|
`email_change_requests` guarda usuario, correo anterior/nuevo, estado, decisión, motivo y fechas. Los tokens de aprobación y confirmación se guardan solo como hash SHA-256. Índices únicos sobre columnas generadas impiden más de una solicitud abierta por usuario o correo nuevo; el cotejamiento `utf8mb4_unicode_ci` evita duplicados por mayúsculas. `notifications.entity_type` agrega `email_change_request` y conserva `project`, `activity` y `meeting`. El migrador reejecuta todos los archivos; la tabla se crea con `IF NOT EXISTS`.
|