# api Backend NestJS (ESM) del ERP base. Proxy hacia SAP Business One Service Layer. ```bash 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 ` 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. ```bash 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,ana` garantiza `*` 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-sqlite3` falla 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 registrar `EmployeesInfo` (contiene `U_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. `$expand` solo si el recurso declara qué colecciones admite. - Cabeceras B1S-* permitidas: `B1S-ReplaceCollectionsOnPatch` (PATCH), `B1S-CaseInsensitive` y `B1S-PageSize` (GET). Se fijan solas: `B1S-PageSize` = `$top` y `B1S-CaseInsensitive` si hay `$filter`. - No hay DELETE. Las acciones (`Cancel`) existen solo donde el recurso las declara en `actions` (pagos recibidos y efectuados, asientos); `Close` y 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 `AUDIT` en 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). ## Consultas a la base de datos de SAP (solo lectura) Algunas pantallas necesitan datos que el Service Layer no entrega bien (por ejemplo, el stock de un lote por almacén). El módulo `src/sapdb/` ejecuta **consultas registradas, de solo lectura**, directamente en la base de SAP. Hoy el motor es **HANA** (`hdb`, JavaScript puro, sin módulos nativos); la interfaz (`SapDbAdapter`) deja listo sumar MSSQL con su adaptador. - **Configuración (`SAPDB_*` en el `.env`):** `SAPDB_ENGINE=hana`, servidor, puerto (30015), usuario, clave y `SAPDB_SCHEMA` (la base de la empresa). Sin motor el módulo responde 503 y el resto de la API no se ve afectado. Usar un usuario de la base que **solo pueda leer**. - **El frontend nunca manda SQL.** Pide una consulta por su nombre con sus parámetros: `POST /api/sapdb/queries/:name` `{params}` → `{rows, truncated}`; `GET /api/sapdb/queries` lista las que el usuario puede ejecutar. - **Registro de consultas** (`sapdb-queries.ts` + `sapdb.extensions.ts`, vacío en `main`; la rama de cada cliente agrega las suyas): cada una declara su SQL por motor, sus parámetros con nombre (`:lote`, tipos `string | number | date`), el permiso de menú que basta para ejecutarla (`menu`, o `null` = cualquier sesión) y un tope de filas. Al arrancar se rechaza una consulta que no sea un único `SELECT`/`WITH`, que use verbos que escriben, que repita nombre o cuyos parámetros no coincidan con el SQL. - **Parámetros:** el SQL lleva `:nombre` y el servidor los cambia por `?` con los valores como argumentos; un valor nunca se concatena al SQL. - **Operación:** una conexión por consulta (se abre, fija el esquema, ejecuta y cierra; nunca confirma nada), cola propia (`SAPDB_MAX_CONCURRENCY`), tiempo máximo (`SAPDB_TIMEOUT`) y tope de filas (`SAPDB_MAX_ROWS`). La auditoría registra `db_query` con el nombre, los nombres de los parámetros y las filas devueltas, nunca los valores.