api
Backend NestJS (ESM) del ERP base. Proxy hacia SAP Business One Service Layer.
cp .env.example .env
npm run start:dev # http://localhost:3000/api/health
npm test # unitarios
npm run test:e2e # e2e
npm run lint
Configuración: src/config/env.schema.ts valida el .env al arrancar (zod); si algo es inválido, el proceso no inicia.
Nota: instalar dependencias con npm 11 (npx npm@11 install); npm 10 falla al resolver peers con TypeScript 6.
Autenticación
Login web contra EmployeesInfo (U_WEB_USER / U_WEB_PASS); el backend usa la sesión SAP compartida.
Guard global: todo /api/* exige Authorization: Bearer <token> salvo rutas @Public().
| Endpoint | Descripción |
|---|---|
POST /api/auth/login {username, password} |
Devuelve {token, user, company} |
GET /api/auth/me |
Usuario de la sesión |
POST /api/auth/logout |
Cierra la sesión (204) |
- Una sesión por usuario (un nuevo login invalida el token anterior); timeout deslizante
SESSION_TIMEOUT_MINUTES(30). - Tras 5 fallos por usuario (o 20 por IP) en 15 min responde 429.
- Sesiones en memoria: ejecutar un solo proceso.
curl -s -H 'Content-Type: application/json' -d '{"username":"USUARIO","password":"CLAVE"}' localhost:3000/api/auth/login
Permisos de menú
Se guardan en SQLite (SQLITE_PATH, por defecto ./data/app.sqlite, fuera del repo). Un usuario web (U_WEB_USER) tiene una lista de patrones de ruta:
| Patrón | Permite |
|---|---|
* |
Todo (administrador) |
/ventas/* |
/ventas y todo lo que cuelga de /ventas/ (no /ventas-x) |
/ventas/ordenes |
Solo esa ruta |
| Endpoint | Quién | Descripción |
|---|---|---|
GET /api/user/menu |
Cualquier sesión | {allowedPaths} del usuario actual |
GET /api/permissions, GET /api/permissions/:webUser |
Administrador | Consulta |
PUT /api/permissions/:webUser {paths} |
Administrador | Reemplaza los permisos |
DELETE /api/permissions/:webUser |
Administrador | Elimina los permisos |
ADMIN_WEB_USERS=george,anagarantiza*a esos usuarios en cada arranque (nunca se queda el sistema sin acceso).- El sistema no deja quitar el acceso total al último administrador (409).
- El esquema se sincroniza automáticamente (
synchronize); pasar a migraciones cuando se estabilice por cliente. - Si
better-sqlite3falla al instalar en una máquina nueva:npm rebuild better-sqlite3.
Proxy del Service Layer (/api/sl)
El frontend arma los parámetros OData y el cuerpo; el backend solo valida y ejecuta (sesión SAP, cola, reintento y errores incluidos). El frontend envía el nombre del recurso, nunca la URL.
| Llamada | Descripción |
|---|---|
GET /api/sl/:recurso?$select&$filter&$orderby&$top&$skip |
Lista. Devuelve value y odata.count. $top por defecto 50, máx. 500. |
GET /api/sl/:recurso/:clave?$select |
Un registro. La clave se valida y escapa según su tipo. |
POST /api/sl/:recurso |
Crea; devuelve lo que responde SAP (ej. DocEntry, DocNum). |
PATCH /api/sl/:recurso/:clave |
Modifica (204). Acepta If-Match con el odata.etag leído (concurrencia optimista: SAP responde 412 si el documento cambió). |
POST /api/sl/:recurso/:clave/:accion |
Acción de un registro (hoy Cancel de pagos y asientos). Solo las que el recurso declara en actions; requiere el permiso de escritura. |
POST /api/sl/:accion |
Acción de solo consulta sin registro (SeriesService_GetDocumentSeries, series de numeración): cualquier sesión. |
Reglas:
- Solo recursos registrados en
src/sl/sl-resources.ts; el resto es 404. Nunca registrarEmployeesInfo(contieneU_WEB_PASS). - Cada recurso declara clave, métodos y la ruta de menú que gobierna lectura/escritura. Los catálogos de consulta (
Items,Warehouses...) se leen con cualquier sesión. - Parámetros OData fuera de la lista (
$apply...) → 400.$expandsolo si el recurso declara qué colecciones admite. - Cabeceras B1S-* permitidas:
B1S-ReplaceCollectionsOnPatch(PATCH),B1S-CaseInsensitiveyB1S-PageSize(GET). Se fijan solas:B1S-PageSize=$topyB1S-CaseInsensitivesi hay$filter. - No hay DELETE. Las acciones (
Cancel) existen solo donde el recurso las declara enactions(pagos recibidos y efectuados, asientos);Closey las demás se agregan por recurso cuando haga falta. - Las escrituras y las acciones se registran en la auditoría (ver abajo) y dejan una línea
AUDITen el log.
Agregar un recurso nuevo = una línea en SL_RESOURCES. El test en vivo (SAP_LIVE=1 npm run test:e2e) comprueba que todos existan en el Service Layer del cliente.
Auditoría
Tabla audit_log en la misma SQLite. Registra inicios de sesión (correctos, fallidos con su motivo y cierres), cada creación, modificación y acción (sl_action) que pasa por el proxy (recurso, clave, resultado y solo los nombres de los campos, nunca valores ni contraseñas), las operaciones denegadas por permisos y los cambios de permisos de menú. Las lecturas no se auditan. AuditService.record nunca lanza: un fallo del registro no cambia la operación del usuario.
| Endpoint | Quién | Descripción |
|---|---|---|
GET /api/audit?usuario&accion&recurso&resultado&desde&hasta&page |
Administrador | Consulta; 50 por página (máx. 200) |
Pendiente de afinar: retención programada, IP real detrás del proxy de desarrollo, exportar a CSV.
Reportes del API Gateway (Crystal Reports)
Servicio aparte del Service Layer (APIGATEWAY_* en el .env): su propio login (/login con base, usuario, clave e instancia), su propia sesión (se renueva sola ante un 401) y su propia cola (APIGATEWAY_MAX_CONCURRENCY reportes a la vez; generar uno pesa). Sin APIGATEWAY_URL el módulo responde 503 y el resto de la API no se ve afectado.
Llama a rs/v1/LoadAuthorizedCRList (los reportes autorizados), rs/v1/LoadCR (sus parámetros) y rs/v1/ExportPDFData (el PDF, en base64; responde 201 y, si falla, un texto de error con 200 o (---) si el reporte no produjo nada).
| Endpoint | Quién | Descripción |
|---|---|---|
GET /api/gateway/authorized |
Administrador | Reportes que el gateway autoriza |
GET /api/gateway/authorized/:code/parameters |
Administrador | Parámetros de un reporte, como definiciones de matrícula |
GET /api/gateway/registry |
Administrador | Reportes matriculados |
PUT /api/gateway/registry/:code |
Administrador | Matricula o actualiza un reporte (título, categoría, habilitado y cómo se pide cada parámetro) |
DELETE /api/gateway/registry/:code |
Administrador | Quita un reporte de la matrícula |
GET /api/gateway/reports |
Permiso de menú /reportes/ejecutar |
Reportes habilitados |
POST /api/gateway/reports/:code/export {values} |
Permiso de menú /reportes/ejecutar |
Genera el PDF |
GET /api/gateway/documents?screen=/ventas/entrega |
Permiso de menú de esa pantalla | Formatos PDF asociados a un documento (el por defecto primero) |
POST /api/gateway/documents/export {screen, code, docEntry} |
Permiso de menú de esa pantalla | PDF del documento: el DocEntry va en el parámetro asociado en la matrícula |
La matrícula (tabla gateway_reports de la SQLite) es lo que arma el cuerpo de ExportPDFData: el usuario solo manda valores y el servidor los ordena según los parámetros matriculados (nombre y tipo de Crystal), exige los obligatorios y descarta lo que no esté matriculado. La auditoría registra report_register, report_unregister y report_export (solo los nombres de los parámetros, nunca los valores).