Files
Milton Gonzalez 420a794c9e feat: version 4.16.0 -- Equipos: tipo de licencia y software agrupado por fabricante
Clasificador por reglas sin IA, correccion manual (migracion 040) y filas desplegables en ListView.
Flyout por encima de popups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:36:02 -05:00

343 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API - project.linkedcorp.net
## Base URL
- Desarrollo: `http://localhost:4019/api`
- Producción: `https://project.linkedcorp.net/api`
## Autenticacion
- Tipo: Bearer JWT (`Authorization: Bearer <token>`), emitido por `/api/auth/login`.
- El backend revalida `is_active` del usuario en cada request (no solo la firma del JWT).
- El JWT incluye `sv` y `sid`. Cada petición valida la cuenta y la fila activa de `user_sessions`.
Cambiar la clave conserva la sesión actual y cierra las demás; restablecerla cierra todas.
Una sesión revocada responde `401` con `code: "SESSION_REVOKED"`; los JWT antiguos sin `sid`
requieren volver a iniciar sesión tras aplicar la migración `033`.
## Formato de respuesta
```json
{
"success": true,
"data": {},
"error": null
}
```
## Edición concurrente
Los `PUT` de proyectos, comentarios, actividades, tareas, departamentos, usuarios, los cuatro
catálogos, notas, tareas recurrentes, equipos y SMTP exigen `editVersion` (entero positivo)
obtenido de la lectura correspondiente como `edit_version`. El `PUT /api/company` exige el mismo
campo en multipart. La API responde `428` si falta o es inválido y `409` si otro guardado cambió
el registro. En empresa y SMTP la respuesta `GET` incluye `edit_version`.
Las reuniones siguen el mismo contrato (`PUT /api/meetings/:id`).
## Conexiones externas
`/api/integrations` (JWT). Superadmin: `GET/POST /connections`, `PUT /connections/:id`,
`POST /connections/:id/test`, `GET /connections/:id/browse?source=&table=` (fuentes, tablas,
estructura + 20 filas), `PUT /sources/:purpose` (`patients`/`employees`, valida el mapeo contra la
estructura real), `POST /sources/patients/sync {full}`. Admin y superadmin: `GET /sources` (el admin
solo ve estado), `GET /employees[?available=1]`, `POST /employees/sync`, `GET /providers?search=` (busca en la fuente
Proveedores, marca `employee_id`), `POST /providers/:codigo/import {lastNames, firstNames, position}`. El token nunca se devuelve.
En la Agenda (recepción): `GET /api/clinic/patients/source`, `GET /api/clinic/patients/external?search=`
(modo en vivo) y `POST /api/clinic/patients/external/:codigo/import`. `POST /api/users` acepta
`employeeId` (Desde nómina).
## Agenda médica (v4.7.0)
Todas las rutas bajo `/api/clinic` exigen JWT, módulo `clinic` y fila activa en `clinic_staff`.
Sin la fila se responde 403. `scope=own` solo accede a su médico; `scope=all` gestiona todos.
Configuración exige además rol admin/superadmin. Fechas `YYYY-MM-DD HH:mm:ss` en hora civil
`America/Guayaquil`; rangos de listado de máximo 93 días.
| Método y ruta | Uso |
|---|---|
| `GET /staff/me` | alcance y condición de médico |
| `GET /doctors` | médicos visibles con horario y tipos activos |
| `GET /availability?doctorId=&date=&typeId=` | huecos de 15 minutos con duración del tipo |
| `GET /appointments?from=&to=[&doctorId=][&patientId=]` | citas del rango, incluidas canceladas y eliminadas (`deleted_at`) para trazabilidad; `patientId` solo recepción |
| `POST /appointments`, `PUT /appointments/:id` | crear o reprogramar/editar; `PUT` exige `editVersion` |
| `POST /appointments/:id/delete` | `{reason,editVersion}` borrado lógico (solo recepción): libera el horario, conserva la fila con quién/cuándo/motivo |
| `POST /appointments/:id/status` | `{status,editVersion}`; médico propio solo `attended`/`no_show` |
| `POST /appointments/:id/cancel` | `{reason,editVersion}`, solo recepción |
| `GET /blocks?from=&to=[&doctorId=]` | ocurrencias expandidas del rango |
| `POST /blocks`, `PUT /blocks/:id` | bloqueo; `{confirmAffected:true}` para guardar si afecta citas |
| `DELETE /blocks/:id?editVersion=` | eliminar bloqueo con versión |
| `GET /patients?search=&page=&pageSize=` | búsqueda paginada, solo recepción |
| `GET /patients/:id/appointments` | últimas 100 citas, solo recepción |
| `POST /patients`, `PUT /patients/:id` | alta y edición, solo recepción; `PUT` exige `editVersion` |
| `GET /config/staff`, `PUT /config/staff/:id` | personal y permiso interno |
| `GET/PUT /config/schedules/:id` | tramos; `PUT` recibe `{segments:[{weekday,startsTime,endsTime}]}` |
| `GET/POST /config/types`, `PUT /config/types/:id` | tipos de cita |
Una cita fuera del horario o en choque devuelve 409 con motivo concreto. Un bloqueo afectado
responde `{requiresConfirmation:true,affected:[...]}` y no se guarda sin confirmación. Las
ediciones con versión obsoleta devuelven 409. Las altas, ediciones, cambios de estado y
cancelaciones se auditan sin copiar datos de pacientes al detalle de auditoría.
## Calendario de reuniones (v4.4.0)
Todas las rutas requieren JWT y módulo `calendar`. Las fechas son `YYYY-MM-DD HH:mm:ss` en hora
civil `America/Guayaquil` (UTC-5). `GET /api/meetings?from=&to=&page=&pageSize=[&projectId=]` (sin `projectId`: solo reuniones que el
usuario organiza o a las que fue invitado, cualquier rol; con `projectId`: todas las del proyecto si
puede leerlo, si no 404) devuelve
`{items,total,page,pageSize}` con ocurrencias expandidas (`occurrence_start`, `occurrence_end`);
rango máximo 93 días y hasta 1500 ocurrencias por página (predeterminado: 1500).
`GET /api/meetings/activities?from=&to=` devuelve hasta 1000 vencimientos de actividades donde el
usuario es responsable (con `is_closed` y `overdue`).
`GET /api/meetings/tasks?from=&to=`: tareas donde el usuario es responsable (`activity_id`, `project_id`,
`is_closed`, `overdue`). `GET /api/meetings/projects?from=&to=`: proyectos donde es dueño o miembro,
una fila por fecha en rango (`milestone`: `start` | `end`). Ambas requieren módulo `projects`.
`GET /api/meetings/recurring?from=&to=` (requiere también el módulo `recurring_tasks`) devuelve los
próximos vencimientos de las tareas recurrentes activas que el usuario creó o de las que es responsable, calculados al vuelo
desde `next_due_date` con la misma regla que `advanceCycle` y cortados en `end_date`. Cada fila trae
`due_date`, `is_next` (el ciclo pendiente real), `overdue` y `can_open` (si puede abrirla en Tareas recurrentes).
Todas las filas de calendario traen `shared` (lo creó/organiza otra persona y me incluye).
- `GET /api/meetings/:id`: detalle, participantes con RSVP y excepciones.
- `POST /api/meetings`: `{title,description,startsAt,endsAt,allDay,location,videoUrl,projectId,reminderMinutes,participantUserIds,externalEmails,recurrence}`.
- `PUT /api/meetings/:id`: mismo cuerpo con `editVersion` (aplica a toda la serie).
- `DELETE /api/meetings/:id`: **cancelación lógica** de toda la serie, con `METHOD:CANCEL`.
- `POST /api/meetings/:id/occurrences/:date/cancel`: excluye una ocurrencia; `date` URL encoded.
- `PUT /api/meetings/:id/rsvp`: `{status:"accepted"|"declined"|"tentative"}`; solo participante propio.
`recurrence` es `null` o `{frequency:"daily"|"weekly"|"monthly",interval,weekdays,until,count}`;
`weekdays` usa 0=lunes a 6=domingo. Se exige exactamente uno de `until` o `count` y máximo
365 ocurrencias. Cuando hay choque de participantes, POST/PUT devuelve
`{requiresConfirmation:true,warnings:[nombres]}` sin guardar; reenviar con
`confirmConflicts:true` confirma. Correo y avisos se despachan en segundo plano; sin SMTP el correo
se registra en consola de desarrollo. Antes de usar estas rutas se debe aplicar la migración 034.
## Endpoints principales
### Auth
- `POST /api/auth/login` — `{ email, password }` → `{ token, user }`. Si el usuario no esta
`active` (`pending_verification`/`pending_approval`) o `is_active=0`, `403` con mensaje especifico.
- `POST /api/auth/register` — público. `{ name, email, password }` → si es el primer usuario del
sistema: `{ bootstrap: true, token, user }` (superadmin, sesion directa); si no:
`{ bootstrap: false }` y se envia (o loguea, sin SMTP) un correo de verificación.
- `GET /api/auth/verify-email?token=...` — público. Pasa el usuario a `pending_approval` y notifica
in-app a los `admin`/`superadmin` activos.
- `GET /api/auth/invite?token=...` — público. `{ email }` de la invitación (para prellenar el
formulario), o `400` si es inválida/expiró.
- `POST /api/auth/accept-invite` — público. `{ token, name, password }` → completa la cuenta
creada por `POST /api/projects/:id/members` con `{ email }` (ver arriba): elige nombre y clave,
pasa a `active` directo (sin aprobación — quien invitó ya la vetó) y devuelve `{ token, user }`
con sesión iniciada, igual que el bootstrap del primer usuario.
- `POST /api/auth/forgot-password` — público. `{ email }` → respuesta **siempre genérica**.
- `POST /api/auth/reset-password` — público. `{ token, password }`.
- `PUT /api/auth/change-password` — autenticado. `{ currentPassword, newPassword }`, exige la
clave actual (distinto de `reset-password`, que usa un token de correo sin sesión).
- `POST /api/auth/logout` — revoca la sesión actual.
- `GET /api/auth/me` — usuario actual
- `GET /api/auth/sessions` — sesiones activas propias (`id`, navegador, SO, IP, fechas, `current`).
- `DELETE /api/auth/sessions/:id` — cierra una sesión propia; la actual equivale a logout.
- `POST /api/auth/sessions/revoke-others` — cierra las demás sesiones propias.
### Usuarios (`/api/users`) — lectura para cualquier autenticado; escritura solo Administrador/Superadministrador
- `GET /api/users` · `POST /api/users` · `PUT /api/users/:id` · `PATCH /api/users/:id/active`
- `GET /api/users/:id/sessions` · `DELETE /api/users/:id/sessions/:sessionId` ·
`POST /api/users/:id/sessions/revoke-all` — solo admin/superadmin; un admin no puede administrar
sesiones de un superadmin. Desactivar un usuario también revoca sus sesiones.
- `POST /api/users/:id/approve` — `{ roleId?, departmentId?, editVersion }`, solo sobre usuarios `pending_approval`; también rechaza cambios concurrentes con `409`.
- `POST /api/users/:id/reject` — borra al usuario `pending_approval` (se audita el email antes)
- `GET /api/users/:id/modules` · `PUT /api/users/:id/modules` `{ modules: [...] }` — solo Superadministrador.
Módulos: `projects`, `calendar`, `recurring_tasks`, `notes`, `vault`, `devices`, `organization`. No aplica a
un superadmin (`400`). `GET /api/auth/me` y el login devuelven `modules` (efectivos del usuario).
Las rutas de un módulo deshabilitado responden `403`; la búsqueda global devuelve vacío.
### Departamentos (`/api/departments`)
- `GET /api/departments` (árbol) · `POST /api/departments` · `PUT /api/departments/:id` · `DELETE /api/departments/:id`
### Proyectos (`/api/projects`)
- `GET /api/projects` (filtros: `statusId`, `priorityId`, `ownerUserId`, `dueSoon=true`,
`progress=hide|only`, `q`, `trash=true` solo superadmin; `page` desde 1 y `pageSize` 1–100,
predeterminado 20) → `{ items, total, page, pageSize }` · `POST /api/projects`.
- `GET /api/projects/options` → `{ id, code, name }[]` accesibles al usuario para selectores.
- `GET /api/projects/:id` · `PUT /api/projects/:id` — para editar, enviar `editVersion` obtenido
de `GET`; `428` sin versión, `409` si otra edición la cambió.
- `DELETE /api/projects/:id` — eliminación lógica (`deleted_at`/`deleted_by`), mismo permiso que
editar el proyecto. `POST /api/projects/:id/restore` y `DELETE /api/projects/:id/permanent`
(borrado físico, irreversible, arrastra actividades/comentarios/adjuntos) — ambos solo
superadmin; `permanent` exige que el proyecto ya esté en la papelera (400 si no).
- `POST /api/projects/:id/members` — `{ userId }` (usuario existente) o `{ email }` (si el correo
no pertenece a nadie todavía, crea una cuenta `invited` y le manda un correo de invitación para
que complete su registro; la fila de `project_members` ya queda creada de una vez). `roleInProject`
opcional: `'admin'` = administrador del proyecto. Responde `{ members, invited }`.
- `PUT /api/projects/:id/members/:userId/role` (`{ admin: true|false }`, otorga o quita el rol de
administrador **de ese proyecto** — un proyecto puede tener uno o más administradores, sin
importar el rol global del usuario; nunca toca al `owner`) ·
`DELETE /api/projects/:id/members/:userId`
### Actividades (`/api/activities`)
- `GET /api/projects/:projectId/activities` · `POST /api/projects/:projectId/activities`
- `GET /api/activities/:id` · `PUT /api/activities/:id`
- `DELETE /api/activities/:id` — bloqueado con `400` si la actividad tiene tareas (hay que
eliminarlas primero); limpia comentarios/adjuntos de la actividad de paso (no hay FK física
cruzada). Arrastra `activity_dependencies` por `ON DELETE CASCADE`.
- `POST /api/activities/:id/dependencies` · `DELETE /api/activities/:id/dependencies/:dependsOnId`
- `PUT /api/projects/:projectId/activities/reorder` — `{ ids: [...] }` (orden manual, arrastrar
con el mouse); posiciones 1..N, nunca 0 (0 = "nunca reordenado a mano", ver `sort_order`).
- `PUT /api/activities/:id/move` — `{ projectId }`. Mueve la actividad a otro proyecto; exige
permiso de escritura en ambos proyectos. Tareas/comentarios/adjuntos se mueven solos (referencian
`activity_id`, no `project_id`). Las dependencias existentes (en cualquiera de los dos sentidos)
se eliminan — solo tienen sentido dentro del mismo proyecto. `sort_order` vuelve a 0 en el destino.
- `POST /api/activities/:id/convert-to-project` — `{ projectName, taskActivityName }`. Crea un
proyecto nuevo (`projectName`) y, dentro, una actividad nueva (`taskActivityName`) que recibe
las tareas/comentarios/adjuntos de la actividad original; la original se borra al final (ya sin
tareas). Exige el mismo permiso que crear un proyecto (no solo escribir la actividad). Responde
`{ project, activity }` (201).
### Tareas (`/api/activities/:activityId/tasks`)
- `GET` · `POST` · `PUT /api/tasks/:id` · `PATCH /api/tasks/:id/done` · `DELETE /api/tasks/:id`
- `PUT /api/activities/:activityId/tasks/reorder` — `{ ids: [...] }` (mismo mecanismo de orden
manual que las actividades).
### Tareas recurrentes (`/api/recurring-tasks`) — menú propio, independiente de Proyectos
- `GET` · `POST` · `PUT /api/recurring-tasks/:id` · `DELETE /api/recurring-tasks/:id`
- `POST /api/recurring-tasks/:id/complete` — cierra el ciclo actual y calcula el próximo
vencimiento sumando la periodicidad (`interval_value` + `interval_unit`).
- `POST /api/recurring-tasks/from-task/:taskId` — crea una tarea recurrente a partir de una tarea
puntual de una actividad (la original no se borra); mismo body que `POST /`.
- Body: `{ name, description?, projectId?, responsibleUserId?, intervalUnit: 'days'|'months'|'years',
intervalValue, nextDueDate, endDate?, reminderDaysBefore, reminderHour: 0-23 }`. `endDate` vacío
= se repite sin fin; al completar un ciclo que la supera, `is_active` pasa a `false` solo.
- Recordatorio por correo: un job en el propio proceso del backend revisa cada 15 minutos
(`src/jobs/recurringTaskReminders.js`) y manda el correo cuando `next_due_date -
reminderDaysBefore` cae hoy, a la hora `reminderHour`. Un solo envío por ciclo
(`last_reminder_sent_at`, se limpia al completar el ciclo).
### Comentarios (`/api/comments`)
- `PUT /api/comments/:id` — `{ message, editVersion }`; `428` sin versión y `409` si cambió.
- `GET /api/comments?entityType=project&entityId=1` · `POST /api/comments`
### Adjuntos (`/api/attachments`)
- `GET /api/attachments?entityType=activity&entityId=1` · `POST /api/attachments?entityType=activity&entityId=1`
(multipart; entidad y permiso se validan antes de recibir el archivo)
- `GET /api/attachments/:id/download` · `DELETE /api/attachments/:id`
### Notas (`/api/notes`)
- `GET /api/notes` — solo las del usuario autenticado, ordenadas por `updated_at` descendente.
- `POST /api/notes` — `{ content }`. `PUT /api/notes/:id` — `{ content, editVersion }`. `DELETE /api/notes/:id`.
- Siempre privadas: una nota de otro usuario responde `404` (no `403`, para no revelar que
existe). Sin restricción de rol — cualquier autenticado tiene las suyas.
### Equipos (`/api/devices`) — superadmin / admin / it_manager (tokens: solo superadmin / admin)
- `GET /api/devices?q=&kind=&status=&stale=1&diskFull=1` · `GET /api/devices/:id` (con hardware,
red, software e historial) · `PUT /api/devices/:id` — `{ status, assignedUserId, departmentId, notes, editVersion }`.
- `GET /api/devices/dashboard` — KPIs y top (SO, tipo, software). `GET /api/devices/software` ·
`GET /api/devices/software/devices?name=` — software transversal. Cada programa trae `license_type`
(`free`/`paid`/`personal_free`/`component`/`null`), `license_manual`, `vendor` y `group`.
- `PUT /api/devices/software/classification` — `{ name, licenseType }`: corrige el tipo de un programa
en todos los equipos; `licenseType` vacío vuelve a las reglas. Roles que editan equipos; audita `classify_software`.
- `GET /api/devices/access` — `{canView, canEdit, canManage}` del usuario (lo usa el menú). Lectura (`GET` de
dashboard/listado/detalle/software) también para usuarios con acceso compartido.
- `GET/POST /api/devices/viewers` · `DELETE /api/devices/viewers/:userId` — compartir Equipos (admin/superadmin).
- `POST /api/devices/:id/cpu-year` → equipo con `cpu_year`: la IA consulta solo el nombre del procesador y devuelve el año de lanzamiento (el más antiguo si hay dos). Roles que editan equipos; `400` sin procesador o IA sin configurar, `422` si no se pudo determinar; 30/hora.
- `GET /api/devices/:id/raw` — último inventario crudo del agente.
- `GET /api/devices/tokens/:id/secret` — token en claro (descifrado) para armar el instalador; audita `reveal`.
- `GET/POST /api/devices/tokens` · `PATCH/DELETE /api/devices/tokens/:id` — tokens de ingesta
(el token en claro solo se devuelve en el `POST`).
### Ingesta del agente GLPI (sin JWT; token de ingesta)
- `POST /api/inventory` (alias `POST /front/inventory.php`) — cuerpo JSON/XML, opcionalmente
comprimido (gzip/zlib). Auth: password Basic / `Bearer` / `X-Ingest-Token` / `?token=`.
`401` sin token válido, `400` formato inválido. Responde `{"status":"ok"}` (o `<REPLY/>` en XML).
### Notificaciones (`/api/notifications`)
- `GET /api/notifications` · `PATCH /api/notifications/:id/read` · `PATCH /api/notifications/read-all`
### Búsqueda (`/api/search`)
- `GET /api/search?q=texto` — global: proyectos, actividades y tareas (de cualquier proyecto) que
coincidan. `{ projects: [...], activities: [...], tasks: [...] }`, cada item de actividades/tareas
incluye los datos de su proyecto (y actividad, para tareas) para armar la migaja de pan en el
cliente. `q` con menos de 2 caracteres devuelve listas vacías. Máx 20 resultados por tipo.
- `GET /api/search?q=texto&projectId=1` — acotado a un proyecto: solo `{ activities, tasks }` de
ESE proyecto (sin el dato del proyecto, ya es el contexto).
### Actualizaciones (`/api/updater`) — solo Superadministrador
- `GET /api/updater/status` — compara la `VERSION` local contra `CHANGELOG.md` de `main` en
Gitea (leído de forma anónima por HTTPS, sin token — requiere repo público y vista anónima
habilitada en el servidor de Gitea). `{ currentVersion, upToDate, latestVersion,
overallCriticality?, hasMigrations, pending: [{ version, date, criticality, summary }] }`.
`criticality` se deriva del dígito de versión que cambió (X.Y.Z: 1ro=alta, 2do=media,
3ro=baja), nunca se etiqueta a mano. `hasMigrations` compara los archivos de
`backend/database/migrations/` locales contra los de `main`. Es solo informativo — no aplica
ningún cambio, la actualización real sigue siendo manual.
### IA (`/api/ai`) — requiere clave configurada por el superadmin
- `GET /api/ai/status` → `{ enabled }` (cualquier autenticado).
- `POST /api/ai/projects/:projectId/plan` `{ description?, lang? }` → `{ activities: [{ name, description, durationDays, tasks: [] }] }`. Requiere módulo Proyectos y escritura en el proyecto; 30/hora.
- `POST /api/ai/projects/:projectId/plan/apply` `{ activities }` → `{ created }` (crea actividades y tareas).
- `POST /api/ai/new-project/propose` `{ objective, startDate, lang? }` → `{ name, objective, description, projectTypeId, projectTypeName, estimatedEndDate, activities }`; `400` si el objetivo es corto o la fecha inválida (sin llamar a la IA), `422` si no es un proyecto. 30/hora.
- `POST /api/ai/new-project` `{ objective, startDate, name, description?, projectTypeId?, requestingDepartmentId, activities }` → `{ id, code }`. Roles que crean proyectos.
- `POST /api/ai/projects/:projectId/report` `{ lang? }` → `{ project, generatedAt, summary, health, achievements, delays: [{ item, reason }], risks, nextSteps }`. Requiere lectura del proyecto; 30/hora.
- `GET /api/settings/ai/usage?period=month|all` → `{ period, users: [{ userId, userName, calls, inputTokens, outputTokens, costUsd, lastUsed }], total }` — solo superadmin.
- `GET/PUT /api/settings/ai` `{ apiKey?, model, editVersion }` y `POST /api/settings/ai/test` — solo superadmin; la clave nunca se devuelve.
### Log de errores (`/api/error-logs`)
- `POST /api/error-logs` — público (sin auth, rate-limited a 30/15min), lo usa el frontend para
reportar errores propios (incluso sin sesión). `{ message, stack?, url?, severity? }`
(`severity`: `alta`/`media`/`baja`; si falta o es inválida, cae en `media`). Si llega un token
válido en `Authorization`, se guarda el usuario asociado, sin exigirlo.
- `GET /api/error-logs?source=backend|frontend&severity=alta|media|baja&limit=` — solo
Superadministrador. Lista los errores más recientes (máx 1000 guardados, se recorta solo).
Backend guarda `stack`/`url`/`method`/usuario automáticamente en cada excepción 500 no manejada
(ver `errorHandler.js`) con `severity=alta` siempre — nunca se etiqueta a mano.
- `DELETE /api/error-logs` — solo Superadministrador. Borra todo el historial.
### Dashboard (`/api/dashboard`)
- `GET /api/dashboard/summary` — KPIs (activos, atrasados, pendientes/vencidas/hoy, avance promedio)
- `GET /api/dashboard/calendar?month=2026-09` — actividades por fecha límite
- `GET /api/projects/:id/gantt` — actividades con fechas/avance para el Gantt
### Catálogos (`/api/catalogs`) — lectura general, escritura solo Superadministrador
- `GET/POST/PUT/DELETE /api/catalogs/project-statuses`
- `GET/POST/PUT/DELETE /api/catalogs/activity-statuses`
- `GET/POST/PUT/DELETE /api/catalogs/priorities`
- `GET/POST/PUT/DELETE /api/catalogs/project-types`
### Auditoría (`/api/audit`) — solo Superadministrador / Administrador / Jefe de TI
- `GET /api/audit?entityType=project&entityId=1`
### Configuración SMTP (`/api/settings`) — solo Superadministrador
- `GET /api/settings/smtp` — `{ host, port, user, from, configured, edit_version }` (nunca la clave)
- `PUT /api/settings/smtp` — `{ host, port, user, from, pass?, editVersion }` (`pass` vacío = no cambiarla)
- `POST /api/settings/smtp/test` — `{ host, port, user, from, pass?, to }` (`pass` vacío = usa la
guardada). Envía un correo real con los valores del formulario (aunque no se hayan guardado) y
responde `{ sent: true }` o `400` con el error real del servidor SMTP. Rate-limited (10/15min).
### Marca de la empresa (`/api/company`)
- `GET /api/company` — público (sin auth, lo usan login/registro/recuperación). `{ name, hasLogo,
logoVersion }`; `name` por defecto "LinkedCorp" si nunca se configuró.
- `GET /api/company/logo` — público. Devuelve el archivo de imagen del logo, o `404` si no hay
ninguno configurado (el cliente cae al logo estático embebido).
- `PUT /api/company` — solo Superadministrador, multipart: `name?` (texto), `editVersion` y `logo?` (archivo,
PNG/JPG/WEBP, máx 3MB; se comprueba la firma real del archivo). Al subir un logo nuevo se borra el archivo anterior (nunca quedan
huérfanos). Guardado en `system_settings` (mismo patrón que SMTP), sin tabla nueva.
## Errores esperados
- `400` validación
- `401` no autenticado / token inválido o expirado
- `403` sin permisos para la acción (según rol)
- `404` no encontrado (incluye cuando el recurso existe pero no pertenece al usuario/alcance)
- `500` error interno (sanitizado, sin detalle de stack en producción)
## Cambio de correo (v4.6.0)
- `GET /api/account/email-change`: última solicitud propia (requiere JWT).
- `POST /api/account/email-change`: `{ newEmail, currentPassword }`; crea una solicitud tras validar la clave, con límite de 20 solicitudes por 15 minutos.
- `DELETE /api/account/email-change`: cancela la solicitud propia pendiente o aprobada.
- `GET /api/users/email-change-requests`: pendientes que el admin/superadmin puede decidir.
- `POST /api/users/email-change-requests/:id/approve`: aprueba y envía el enlace de confirmación al correo nuevo.
- `POST /api/users/email-change-requests/:id/reject`: `{ reason }` obligatorio; avisa al correo actual.
- `POST /api/auth/email-change/self-approve`: `{ token }`; enlace público de aprobación exclusiva del superadmin, un solo uso.
- `POST /api/auth/email-change/confirm`: `{ token }`; enlace público de confirmación del correo nuevo. Revoca todas las sesiones al aplicar.
Los endpoints públicos tienen límite de 20 solicitudes por 15 minutos. Los enlaces duran 24 horas; las solicitudes abiertas vencen a los 7 días. Los tokens se almacenan solo como SHA-256.