diff --git a/README_tmp.html b/README_tmp.html
deleted file mode 100644
index 2e7d454..0000000
--- a/README_tmp.html
+++ /dev/null
@@ -1,664 +0,0 @@
-
-
-
-README.md
-
-
-
-
-
-
-
-
-
-
-
-
-SuiteCoffee — Sistema de gestión para cafeterías (Dockerizado y multi‑servicio)
-SuiteCoffee es un sistema modular pensado para la gestión de cafeterías (y negocios afines), con servicios Node.js para aplicación y autenticación , bases de datos PostgreSQL separadas para negocio y multi‑tenencia, y un stack Docker Compose que facilita levantar entornos de desarrollo y producción . Incluye herramientas auxiliares como Nginx Proxy Manager (NPM) y CloudBeaver para administrar bases de datos desde el navegador.
-
-Repositorio: https://gitea.mateosaldain.uy/msaldain/SuiteCoffee.git
-
-
-Tabla de contenidos
-
-
-Arquitectura
-Servicios principales
-
-app (Node.js / Express): API de negocio y páginas simples para cargar y listar roles, usuarios, categorías y productos .
-auth (Node.js / Express + bcrypt): endpoints de registro e inicio de sesión .
-db (PostgreSQL 16): base de datos de la aplicación.
-tenants (PostgreSQL 16): base de datos separada para multi-tenencia (aislar clientes/tiendas).
-
-Herramientas
-
-Nginx Proxy Manager (NPM) : reverse proxy y certificados (Let’s Encrypt) para exponer servicios.
-CloudBeaver (DBeaver) : administración de PostgreSQL vía web.
-
-Redes & Volúmenes
-
-Redes independientes por entorno (suitecoffee_dev_net / suitecoffee_prod_net).
-Volúmenes gestionados por Compose para persistencia: suitecoffee-db, tenants-db, etc.
-
-Diagrama (alto nivel)
-@startuml
-skinparam componentStyle rectangle
-skinparam rectangle {
- BorderColor #555
- RoundCorner 10
-}
-actor Usuario
-
-package "Entorno DEV/PROD" {
- [app (Express)] as APP
- [auth (Express + bcrypt)] as AUTH
- database "db (PostgreSQL)" as DB
- database "tenants (PostgreSQL)" as TENANTS
- APP -down-> DB : Pool PG
- APP -down-> TENANTS : Pool PG
- AUTH -down-> DB : Pool PG (usuarios)
- Usuario --> APP : UI / API
- Usuario --> AUTH : Login/Registro
-}
-
-package "Herramientas" {
- [Nginx Proxy Manager] as NPM
- [CloudBeaver] as DBVR
- NPM ..> APP : proxy
- NPM ..> AUTH : proxy
- DBVR ..> DB : admin
- DBVR ..> TENANTS : admin
-}
-@enduml
-
-
-Características principales
-
-API REST para entidades clave (roles, usuarios, categorías y productos).
-Autenticación básica (registro y login) con hash de contraseñas (bcrypt).
-Multi‑tenencia con base tenants separada para aislar clientes/tiendas.
-Docker Compose v2 con entornos de desarrollo y producción .
-Herramientas integradas (NPM + CloudBeaver) en un compose.tools.yaml aparte.
-Scripts de backup/restauración de volúmenes y gestión de entornos .
-
-
-Requisitos
-
-Docker y Docker Compose v2 (recomendado).
-Python 3.9+ (para scripts suitecoffee.py, backups y utilidades).
-Node.js 20+ (sólo si vas a ejecutar servicios Node fuera de Docker).
-
-
-Inicio rápido
-Opción A — Gestor interactivo (recomendado)
-
-Clona el repo y entra al directorio:git clone https://gitea.mateosaldain.uy/msaldain/SuiteCoffee.git
-cd SuiteCoffee
-
-
-(Opcional) Crea/copía tus archivos .env para app y auth en ./services/<service>/.env.development (ver sección de variables).
-Ejecuta el gestor:python3 suitecoffee.py
-
-
-Verás un menú para levantar DESARROLLO o PRODUCCIÓN .
-Desde ahí también puedes levantar/apagar las herramientas NPM y CloudBeaver .
-
-
-Accede:
-
-App (dev): suele estar disponible via NPM o directamente dentro de la red, según tu configuración.
-Páginas simples: /roles, /usuarios, /categorias, /productos (servidas por app).
-Salud: /health en app y auth.
-
-
-
-
-Consejo: primero levanta desarrollo/producción y luego las herramientas para que existan las redes externas suitecoffee_dev_net/suitecoffee_prod_net que usa compose.tools.yaml.
-
-Opción B — Comandos Docker Compose (avanzado)
-
-
-Desarrollo :
-docker compose -f compose.yaml -f compose.dev.yaml --env-file ./services/app/.env.development --env-file ./services/auth/.env.development -p suitecoffee_dev up -d
-
-
-
-Producción :
-docker compose -f compose.yaml -f compose.prod.yaml --env-file ./services/app/.env.production --env-file ./services/auth/.env.production -p suitecoffee_prod up -d
-
-
-
-
-Los puertos se exponen para herramientas (NPM UI :81, CloudBeaver :8978); los servicios app y auth se exponen dentro de la red y se publican externamente a través de NPM.
-
-
-Variables de entorno
-Crea un archivo .env.development (y uno .env.production) en cada servicio (./services/app y ./services/auth). Variables comunes:
-# Servidor
-PORT=4000 # puerto HTTP del servicio
-NODE_ENV=development # development | production
-
-# Base de datos
-DB_HOST=db # nombre del servicio postgres (o host)
-DB_LOCAL_PORT=5432 # puerto de PG al que conectarse
-DB_USER=postgres
-DB_PASS=postgres
-DB_NAME=suitecoffee_db # para 'db' (aplicación)
-TENANTS_DB_NAME=tenants_db # si el servicio necesita apuntar a 'tenants'
-
-
-Ajusta DB_HOST a db o tenants según corresponda. En desarrollo, los alias útiles son dev-db y dev-tenants; en producción: prod-db y prod-tenants.
-
-
-Endpoints
-Servicio app (negocio)
-
-GET /health
-GET /api/roles — lista roles
-POST /api/roles — crea un rol
-GET /api/usuarios — lista usuarios
-POST /api/usuarios — crea un usuario
-GET /api/categorias — lista categorías
-POST /api/categorias — crea una categoría
-GET /api/productos — lista productos
-POST /api/productos — crea un producto
-Páginas estáticas simples para probar: /roles, /usuarios, /categorias, /productos
-
-Servicio auth (autenticación)
-
-GET /health
-POST /register — registro de usuario (password con bcrypt )
-POST /auth/login — inicio de sesión
-
-
-Nota : En esta etapa los endpoints son básicos y pensados para desarrollo/PoC. Ver la sección Sugerencias de mejora para próximos pasos (JWT, autorización, etc.).
-
-
-Estructura del proyecto
-SuiteCoffee/
-├─ services/
-│ ├─ app/
-│ │ ├─ src/
-│ │ │ ├─ index.js # API y páginas simples
-│ │ │ └─ pages/ # roles.html, usuarios.html, categorias.html, productos.html
-│ │ ├─ .env.development # variables (ejemplo)
-│ │ └─ .env.production
-│ └─ auth/
-│ ├─ src/
-│ │ └─ index.js # /register y /auth/login
-│ ├─ .env.development
-│ └─ .env.production
-├─ compose.yaml # base (db, tenants)
-├─ compose.dev.yaml # entorno desarrollo (app, auth, db, tenants)
-├─ compose.prod.yaml # entorno producción (app, auth, db, tenants)
-├─ compose.tools.yaml # herramientas (NPM, CloudBeaver) con redes externas
-├─ suitecoffee.py # gestor interactivo (Docker Compose)
-├─ backup_compose_volumes.py # backups de volúmenes Compose
-└─ restore_compose_volumes.py# restauración de volúmenes Compose
-
-
-Herramientas auxiliares (NPM y CloudBeaver)
-Los servicios de herramientas están separados para poder usarlos con ambos entornos (dev y prod) a la vez. Se levantan con compose.tools.yaml y se conectan a las redes externas suitecoffee_dev_net y suitecoffee_prod_net.
-
-Nginx Proxy Manager (NPM)
-Puertos: 80 (HTTP), 81 (UI). Volúmenes: npm_data, npm_letsencrypt.
-CloudBeaver
-Puerto: 8978. Volúmenes: dbeaver_logs, dbeaver_workspace.
-
-
-Si es la primera vez, arranca un entorno (dev/prod) para que Compose cree las redes; luego levanta las herramientas:
-docker compose -f compose.tools.yaml --profile npm -p suitecoffee up -d
-docker compose -f compose.tools.yaml --profile dbeaver -p suitecoffee up -d
-
-
-
-Backups y restauración de volúmenes
-Este repo incluye dos utilidades:
-
-backup_compose_volumes.py — detecta volúmenes de un proyecto de Compose (por labels y nombres) y los exporta a tar.gz usando un contenedor alpine temporal.
-restore_compose_volumes.py — permite restaurar esos tar.gz en volúmenes (útil para migraciones y pruebas).
-
-Ejemplos básicos
-
-python3 backup_compose_volumes.py --help
-
-
-python3 backup_compose_volumes.py --project suitecoffee_dev --output ./backups
-
-
-python3 restore_compose_volumes.py --archive ./backups/suitecoffee_dev_suitecoffee-db-YYYYmmddHHMMSS.tar.gz --volume suitecoffee_dev_suitecoffee-db
-
-
-Consejo: si migraste manualmente y ves advertencias tipo “volume ... already exists but was not created by Docker Compose”, considera marcar el volumen como external: true en el YAML o recrearlo para que Compose lo etiquete correctamente.
-
-
-Comandos útiles
-
-python3 suitecoffee.py
-
-
-python3 suitecoffee.py
-
-
-docker compose -f compose.tools.yaml --profile npm -p suitecoffee up -d
-docker compose -f compose.tools.yaml --profile dbeaver -p suitecoffee up -d
-
-
-docker compose -f compose.yaml -f compose.dev.yaml config --services
-docker compose -f compose.yaml -f compose.dev.yaml config --format json | jq .volumes
-
-
-Licencia
-
-ISC (ver package.json).
-
-
-Sugerencias de mejora
-
-Autenticación y seguridad
-
-Emitir JWT en el login y proteger rutas (roles/autorización por perfil).
-Configurar CORS por orígenes (en dev está abierto; en prod restringir).
-Añadir rate‑limit y helmet en Express.
-
-
-Esquema de datos y migraciones
-
-Añadir migraciones automatizadas (p.ej. Prisma , Knex , Sequelize o SQL versionado) y seeds iniciales.
-Clarificar el modelo multi‑tenant : por BD por tenant o schema por tenant ; documentar estrategia.
-
-
-Calidad & DX
-
-Tests (unitarios e integración) y CI básico.
-Validación de entrada (zod / joi ), manejo de errores consistente y logs estructurados.
-
-
-Docker/DevOps
-
-Documentar variables .env completas por servicio.
-Publicar imágenes de producción y usar IMAGE:TAG en compose.prod.yaml (evitar build en servidor).
-Añadir healthchecks a app/auth (ya hay ejemplos comentados).
-
-
-Frontend
-
-Reemplazar páginas HTML de prueba por un frontend (React/Vite) o una UI admin mínima.
-
-
-Pequeños fixes
-
-En los HTML de ejemplo corregir las referencias a campos id_rol, id_categoria, etc.
-Centralizar constantes (nombres de tablas/campos) y normalizar respuestas API.
-
-
-
-
-
-
-
diff --git a/compose.dev.yaml b/compose.dev.yaml
index 5129083..7dc5f1b 100644
--- a/compose.dev.yaml
+++ b/compose.dev.yaml
@@ -69,38 +69,38 @@ services:
#################
# --- Authentik db (solo interno)
authentik-db:
- image: postgres:16-alpine
+ # image: postgres:16-alpine
environment:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASS}
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U authentik -d authentik"]
- interval: 10s
- timeout: 3s
- retries: 10
+ # healthcheck:
+ # test: ["CMD-SHELL", "pg_isready -U authentik -d authentik"]
+ # interval: 10s
+ # timeout: 3s
+ # retries: 10
volumes:
- authentik-db:/var/lib/postgresql/data
- networks: {
- net: {
+ networks:
+ net:
aliases: [ak-db]
- }
- }
- restart: unless-stopped
+ # restart: unless-stopped
# --- Authentik Redis (solo interno)
authentik-redis:
- image: redis:7-alpine
+ # image: redis:7-alpine
command: ["redis-server", "--save", "", "--appendonly", "no"]
- networks: { net: { aliases: [ak-redis] } }
- restart: unless-stopped
+ networks:
+ net:
+ aliases: [ak-redis]
+ # restart: unless-stopped
# --- Authentik Server (sin puertos públicos)
authentik:
- image: ghcr.io/goauthentik/server:latest
- depends_on:
- authentik-db: { condition: service_healthy }
- authentik-redis: { condition: service_started }
+ # image: ghcr.io/goauthentik/server:latest
+ # depends_on:
+ # authentik-db: { condition: service_healthy }
+ # authentik-redis: { condition: service_started }
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_DEBUG: "false"
@@ -112,23 +112,22 @@ services:
# Opcional: bootstrap automático del admin
AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD}
AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL}
- expose:
- - "9000" # HTTP interno
- - "9443" # HTTPS interno
- networks: {
- net: {
+ # expose:
+ # - "9000" # HTTP interno
+ # - "9443" # HTTPS interno
+ networks:
+ net:
aliases: [authentik]
- }
- }
- restart: unless-stopped
+ # restart: unless-stopped
# Habilitá ESTO SOLO si querés abrir la UI local:
profiles: ["ak-ui"]
ports:
- - "127.0.0.1:9000:9000" # SOLO localhost
+ - 9000:9000
+ - 9443:9443
# --- Authentik Worker
authentik-worker:
- image: ghcr.io/goauthentik/server:latest
+ # image: ghcr.io/goauthentik/server:latest
command: worker
depends_on:
authentik-db: { condition: service_healthy }
@@ -140,13 +139,9 @@ services:
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASS}
AUTHENTIK_REDIS__HOST: authentik-redis
- profiles: ["ak-ui"]
- networks: {
- net: {
-
- }
- }
- restart: unless-stopped
+ networks:
+ net:
+ aliases: [ak-work]
volumes:
tenants-db:
diff --git a/compose.manso.yaml b/compose.manso.yaml
index bf12404..e5b0402 100644
--- a/compose.manso.yaml
+++ b/compose.manso.yaml
@@ -6,6 +6,11 @@ services:
manso:
image: node:20-bookworm
+ depends_on:
+ db:
+ condition: service_healthy
+ tenants:
+ condition: service_healthy
expose:
- ${APP_LOCAL_PORT}
working_dir: /app
@@ -20,7 +25,15 @@ services:
networks:
net:
aliases: [manso]
+ healthcheck:
+ test: ["CMD-SHELL", "curl -fsS http://localhost:${APP_DOCKER_PORT}/health || exit 1"]
+ interval: 10s
+ timeout: 3s
+ retries: 10
+ start_period: 20s
command: npm run dev
+ profiles: [manso]
+ restart: unless-stopped
db:
image: postgres:16
diff --git a/compose.yaml b/compose.yaml
index bfd0e00..800a201 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,10 +1,10 @@
# compose.yml
-# Comose base
+# Compose base
name: ${COMPOSE_PROJECT_NAME:-suitecoffee}
services:
- manso:
+ app:
depends_on:
db:
condition: service_healthy
@@ -17,19 +17,6 @@ services:
retries: 10
start_period: 20s
restart: unless-stopped
- # app:
- # depends_on:
- # db:
- # condition: service_healthy
- # tenants:
- # condition: service_healthy
- # healthcheck:
- # test: ["CMD-SHELL", "curl -fsS http://localhost:${APP_DOCKER_PORT}/health || exit 1"]
- # interval: 10s
- # timeout: 3s
- # retries: 10
- # start_period: 20s
- # restart: unless-stopped
auth:
depends_on:
@@ -65,4 +52,29 @@ services:
timeout: 3s
retries: 20
start_period: 10s
- restart: unless-stopped
\ No newline at end of file
+ restart: unless-stopped
+
+ authentik-db:
+ image: postgres:16-alpine
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U authentik -d authentik"]
+ interval: 10s
+ timeout: 3s
+ retries: 10
+ restart: unless-stopped
+
+ authentik-redis:
+ image: redis:7-alpine
+ restart: unless-stopped
+
+ authentik:
+ image: ghcr.io/goauthentik/server:latest
+ depends_on:
+ authentik-db: { condition: service_healthy }
+ authentik-redis: { condition: service_started }
+ restart: unless-stopped
+
+ authentik-worker:
+ image: ghcr.io/goauthentik/server:latest
+ restart: unless-stopped
+
\ No newline at end of file
diff --git a/services/app/package-lock.json b/services/app/package-lock.json
index 0a0679e..ca34343 100644
--- a/services/app/package-lock.json
+++ b/services/app/package-lock.json
@@ -12,10 +12,12 @@
"chalk": "^5.6.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
+ "ejs": "^3.1.10",
"express": "^5.1.0",
"express-ejs-layouts": "^2.5.1",
"pg": "^8.16.3",
- "pg-format": "^1.0.4"
+ "pg-format": "^1.0.4",
+ "serve-favicon": "^2.5.1"
},
"devDependencies": {
"cross-env": "^10.0.0",
@@ -24,15 +26,11 @@
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
- "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"dev": true,
"license": "MIT"
},
"node_modules/accepts": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
@@ -44,8 +42,6 @@
},
"node_modules/anymatch": {
"version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -56,17 +52,16 @@
"node": ">= 8"
}
},
+ "node_modules/async": {
+ "version": "3.2.6",
+ "license": "MIT"
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true,
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
- "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -78,8 +73,6 @@
},
"node_modules/body-parser": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
- "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
@@ -98,8 +91,6 @@
},
"node_modules/brace-expansion": {
"version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -109,8 +100,6 @@
},
"node_modules/braces": {
"version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -122,8 +111,6 @@
},
"node_modules/bytes": {
"version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -131,8 +118,6 @@
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -144,8 +129,6 @@
},
"node_modules/call-bound": {
"version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -160,8 +143,6 @@
},
"node_modules/chalk": {
"version": "5.6.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz",
- "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -172,8 +153,6 @@
},
"node_modules/chokidar": {
"version": "3.6.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
- "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -197,15 +176,11 @@
},
"node_modules/concat-map": {
"version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
- "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
@@ -216,8 +191,6 @@
},
"node_modules/content-type": {
"version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -225,8 +198,6 @@
},
"node_modules/cookie": {
"version": "0.7.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
- "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -234,8 +205,6 @@
},
"node_modules/cookie-signature": {
"version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
- "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
@@ -243,8 +212,6 @@
},
"node_modules/cors": {
"version": "2.8.5",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
- "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
@@ -256,8 +223,6 @@
},
"node_modules/cross-env": {
"version": "10.0.0",
- "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz",
- "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -274,8 +239,6 @@
},
"node_modules/cross-spawn": {
"version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -289,8 +252,6 @@
},
"node_modules/debug": {
"version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -306,8 +267,6 @@
},
"node_modules/depd": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -315,8 +274,6 @@
},
"node_modules/dotenv": {
"version": "17.2.1",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
- "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -327,8 +284,6 @@
},
"node_modules/dunder-proto": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -341,14 +296,23 @@
},
"node_modules/ee-first": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/encodeurl": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -356,8 +320,6 @@
},
"node_modules/es-define-property": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -365,8 +327,6 @@
},
"node_modules/es-errors": {
"version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -374,8 +334,6 @@
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -386,14 +344,10 @@
},
"node_modules/escape-html": {
"version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -401,8 +355,6 @@
},
"node_modules/express": {
"version": "5.1.0",
- "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
- "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
@@ -442,14 +394,34 @@
}
},
"node_modules/express-ejs-layouts": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz",
- "integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA=="
+ "version": "2.5.1"
+ },
+ "node_modules/filelist": {
+ "version": "1.0.4",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.6",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
},
"node_modules/fill-range": {
"version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -461,8 +433,6 @@
},
"node_modules/finalhandler": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
- "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
@@ -478,8 +448,6 @@
},
"node_modules/forwarded": {
"version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -487,32 +455,13 @@
},
"node_modules/fresh": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
- "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
"node_modules/function-bind": {
"version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -520,8 +469,6 @@
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -544,8 +491,6 @@
},
"node_modules/get-proto": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -557,8 +502,6 @@
},
"node_modules/glob-parent": {
"version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -570,8 +513,6 @@
},
"node_modules/gopd": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -582,8 +523,6 @@
},
"node_modules/has-flag": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -592,8 +531,6 @@
},
"node_modules/has-symbols": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -604,8 +541,6 @@
},
"node_modules/hasown": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -616,8 +551,6 @@
},
"node_modules/http-errors": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
- "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
@@ -632,8 +565,6 @@
},
"node_modules/http-errors/node_modules/statuses": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
- "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -641,8 +572,6 @@
},
"node_modules/iconv-lite": {
"version": "0.6.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
- "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -653,21 +582,15 @@
},
"node_modules/ignore-by-default": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
- "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true,
"license": "ISC"
},
"node_modules/inherits": {
"version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
@@ -675,8 +598,6 @@
},
"node_modules/is-binary-path": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -688,8 +609,6 @@
},
"node_modules/is-extglob": {
"version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -698,8 +617,6 @@
},
"node_modules/is-glob": {
"version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -711,8 +628,6 @@
},
"node_modules/is-number": {
"version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
@@ -721,21 +636,30 @@
},
"node_modules/is-promise": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
- "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
+ "node_modules/jake": {
+ "version": "10.9.4",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.6",
+ "filelist": "^1.0.4",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -743,8 +667,6 @@
},
"node_modules/media-typer": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
- "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -752,8 +674,6 @@
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
- "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -764,8 +684,6 @@
},
"node_modules/mime-db": {
"version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -773,8 +691,6 @@
},
"node_modules/mime-types": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
- "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
@@ -785,8 +701,6 @@
},
"node_modules/minimatch": {
"version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -798,14 +712,10 @@
},
"node_modules/ms": {
"version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
- "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -813,8 +723,6 @@
},
"node_modules/nodemon": {
"version": "3.1.10",
- "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
- "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -842,8 +750,6 @@
},
"node_modules/normalize-path": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -852,8 +758,6 @@
},
"node_modules/object-assign": {
"version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -861,8 +765,6 @@
},
"node_modules/object-inspect": {
"version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -873,8 +775,6 @@
},
"node_modules/on-finished": {
"version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
@@ -885,8 +785,6 @@
},
"node_modules/once": {
"version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -894,8 +792,6 @@
},
"node_modules/parseurl": {
"version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -903,8 +799,6 @@
},
"node_modules/path-key": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -913,8 +807,6 @@
},
"node_modules/path-to-regexp": {
"version": "8.2.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
- "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
"license": "MIT",
"engines": {
"node": ">=16"
@@ -922,8 +814,6 @@
},
"node_modules/pg": {
"version": "8.16.3",
- "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
- "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
@@ -949,21 +839,15 @@
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
- "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
- "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
- "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
- "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/pg-format": {
"version": "1.0.4",
- "resolved": "https://registry.npmjs.org/pg-format/-/pg-format-1.0.4.tgz",
- "integrity": "sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==",
"license": "MIT",
"engines": {
"node": ">=4.0"
@@ -971,8 +855,6 @@
},
"node_modules/pg-int8": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
- "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
@@ -980,8 +862,6 @@
},
"node_modules/pg-pool": {
"version": "3.10.1",
- "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
- "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
@@ -989,14 +869,10 @@
},
"node_modules/pg-protocol": {
"version": "1.10.3",
- "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
- "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
- "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
@@ -1011,17 +887,17 @@
},
"node_modules/pgpass": {
"version": "1.0.5",
- "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
- "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "license": "ISC"
+ },
"node_modules/picomatch": {
"version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1033,8 +909,6 @@
},
"node_modules/postgres-array": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
- "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
@@ -1042,8 +916,6 @@
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
- "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -1051,8 +923,6 @@
},
"node_modules/postgres-date": {
"version": "1.0.7",
- "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
- "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -1060,8 +930,6 @@
},
"node_modules/postgres-interval": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
- "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
@@ -1072,8 +940,6 @@
},
"node_modules/proxy-addr": {
"version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
@@ -1085,15 +951,11 @@
},
"node_modules/pstree.remy": {
"version": "1.1.8",
- "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
- "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true,
"license": "MIT"
},
"node_modules/qs": {
"version": "6.14.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
- "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -1107,8 +969,6 @@
},
"node_modules/range-parser": {
"version": "1.2.1",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
- "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -1116,8 +976,6 @@
},
"node_modules/raw-body": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
- "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
@@ -1131,8 +989,6 @@
},
"node_modules/readdirp": {
"version": "3.6.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
- "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1144,8 +1000,6 @@
},
"node_modules/router": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
- "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
@@ -1160,8 +1014,6 @@
},
"node_modules/safe-buffer": {
"version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
@@ -1180,14 +1032,10 @@
},
"node_modules/safer-buffer": {
"version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -1199,8 +1047,6 @@
},
"node_modules/send": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
- "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
@@ -1219,10 +1065,29 @@
"node": ">= 18"
}
},
+ "node_modules/serve-favicon": {
+ "version": "2.5.1",
+ "license": "MIT",
+ "dependencies": {
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "ms": "~2.1.3",
+ "parseurl": "~1.3.2",
+ "safe-buffer": "~5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serve-favicon/node_modules/fresh": {
+ "version": "0.5.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/serve-static": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
- "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
@@ -1236,14 +1101,10 @@
},
"node_modules/setprototypeof": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1255,8 +1116,6 @@
},
"node_modules/shebang-regex": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1265,8 +1124,6 @@
},
"node_modules/side-channel": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
- "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -1284,8 +1141,6 @@
},
"node_modules/side-channel-list": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -1300,8 +1155,6 @@
},
"node_modules/side-channel-map": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -1318,8 +1171,6 @@
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -1337,8 +1188,6 @@
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
- "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1350,8 +1199,6 @@
},
"node_modules/split2": {
"version": "4.2.0",
- "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
- "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
@@ -1359,8 +1206,6 @@
},
"node_modules/statuses": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -1368,8 +1213,6 @@
},
"node_modules/supports-color": {
"version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1381,8 +1224,6 @@
},
"node_modules/to-regex-range": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1394,8 +1235,6 @@
},
"node_modules/toidentifier": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
@@ -1403,8 +1242,6 @@
},
"node_modules/touch": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
- "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -1413,8 +1250,6 @@
},
"node_modules/type-is": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
- "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
@@ -1427,15 +1262,11 @@
},
"node_modules/undefsafe": {
"version": "2.0.5",
- "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
- "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true,
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -1443,8 +1274,6 @@
},
"node_modules/vary": {
"version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -1452,8 +1281,6 @@
},
"node_modules/which": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -1468,14 +1295,10 @@
},
"node_modules/wrappy": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xtend": {
"version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
diff --git a/services/app/package.json b/services/app/package.json
index 4a110e7..e1c4854 100644
--- a/services/app/package.json
+++ b/services/app/package.json
@@ -18,10 +18,12 @@
"chalk": "^5.6.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
+ "ejs": "^3.1.10",
"express": "^5.1.0",
"express-ejs-layouts": "^2.5.1",
"pg": "^8.16.3",
- "pg-format": "^1.0.4"
+ "pg-format": "^1.0.4",
+ "serve-favicon": "^2.5.1"
},
"keywords": [],
"description": ""
diff --git a/services/app/src/index.js b/services/app/src/index.js
index 14ecb35..b093dee 100644
--- a/services/app/src/index.js
+++ b/services/app/src/index.js
@@ -1,5 +1,6 @@
// app/src/index.js
import chalk from 'chalk'; // Colores!
+import favicon from 'serve-favicon'; // Favicon
import express from 'express';
import expressLayouts from 'express-ejs-layouts';
import cors from 'cors';
@@ -12,239 +13,830 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Variables de Entorno
-import dotenv, { config } from 'dotenv';
+import dotenv from 'dotenv';
-// Obtención de la ruta de la variable de entorno correspondiente a NODE_ENV
-try {
- if (process.env.NODE_ENV === 'development') {
- dotenv.config({ path: path.resolve(__dirname, '../.env.development' )});
- console.log(`Activando entorno de ->${chalk.green(` DEVELOPMENT `)}`);
- } else if (process.env.NODE_ENV === 'stage') {
- dotenv.config({ path: path.resolve(__dirname, '../.env.test' )});
- console.log(`Activando entorno de ->${chalk.yellow(` TESTING `)}`);
- } else if (process.env.NODE_ENV === 'production') {
- dotenv.config({ path: path.resolve(__dirname, '../.env.production' )});
- console.log(`Activando entorno de ->${chalk.red(` PRODUCTION `)}`);
- }
-} catch (error) {
- console.log("A ocurrido un error al seleccionar el entorno. \nError: " + error);
+// Cargar .env según entorno
+if (process.env.NODE_ENV === 'development') {
+ dotenv.config({ path: path.resolve(__dirname, '../.env.development') });
+} else if (process.env.NODE_ENV === 'test') {
+ dotenv.config({ path: path.resolve(__dirname, '../.env.test') });
+} else if (process.env.NODE_ENV === 'production') {
+ dotenv.config({ path: path.resolve(__dirname, '../.env.production') });
+} else {
+ dotenv.config(); // .env por defecto
}
-// Renderiado
+// ----------------------------------------------------------
+// App
+// ----------------------------------------------------------
const app = express();
+app.set('trust proxy', true);
app.use(cors());
app.use(express.json());
+app.use(express.json({ limit: '1mb' }));
app.use(express.static(path.join(__dirname, 'pages')));
-// Configuración de conexión PostgreSQL
+// ----------------------------------------------------------
+// Motor de vistas EJS
+// ----------------------------------------------------------
+app.set("views", path.join(__dirname, "views"));
+app.set("view engine", "ejs");
+app.use(expressLayouts);
+app.set("layout", "layouts/main");
+
+// Archivos estáticos
+app.use(express.static(path.join(__dirname, "public")));
+
+app.use('/favicon', express.static(path.join(__dirname, 'public', 'favicon'), {
+ maxAge: '1y'
+}));
+
+app.use(favicon(path.join(__dirname, 'public', 'favicon', 'favicon.ico'), {
+ maxAge: '1y'
+}));
+
+const url = v => !v ? "" : (v.startsWith("http") ? v : `/img/productos/${v}`);
+
+// ----------------------------------------------------------
+// Configuración de conexión PostgreSQL
+// ----------------------------------------------------------
const dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
- port: process.env.DB_LOCAL_PORT
+ port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined,
+ ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined,
};
const pool = new Pool(dbConfig);
+// ----------------------------------------------------------
+// Seguridad: Tablas permitidas
+// ----------------------------------------------------------
+const ALLOWED_TABLES = [
+ 'roles','usuarios','usua_roles',
+ 'categorias','productos',
+ 'clientes','mesas',
+ 'comandas','deta_comandas',
+ 'proveedores','compras','deta_comp_producto',
+ 'mate_primas','deta_comp_materias',
+ 'prov_producto','prov_mate_prima',
+ 'receta_producto', 'asistencia_resumen_diario',
+ 'asistencia_intervalo', 'vw_compras'
+];
+const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
+
+// Identificadores SQL -> comillas dobles y escape correcto
+const q = (s) => `"${String(s).replace(/"/g, '""')}"`;
+
+function ensureTable(name) {
+ const t = String(name || '').toLowerCase();
+ if (!ALLOWED_TABLES.includes(t)) throw new Error('Tabla no permitida');
+ return t;
+}
+
+async function getClient() {
+ const client = await pool.connect();
+ return client;
+}
+
+// ----------------------------------------------------------
+// Introspección de esquema
+// ----------------------------------------------------------
+async function loadColumns(client, table) {
+ const sql = `
+ SELECT
+ c.column_name,
+ c.data_type,
+ c.is_nullable = 'YES' AS is_nullable,
+ c.column_default,
+ (SELECT EXISTS (
+ SELECT 1 FROM pg_attribute a
+ JOIN pg_class t ON t.oid = a.attrelid
+ JOIN pg_index i ON i.indrelid = t.oid AND a.attnum = ANY(i.indkey)
+ WHERE t.relname = $1 AND i.indisprimary AND a.attname = c.column_name
+ )) AS is_primary,
+ (SELECT a.attgenerated = 's' OR a.attidentity IN ('a','d')
+ FROM pg_attribute a
+ JOIN pg_class t ON t.oid = a.attrelid
+ WHERE t.relname = $1 AND a.attname = c.column_name
+ ) AS is_identity
+ FROM information_schema.columns c
+ WHERE c.table_schema='public' AND c.table_name=$1
+ ORDER BY c.ordinal_position
+ `;
+ const { rows } = await client.query(sql, [table]);
+ return rows;
+}
+
+async function loadForeignKeys(client, table) {
+ const sql = `
+ SELECT
+ kcu.column_name,
+ ccu.table_name AS foreign_table,
+ ccu.column_name AS foreign_column
+ FROM information_schema.table_constraints tc
+ JOIN information_schema.key_column_usage kcu
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
+ JOIN information_schema.constraint_column_usage ccu
+ ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
+ WHERE tc.table_schema='public' AND tc.table_name=$1 AND tc.constraint_type='FOREIGN KEY'
+ `;
+ const { rows } = await client.query(sql, [table]);
+ const map = {};
+ for (const r of rows) map[r.column_name] = { foreign_table: r.foreign_table, foreign_column: r.foreign_column };
+ return map;
+}
+
+async function loadPrimaryKey(client, table) {
+ const sql = `
+ SELECT a.attname AS column_name
+ FROM pg_index i
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
+ JOIN pg_class t ON t.oid = i.indrelid
+ WHERE t.relname = $1 AND i.indisprimary
+ `;
+ const { rows } = await client.query(sql, [table]);
+ return rows.map(r => r.column_name);
+}
+
+// label column for FK options
+async function pickLabelColumn(client, refTable) {
+ const preferred = ['nombre','raz_social','apodo','documento','correo','telefono'];
+ const { rows } = await client.query(
+ `SELECT column_name, data_type
+ FROM information_schema.columns
+ WHERE table_schema='public' AND table_name=$1
+ ORDER BY ordinal_position`, [refTable]
+ );
+ for (const cand of preferred) {
+ if (rows.find(r => r.column_name === cand)) return cand;
+ }
+ const textish = rows.find(r => /text|character varying|varchar/i.test(r.data_type));
+ if (textish) return textish.column_name;
+ return rows[0]?.column_name || 'id';
+}
+
+// ----------------------------------------------------------
+// Middleware para datos globales
+// ----------------------------------------------------------
+app.use((req, res, next) => {
+ res.locals.currentPath = req.path;
+ res.locals.pageTitle = "SuiteCoffee";
+ res.locals.pageId = "";
+ next();
+});
+
+
+// ----------------------------------------------------------
+// Rutas de UI
+// ----------------------------------------------------------
+
+app.get("/", (req, res) => {
+ res.locals.pageTitle = "Dashboard";
+ res.locals.pageId = "home"; // para el sidebar contextual
+ res.render("dashboard");
+});
+
+app.get("/dashboard", (req, res) => {
+ res.locals.pageTitle = "Dashboard";
+ res.locals.pageId = "dashboard"; // <- importante
+ res.render("dashboard");
+});
+
+// app.get('/', (req, res) => {
+// res.sendFile(path.join(__dirname, 'pages', 'dashboard.html'));
+// });
+
+app.get("/comandas", (req, res) => {
+ res.locals.pageTitle = "Comandas";
+ res.locals.pageId = "comandas"; // <- importante para el sidebar contextual
+ res.render("comandas");
+});
+
+// app.get('/comandas', (req, res) => {
+// res.sendFile(path.join(__dirname, 'pages', 'comandas.html'));
+// });
+
+app.get("/estadoComandas", (req, res) => {
+ res.locals.pageTitle = "Estado de Comandas";
+ res.locals.pageId = "estadoComandas";
+ res.render("estadoComandas");
+});
+
+// app.get('/estadoComandas', (req, res) => {
+// res.sendFile(path.join(__dirname, 'pages', 'estadoComandas.html'));
+// });
+
+app.get("/productos", (req, res) => {
+ res.locals.pageTitle = "Productos";
+ res.locals.pageId = "productos";
+ res.render("productos");
+});
+
+app.get('/usuarios', (req, res) => {
+ res.locals.pageTitle = 'Usuarios';
+ res.locals.pageId = 'usuarios';
+ res.render('usuarios');
+});
+
+app.get('/reportes', (req, res) => {
+ res.locals.pageTitle = 'Reportes';
+ res.locals.pageId = 'reportes';
+ res.render('reportes');
+});
+
+app.get('/compras', (req, res) => {
+ res.locals.pageTitle = 'Compras';
+ res.locals.pageId = 'compras';
+ res.render('compras');
+});
+
+// ----------------------------------------------------------
+// API
+// ----------------------------------------------------------
+app.get('/api/tables', async (_req, res) => {
+ res.json(ALLOWED_TABLES);
+});
+
+app.get('/api/schema/:table', async (req, res) => {
+ try {
+ const table = ensureTable(req.params.table);
+ const client = await getClient();
+ try {
+ const columns = await loadColumns(client, table);
+ const fks = await loadForeignKeys(client, table);
+ const enriched = columns.map(c => ({ ...c, foreign: fks[c.column_name] || null }));
+ res.json({ table, columns: enriched });
+ } finally { client.release(); }
+ } catch (e) {
+ res.status(400).json({ error: e.message });
+ }
+});
+
+app.get('/api/options/:table/:column', async (req, res) => {
+ try {
+ const table = ensureTable(req.params.table);
+ const column = req.params.column;
+ if (!VALID_IDENT.test(column)) throw new Error('Columna inválida');
+
+ const client = await getClient();
+ try {
+ const fks = await loadForeignKeys(client, table);
+ const fk = fks[column];
+ if (!fk) return res.json([]);
+
+ const refTable = fk.foreign_table;
+ const refId = fk.foreign_column;
+ const labelCol = await pickLabelColumn(client, refTable);
+
+ const sql = `SELECT ${q(refId)} AS id, ${q(labelCol)} AS label FROM ${q(refTable)} ORDER BY ${q(labelCol)} LIMIT 1000`;
+ const result = await client.query(sql);
+ res.json(result.rows);
+ } finally { client.release(); }
+ } catch (e) {
+ res.status(400).json({ error: e.message });
+ }
+});
+
+app.get('/api/table/:table', async (req, res) => {
+ try {
+ const table = ensureTable(req.params.table);
+ const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000);
+ const client = await getClient();
+ try {
+ const pks = await loadPrimaryKey(client, table);
+ const orderBy = pks.length ? `ORDER BY ${pks.map(q).join(', ')} DESC` : '';
+ const sql = `SELECT * FROM ${q(table)} ${orderBy} LIMIT ${limit}`;
+ const result = await client.query(sql);
+
+ // Normalizar: siempre devolver objetos {col: valor}
+ const colNames = result.fields.map(f => f.name);
+ let rows = result.rows;
+ if (rows.length && Array.isArray(rows[0])) {
+ rows = rows.map(r => Object.fromEntries(r.map((v, i) => [colNames[i], v])));
+ }
+ res.json(rows);
+ } finally { client.release(); }
+ } catch (e) {
+ res.status(400).json({ error: e.message, code: e.code, detail: e.detail });
+ }
+});
+
+app.post('/api/table/:table', async (req, res) => {
+ const table = ensureTable(req.params.table);
+ const payload = req.body || {};
+ try {
+ const client = await getClient();
+ try {
+ const columns = await loadColumns(client, table);
+ const insertable = columns.filter(c =>
+ !c.is_primary && !c.is_identity && !(c.column_default || '').startsWith('nextval(')
+ );
+ const allowedCols = new Set(insertable.map(c => c.column_name));
+
+ const cols = [];
+ const vals = [];
+ const params = [];
+ let idx = 1;
+ for (const [k, v] of Object.entries(payload)) {
+ if (!allowedCols.has(k)) continue;
+ if (!VALID_IDENT.test(k)) continue;
+ cols.push(q(k));
+ vals.push(`$${idx++}`);
+ params.push(v);
+ }
+
+ if (!cols.length) {
+ const { rows } = await client.query(`INSERT INTO ${q(table)} DEFAULT VALUES RETURNING *`);
+ res.status(201).json({ inserted: rows[0] });
+ } else {
+ const { rows } = await client.query(
+ `INSERT INTO ${q(table)} (${cols.join(', ')}) VALUES (${vals.join(', ')}) RETURNING *`,
+ params
+ );
+ res.status(201).json({ inserted: rows[0] });
+ }
+ } catch (e) {
+ if (e.code === '23503') return res.status(400).json({ error: 'Violación de clave foránea', detail: e.detail });
+ if (e.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail });
+ if (e.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail });
+ if (e.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail });
+ throw e;
+ }
+ } catch (e) {
+ res.status(400).json({ error: e.message });
+ }
+});
+
+app.get('/api/comandas', async (req, res, next) => {
+ try {
+ const estado = (req.query.estado || '').trim() || null;
+ const limit = Math.min(parseInt(req.query.limit || '200', 10), 1000);
+
+ const { rows } = await pool.query(
+ `SELECT * FROM public.f_comandas_resumen($1, $2)`,
+ [estado, limit]
+ );
+ res.json(rows);
+ } catch (e) { next(e); }
+});
+
+
+// app.get('/api/comandas', async (req, res, next) => {
+// try {
+// const estado = (req.query.estado || '').trim();
+// const limit = Math.min(parseInt(req.query.limit || '200', 10), 1000);
+// const params = [];
+// let where = '';
+// if (estado) { params.push(estado); where = `WHERE c.estado = $${params.length}`; }
+// params.push(limit);
+
+// const sql = `
+// WITH items AS (
+// SELECT d.id_comanda,
+// COUNT(*) AS items,
+// SUM(d.cantidad * d.pre_unitario) AS total
+// FROM deta_comandas d
+// GROUP BY d.id_comanda
+// )
+// SELECT
+// c.id_comanda, c.fec_creacion, c.estado, c.observaciones,
+// u.id_usuario, u.nombre AS usuario_nombre, u.apellido AS usuario_apellido,
+// m.id_mesa, m.numero AS mesa_numero, m.apodo AS mesa_apodo,
+// COALESCE(i.items, 0) AS items,
+// COALESCE(i.total, 0) AS total
+// FROM comandas c
+// JOIN usuarios u ON u.id_usuario = c.id_usuario
+// JOIN mesas m ON m.id_mesa = c.id_mesa
+// LEFT JOIN items i ON i.id_comanda = c.id_comanda
+// ${where}
+// ORDER BY c.id_comanda DESC
+// LIMIT $${params.length}
+// `;
+// const client = await pool.connect();
+// try {
+// const { rows } = await client.query(sql, params);
+// res.json(rows);
+// } finally { client.release(); }
+// } catch (e) { next(e); }
+// });
+
+
+// Detalle de una comanda (con nombres de productos)
+
+// GET /api/comandas/:id/detalle
+app.get('/api/comandas/:id/detalle', (req, res, next) =>
+ pool.query(
+ `SELECT id_det_comanda, id_producto, producto_nombre,
+ cantidad, pre_unitario, subtotal, observaciones
+ FROM public.v_comandas_detalle_items
+ WHERE id_comanda = $1::int
+ ORDER BY id_det_comanda`,
+ [req.params.id]
+ )
+ .then(r => res.json(r.rows))
+ .catch(next)
+);
+
+
+// app.get('/api/comandas/:id/detalle', async (req, res, next) => {
+// try {
+// const id = parseInt(req.params.id, 10);
+// if (!Number.isInteger(id) || id <= 0) {
+// return res.status(400).json({ error: 'id inválido' });
+// }
+
+// const sql = `
+// SELECT
+// id_det_comanda, id_producto, producto_nombre,
+// cantidad, pre_unitario, subtotal, observaciones
+// FROM public.v_comandas_detalle_items
+// WHERE id_comanda = $1::int
+// ORDER BY id_det_comanda
+// `;
+// const { rows } = await pool.query(sql, [id]);
+// res.json(rows);
+// } catch (e) { next(e); }
+// });
+
+
+// app.get('/api/comandas/:id/detalle', async (req, res, next) => {
+// try {
+// const id = parseInt(req.params.id, 10);
+// if (!id) return res.status(400).json({ error: 'id inválido' });
+
+// const sql = `
+// SELECT d.id_det_comanda, d.id_producto, p.nombre AS producto_nombre,
+// d.cantidad, d.pre_unitario, (d.cantidad * d.pre_unitario) AS subtotal,
+// d.observaciones
+// FROM deta_comandas d
+// JOIN productos p ON p.id_producto = d.id_producto
+// WHERE d.id_comanda = $1
+// ORDER BY d.id_det_comanda
+// `;
+// const { rows } = await pool.query(sql, [id]);
+// res.json(rows);
+// } catch (e) { next(e); }
+// });
+
+
+
+// Cerrar comanda (setea estado y fec_cierre en DB)
+app.post('/api/comandas/:id/cerrar', async (req, res, next) => {
+ try {
+ const id = Number(req.params.id);
+ if (!Number.isInteger(id) || id <= 0) {
+ return res.status(400).json({ error: 'id inválido' });
+ }
+ const { rows } = await pool.query(
+ `SELECT public.f_cerrar_comanda($1) AS data`,
+ [id]
+ );
+ if (!rows.length || rows[0].data === null) {
+ return res.status(404).json({ error: 'Comanda no encontrada' });
+ }
+ res.json(rows[0].data);
+ } catch (err) { next(err); }
+});
+
+// Abrir (reabrir) comanda
+app.post('/api/comandas/:id/abrir', async (req, res, next) => {
+ try {
+ const id = Number(req.params.id);
+ if (!Number.isInteger(id) || id <= 0) {
+ return res.status(400).json({ error: 'id inválido' });
+ }
+ const { rows } = await pool.query(
+ `SELECT public.f_abrir_comanda($1) AS data`,
+ [id]
+ );
+ if (!rows.length || rows[0].data === null) {
+ return res.status(404).json({ error: 'Comanda no encontrada' });
+ }
+ res.json(rows[0].data);
+ } catch (err) { next(err); }
+});
+
+// // Cambiar estado (abrir/cerrar)
+// app.post('/api/comandas/:id/estado', async (req, res, next) => {
+// try {
+// const id = parseInt(req.params.id, 10);
+// let { estado } = req.body || {};
+// if (!id) return res.status(400).json({ error: 'id inválido' });
+
+// const allowed = new Set(['abierta','cerrada','pagada','anulada']);
+// if (!allowed.has(estado)) return res.status(400).json({ error: 'estado inválido' });
+
+// const { rows } = await pool.query(
+// `UPDATE comandas SET estado = $2 WHERE id_comanda = $1 RETURNING *`,
+// [id, estado]
+// );
+// if (!rows.length) return res.status(404).json({ error: 'comanda no encontrada' });
+// res.json({ updated: rows[0] });
+// } catch (e) { next(e); }
+// });
+
+// GET producto + receta
+app.get('/api/rpc/get_producto/:id', async (req, res) => {
+ const id = Number(req.params.id);
+ const { rows } = await pool.query('SELECT public.get_producto($1) AS data', [id]);
+ res.json(rows[0]?.data || {});
+});
+
+// POST guardar producto + receta
+
+app.post('/api/rpc/save_producto', async (req, res) => {
+ try {
+ // console.debug('receta payload:', req.body?.receta); // habilitalo si lo necesitás
+ const q = 'SELECT public.save_producto($1,$2,$3,$4,$5,$6,$7::jsonb) AS id_producto';
+ const { id_producto=null, nombre, img_producto=null, precio=0, activo=true, id_categoria=null, receta=[] } = req.body || {};
+ const params = [id_producto, nombre, img_producto, precio, activo, id_categoria, JSON.stringify(receta||[])];
+ const { rows } = await pool.query(q, params);
+ res.json(rows[0] || {});
+ } catch(e) {
+ console.error(e);
+ res.status(500).json({ error: 'save_producto failed' });
+ }
+});
+
+
+// app.post('/api/rpc/save_producto', async (req, res) => {
+// const { id_producto=null, nombre, img_producto=null, precio=0, activo=true, id_categoria=null, receta=[] } = req.body || {};
+// const q = 'SELECT * FROM public.save_producto($1,$2,$3,$4,$5,$6,$7::jsonb)';
+// const params = [id_producto, nombre, img_producto, precio, activo, id_categoria, JSON.stringify(receta||[])];
+// const { rows } = await pool.query(q, params);
+// res.json(rows[0] || {});
+// });
+
+// GET MP + proveedores
+app.get('/api/rpc/get_materia/:id', async (req, res) => {
+ const id = Number(req.params.id);
+ try {
+ const { rows } = await pool.query('SELECT public.get_materia_prima($1) AS data', [id]);
+ res.json(rows[0]?.data || {});
+ } catch (e) {
+ console.error(e);
+ res.status(500).json({ error: 'get_materia failed' });
+ }
+});
+
+// SAVE MP + proveedores (array)
+app.post('/api/rpc/save_materia', async (req, res) => {
+ const { id_mat_prima=null, nombre, unidad, activo=true, proveedores=[] } = req.body || {};
+ try {
+ const q = 'SELECT public.save_materia_prima($1,$2,$3,$4,$5::jsonb) AS id_mat_prima';
+ const params = [id_mat_prima, nombre, unidad, activo, JSON.stringify(proveedores||[])];
+ const { rows } = await pool.query(q, params);
+ res.json(rows[0] || {});
+ } catch (e) {
+ console.error(e);
+ res.status(500).json({ error: 'save_materia failed' });
+ }
+});
+
+// POST /api/rpc/find_usuarios_por_documentos { docs: ["12345678","09123456", ...] }
+app.post('/api/rpc/find_usuarios_por_documentos', async (req, res) => {
+ try {
+ const docs = Array.isArray(req.body?.docs) ? req.body.docs : [];
+ const sql = 'SELECT public.find_usuarios_por_documentos($1::jsonb) AS data';
+ const { rows } = await pool.query(sql, [JSON.stringify(docs)]);
+ res.json(rows[0]?.data || {});
+ } catch (e) {
+ console.error(e);
+ res.status(500).json({ error: 'find_usuarios_por_documentos failed' });
+ }
+});
+
+// POST /api/rpc/import_asistencia { registros: [...], origen?: "AGL_001.txt" }
+app.post('/api/rpc/import_asistencia', async (req, res) => {
+ try {
+ const registros = Array.isArray(req.body?.registros) ? req.body.registros : [];
+ const origen = req.body?.origen || null;
+ const sql = 'SELECT public.import_asistencia($1::jsonb,$2) AS data';
+ const { rows } = await pool.query(sql, [JSON.stringify(registros), origen]);
+ res.json(rows[0]?.data || {});
+ } catch (e) {
+ console.error(e);
+ res.status(500).json({ error: 'import_asistencia failed' });
+ }
+});
+
+// Consultar datos de asistencia (raw + pares) para un usuario y rango
+app.post('/api/rpc/asistencia_get', async (req, res) => {
+ try {
+ const { doc, desde, hasta } = req.body || {};
+ const sql = 'SELECT public.asistencia_get($1::text,$2::date,$3::date) AS data';
+ const { rows } = await pool.query(sql, [doc, desde, hasta]);
+ res.json(rows[0]?.data || {});
+ } catch (e) {
+ console.error(e); res.status(500).json({ error: 'asistencia_get failed' });
+ }
+});
+
+// Editar un registro crudo y recalcular pares
+app.post('/api/rpc/asistencia_update_raw', async (req, res) => {
+ try {
+ const { id_raw, fecha, hora, modo } = req.body || {};
+ const sql = 'SELECT public.asistencia_update_raw($1::bigint,$2::date,$3::text,$4::text) AS data';
+ const { rows } = await pool.query(sql, [id_raw, fecha, hora, modo ?? null]);
+ res.json(rows[0]?.data || {});
+ } catch (e) {
+ console.error(e); res.status(500).json({ error: 'asistencia_update_raw failed' });
+ }
+});
+
+// Eliminar un registro crudo y recalcular pares
+app.post('/api/rpc/asistencia_delete_raw', async (req, res) => {
+ try {
+ const { id_raw } = req.body || {};
+ const sql = 'SELECT public.asistencia_delete_raw($1::bigint) AS data';
+ const { rows } = await pool.query(sql, [id_raw]);
+ res.json(rows[0]?.data || {});
+ } catch (e) {
+ console.error(e); res.status(500).json({ error: 'asistencia_delete_raw failed' });
+ }
+});
+
+// POST /api/rpc/report_tickets { year }
+app.post('/api/rpc/report_tickets', async (req, res) => {
+ try {
+ const y = parseInt(req.body?.year ?? req.query?.year, 10);
+ const year = (Number.isFinite(y) && y >= 2000 && y <= 2100)
+ ? y
+ : (new Date()).getFullYear();
+
+ const { rows } = await pool.query(
+ 'SELECT public.report_tickets_year($1::int) AS j', [year]
+ );
+ res.json(rows[0].j);
+ } catch (e) {
+ console.error('report_tickets error:', e);
+ res.status(500).json({
+ error: 'report_tickets failed',
+ message: e.message, detail: e.detail, where: e.where, code: e.code
+ });
+ }
+});
+
+// POST /api/rpc/report_asistencia { desde: 'YYYY-MM-DD', hasta: 'YYYY-MM-DD' }
+app.post('/api/rpc/report_asistencia', async (req, res) => {
+ try {
+ let { desde, hasta } = req.body || {};
+ // defaults si vienen vacíos/invalidos
+ const re = /^\d{4}-\d{2}-\d{2}$/;
+ if (!re.test(desde) || !re.test(hasta)) {
+ const end = new Date();
+ const start = new Date(end); start.setDate(end.getDate()-30);
+ desde = start.toISOString().slice(0,10);
+ hasta = end.toISOString().slice(0,10);
+ }
+
+ const { rows } = await pool.query(
+ 'SELECT public.report_asistencia($1::date,$2::date) AS j', [desde, hasta]
+ );
+ res.json(rows[0].j);
+ } catch (e) {
+ console.error('report_asistencia error:', e);
+ res.status(500).json({
+ error: 'report_asistencia failed',
+ message: e.message, detail: e.detail, where: e.where, code: e.code
+ });
+ }
+});
+
+
+// app.post('/api/rpc/report_asistencia', async (req,res)=>{
+// try{
+// const {desde, hasta} = req.body||{};
+// const sql = 'SELECT * FROM public.report_asistencia($1::date,$2::date)';
+// const {rows} = await pool.query(sql,[desde, hasta]);
+// res.json(rows);
+// } catch (e) {
+// console.error(e);
+// res.status(500).json({ error: 'report_tickets failed' + e });
+// }
+// });
+
+// app.post('/api/rpc/report_tickets', async (req, res) => {
+// try {
+// const { year } = req.body || {};
+// const sql = 'SELECT public.report_tickets_year($1::int) AS data';
+// const { rows } = await pool.query(sql, [year]);
+// res.json(rows[0]?.data || {});
+// } catch (e) {
+// console.error(e);
+// res.status(500).json({ error: 'report_tickets failed' + e });
+// }
+// });
+
+
+// Guardar (insert/update)
+app.post('/api/rpc/save_compra', async (req, res) => {
+ try {
+ const { id_compra, id_proveedor, fec_compra, detalles } = req.body || {};
+ const sql = 'SELECT * FROM public.save_compra($1::int,$2::int,$3::timestamptz,$4::jsonb)';
+ const args = [id_compra ?? null, id_proveedor, fec_compra ? new Date(fec_compra) : null, JSON.stringify(detalles)];
+ const { rows } = await pool.query(sql, args);
+ res.json(rows[0]); // { id_compra, total }
+ } catch (e) {
+ console.error('save_compra error:', e);
+ res.status(500).json({ error: 'save_compra failed', message: e.message, detail: e.detail, where: e.where, code: e.code });
+ }
+});
+
+
+// Obtener para editar
+app.post('/api/rpc/get_compra', async (req, res) => {
+ try {
+ const { id_compra } = req.body || {};
+ const sql = `SELECT public.get_compra($1::int) AS data`;
+ const { rows } = await pool.query(sql, [id_compra]);
+ res.json(rows[0]?.data || {});
+ } catch (e) {
+ console.error(e); res.status(500).json({ error: 'get_compra failed' });
+ }
+});
+
+// Eliminar
+app.post('/api/rpc/delete_compra', async (req, res) => {
+ try {
+ const { id_compra } = req.body || {};
+ await pool.query(`SELECT public.delete_compra($1::int)`, [id_compra]);
+ res.json({ ok: true });
+ } catch (e) {
+ console.error(e); res.status(500).json({ error: 'delete_compra failed' });
+ }
+});
+
+
+// POST /api/rpc/report_gastos { year: 2025 }
+app.post('/api/rpc/report_gastos', async (req, res) => {
+ try {
+ const year = parseInt(req.body?.year ?? new Date().getFullYear(), 10);
+ const { rows } = await pool.query(
+ 'SELECT public.report_gastos($1::int) AS j', [year]
+ );
+ res.json(rows[0].j);
+ } catch (e) {
+ console.error('report_gastos error:', e);
+ res.status(500).json({
+ error: 'report_gastos failed',
+ message: e.message, detail: e.detail, code: e.code
+ });
+ }
+});
+
+// (Opcional) GET para probar rápido desde el navegador:
+// /api/rpc/report_gastos?year=2025
+app.get('/api/rpc/report_gastos', async (req, res) => {
+ try {
+ const year = parseInt(req.query.year ?? new Date().getFullYear(), 10);
+ const { rows } = await pool.query(
+ 'SELECT public.report_gastos($1::int) AS j', [year]
+ );
+ res.json(rows[0].j);
+ } catch (e) {
+ console.error('report_gastos error:', e);
+ res.status(500).json({
+ error: 'report_gastos failed',
+ message: e.message, detail: e.detail, code: e.code
+ });
+ }
+});
+
+
+// ----------------------------------------------------------
+// Verificación de conexión
+// ----------------------------------------------------------
async function verificarConexion() {
try {
const client = await pool.connect();
const res = await client.query('SELECT NOW() AS hora');
console.log(`\nConexión con la base de datos ${chalk.green(process.env.DB_NAME)} fue exitosa.`);
console.log('Fecha y hora actual de la base de datos:', res.rows[0].hora);
- client.release(); // liberar el cliente de nuevo al pool
+ client.release();
} catch (error) {
console.error('Error al conectar con la base de datos al iniciar:', error.message);
- console.error(`Troubleshooting:\n1. Compruebe que las bases de datos se iniciaron correctamente.\n2. Verifique las credenciales y puertos de acceso a la base de datos.\n3. Si está conectandose a una base de datos externa a localhost, verifique las reglas del firewal de entrada y salida de ambos dispositivos.`);
-
+ console.error('Revisar credenciales y accesos de red.');
}
}
+// ----------------------------------------------------------
+// Inicio del servidor
+// ----------------------------------------------------------
-// === Servir páginas estáticas ===
-
-app.get('/roles', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'roles.html')));
-app.get('/usuarios', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'usuarios.html')));
-app.get('/categorias',(req, res) => res.sendFile(path.join(__dirname, 'pages', 'categorias.html')));
-app.get('/productos', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'productos.html')));
-
-
-// Helper de consulta con acquire/release explícito
-async function q(text, params) {
- const client = await pool.connect();
- try {
- return await client.query(text, params);
- } finally {
- client.release();
- }
-}
-
-// === API Roles ===
- // GET: listar
- app.get('/api/roles', async (req, res) => {
- try {
- const { rows } = await q('SELECT id_rol, nombre FROM roles ORDER BY id_rol ASC');
- res.json(rows);
- } catch (e) {
- console.error(e);
- res.status(500).json({ error: 'No se pudo listar roles' });
- }
- });
-
- // POST: crear
- app.post('/api/roles', async (req, res) => {
- try {
- const { nombre } = req.body;
- if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' });
- const { rows } = await q(
- 'INSERT INTO roles (nombre) VALUES ($1) RETURNING id_rol, nombre',
- [nombre.trim()]
- );
- res.status(201).json(rows[0]);
- } catch (e) {
- console.error(e);
- // Manejo de único/duplicado
- if (e.code === '23505') return res.status(409).json({ error: 'El rol ya existe' });
- res.status(500).json({ error: 'No se pudo crear el rol' });
- }
- });
-
-// === API Usuarios ===
- // GET: listar
- app.get('/api/usuarios', async (req, res) => {
- try {
- const { rows } = await q(`
- SELECT id_usuario, documento, img_perfil, nombre, apellido, correo, telefono, fec_nacimiento, activo
- FROM usuarios
- ORDER BY id_usuario ASC
- `);
- res.json(rows);
- } catch (e) {
- console.error(e);
- res.status(500).json({ error: 'No se pudo listar usuarios' });
- }
- });
-
- // POST: crear
- app.post('/api/usuarios', async (req, res) => {
- try {
- const { documento, nombre, apellido, correo, telefono, fec_nacimiento } = req.body;
- if (!nombre || !apellido) return res.status(400).json({ error: 'Nombre y apellido requeridos' });
-
- const { rows } = await q(`
- INSERT INTO usuarios (documento, nombre, apellido, correo, telefono, fec_nacimiento)
- VALUES ($1, $2, $3, $4, $5, $6)
- RETURNING id_usuario, documento, nombre, apellido, correo, telefono, fec_nacimiento, activo
- `, [
- documento || null,
- nombre.trim(),
- apellido.trim(),
- correo || null,
- telefono || null,
- fec_nacimiento || null
- ]);
-
- res.status(201).json(rows[0]);
- } catch (e) {
- console.error(e);
- if (e.code === '23505') return res.status(409).json({ error: 'Documento/Correo/Teléfono ya existe' });
- res.status(500).json({ error: 'No se pudo crear el usuario' });
- }
- });
-
-// === API Categorías ===
- // GET: listar
- app.get('/api/categorias', async (req, res) => {
- try {
- const { rows } = await q('SELECT id_categoria, nombre, visible FROM categorias ORDER BY id_categoria ASC');
- res.json(rows);
- } catch (e) {
- console.error(e);
- res.status(500).json({ error: 'No se pudo listar categorías' });
- }
- });
-
- // POST: crear
- app.post('/api/categorias', async (req, res) => {
- try {
- const { nombre, visible } = req.body;
- if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' });
- const vis = (typeof visible === 'boolean') ? visible : true;
-
- const { rows } = await q(`
- INSERT INTO categorias (nombre, visible)
- VALUES ($1, $2)
- RETURNING id_categoria, nombre, visible
- `, [nombre.trim(), vis]);
-
- res.status(201).json(rows[0]);
- } catch (e) {
- console.error(e);
- if (e.code === '23505') return res.status(409).json({ error: 'La categoría ya existe' });
- res.status(500).json({ error: 'No se pudo crear la categoría' });
- }
- });
-
-// === API Productos ===
- // GET: listar
- app.get('/api/productos', async (req, res) => {
- try {
- const { rows } = await q(`
- SELECT id_producto, nombre, img_producto, precio, activo, id_categoria
- FROM productos
- ORDER BY id_producto ASC
- `);
- res.json(rows);
- } catch (e) {
- console.error(e);
- res.status(500).json({ error: 'No se pudo listar productos' });
- }
- });
-
- // POST: crear
- app.post('/api/productos', async (req, res) => {
- try {
- let { nombre, id_categoria, precio } = req.body;
- if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' });
- id_categoria = parseInt(id_categoria, 10);
- precio = parseFloat(precio);
- if (!Number.isInteger(id_categoria)) return res.status(400).json({ error: 'id_categoria inválido' });
- if (!(precio >= 0)) return res.status(400).json({ error: 'precio inválido' });
-
- const { rows } = await q(`
- INSERT INTO productos (nombre, id_categoria, precio)
- VALUES ($1, $2, $3)
- RETURNING id_producto, nombre, precio, activo, id_categoria
- `, [nombre.trim(), id_categoria, precio]);
-
- res.status(201).json(rows[0]);
- } catch (e) {
- console.error(e);
- // FK categories / checks
- if (e.code === '23503') return res.status(400).json({ error: 'La categoría no existe' });
- res.status(500).json({ error: 'No se pudo crear el producto' });
- }
- });
-
-
-// Colores personalizados
-let primaryColor = chalk.hex('#'+`${process.env.COL_PRI}`);
-let secondaryColor = chalk.hex('#'+`${process.env.COL_SEC}`);
-// let backgroundColor = chalk.hex('#'+`${process.env.COL_BG}`);
-
-
-app.use(expressLayouts);
-// Iniciar servidor
-app.listen( process.env.PORT, () => {
- console.log(`Servidor de ${chalk.red('aplicación')} de ${secondaryColor('SuiteCoffee')} corriendo en ${chalk.yellow(`http://localhost:${process.env.PORT}\n`)}` );
- console.log(chalk.grey(`Comprobando accesibilidad a la db ${chalk.green(process.env.DB_NAME)} del host ${chalk.white(`${process.env.DB_HOST}`)} ...`));
+const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
+app.listen(PORT, () => {
+ console.log(`Servidor de aplicación escuchando en ${chalk.yellow(`http://localhost:${PORT}`)}`);
+ console.log(chalk.grey(`Comprobando accesibilidad a la db ${chalk.white(process.env.DB_NAME)} del host ${chalk.white(process.env.DB_HOST)} ...`));
verificarConexion();
});
-app.get("/health", async (req, res) => {
- // Podés chequear DB aquí. 200 = healthy; 503 = not ready.
- res.status(200).json({ status: "ok" });
-});
\ No newline at end of file
+// Healthcheck
+app.get('/health', async (_req, res) => {
+ res.status(200).json({ status: 'ok' });
+});
diff --git a/services/app/src/pages/categorias.html b/services/app/src/pages/categorias.html
deleted file mode 100644
index 0a61191..0000000
--- a/services/app/src/pages/categorias.html
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
-
- Categorías
-
-
- Categorías
-
- Crear categoría
-
-
- Listado
- Recargar
-
-
-
-
-
diff --git a/services/app/src/pages/comandas.html.bak b/services/app/src/pages/comandas.html.bak
new file mode 100644
index 0000000..93c6e5d
--- /dev/null
+++ b/services/app/src/pages/comandas.html.bak
@@ -0,0 +1,355 @@
+
+
+
+
+
+ Comandas
+
+
+
+
+ 📋 Nueva Comanda
+
+ /api/*
+
+
+
+
+
+
+
Productos
+
+
+ 0 ítems
+
+
+
+
+
+
+
+ Detalles
+
+
+
+ Mesa
+
+
+
+ Usuario
+
+
+
+
+
+ Observaciones
+
+
+
+
La fecha se completa automáticamente y los estados/activos usan sus valores por defecto.
+
+
+
Carrito
+
+
Aún no agregaste productos.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/pages/dashboard.html.bak b/services/app/src/pages/dashboard.html.bak
new file mode 100644
index 0000000..78fbbb6
--- /dev/null
+++ b/services/app/src/pages/dashboard.html.bak
@@ -0,0 +1,293 @@
+
+
+
+
+
+ Dashboard
+
+
+
+
+
+
+
+
Tabla
+
+
+
+ Recargar
+
+
+
+
+
+ Datos
+ Nuevo
+ Esquema
+
+
+ Mostrando hasta 100 filas.
+
+
+
+
+
+
+ Endpoints
+ GET /api/tables • GET /api/schema/:tabla • GET /api/table/:tabla?limit=100 • POST /api/table/:tabla
+
+
+
+
+
+
diff --git a/services/app/src/pages/estadoComandas.html.bak b/services/app/src/pages/estadoComandas.html.bak
new file mode 100644
index 0000000..dd69c9a
--- /dev/null
+++ b/services/app/src/pages/estadoComandas.html.bak
@@ -0,0 +1,280 @@
+
+
+
+
+
+
+ Estado de Comandas
+
+
+
+
+
+
+
+
+
+
Listado
+
+
+
+ Solo abiertas
+
+
+
+
+
+
+
+
+
+
Selecciona una comanda para ver el detalle.
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/pages/productos.html b/services/app/src/pages/productos.html
deleted file mode 100644
index 82f164f..0000000
--- a/services/app/src/pages/productos.html
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-
-
- Productos
-
-
- Productos
-
- Crear producto
-
-
- Listado
- Recargar
-
-
- ID Nombre Precio Activo ID Categoría
-
-
-
-
-
-
-
diff --git a/services/app/src/pages/roles.html b/services/app/src/pages/roles.html
deleted file mode 100644
index 4f26d36..0000000
--- a/services/app/src/pages/roles.html
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
- Roles
-
-
- Roles
-
- Crear rol
-
-
- Listado
- Recargar
-
-
-
-
-
diff --git a/services/app/src/pages/usuarios.html b/services/app/src/pages/usuarios.html
deleted file mode 100644
index 9c6e7b9..0000000
--- a/services/app/src/pages/usuarios.html
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
-
- Usuarios
-
-
- Usuarios
-
- Crear usuario
-
-
- Listado
- Recargar
-
-
-
- ID Documento Nombre Apellido
- Correo Teléfono Nacimiento Activo
-
-
-
-
-
-
-
-
diff --git a/services/app/src/public/favicon/android-chrome-192x192.png b/services/app/src/public/favicon/android-chrome-192x192.png
new file mode 100644
index 0000000..1210323
Binary files /dev/null and b/services/app/src/public/favicon/android-chrome-192x192.png differ
diff --git a/services/app/src/public/favicon/android-chrome-512x512.png b/services/app/src/public/favicon/android-chrome-512x512.png
new file mode 100644
index 0000000..1b029ab
Binary files /dev/null and b/services/app/src/public/favicon/android-chrome-512x512.png differ
diff --git a/services/app/src/public/favicon/apple-touch-icon.png b/services/app/src/public/favicon/apple-touch-icon.png
new file mode 100644
index 0000000..4164fdb
Binary files /dev/null and b/services/app/src/public/favicon/apple-touch-icon.png differ
diff --git a/services/app/src/public/favicon/favicon-16x16.png b/services/app/src/public/favicon/favicon-16x16.png
new file mode 100644
index 0000000..eabf7e6
Binary files /dev/null and b/services/app/src/public/favicon/favicon-16x16.png differ
diff --git a/services/app/src/public/favicon/favicon-32x32.png b/services/app/src/public/favicon/favicon-32x32.png
new file mode 100644
index 0000000..e98f802
Binary files /dev/null and b/services/app/src/public/favicon/favicon-32x32.png differ
diff --git a/services/app/src/public/favicon/favicon.ico b/services/app/src/public/favicon/favicon.ico
new file mode 100644
index 0000000..c8e2059
Binary files /dev/null and b/services/app/src/public/favicon/favicon.ico differ
diff --git a/services/app/src/public/favicon/site.webmanifest b/services/app/src/public/favicon/site.webmanifest
new file mode 100644
index 0000000..45dc8a2
--- /dev/null
+++ b/services/app/src/public/favicon/site.webmanifest
@@ -0,0 +1 @@
+{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
diff --git a/services/app/src/public/img/productos/img_producto.png b/services/app/src/public/img/productos/img_producto.png
new file mode 100644
index 0000000..a0529b4
Binary files /dev/null and b/services/app/src/public/img/productos/img_producto.png differ
diff --git a/services/app/src/views/comandas.ejs b/services/app/src/views/comandas.ejs
new file mode 100644
index 0000000..ae564e9
--- /dev/null
+++ b/services/app/src/views/comandas.ejs
@@ -0,0 +1,558 @@
+
+
+
📋 Nueva Comanda
+ /api/*
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mesa
+
+
+
+ Usuario
+
+
+
+
+
+ Observaciones
+
+
+
+
+ La fecha se completa automáticamente y los estados/activos usan sus valores por defecto.
+
+
+
+
+
+
+
+
Aún no agregaste productos.
+
+
+
Ítems: 0
+
Total: $ 0.00
+
+
Vaciar
+
Crear Comanda
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/views/compras.ejs b/services/app/src/views/compras.ejs
new file mode 100644
index 0000000..6111cdd
--- /dev/null
+++ b/services/app/src/views/compras.ejs
@@ -0,0 +1,361 @@
+<% /* Compras / Gastos */ %>
+
+
+
Compras / Gastos
+
+ Nueva
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+ Proveedor
+ Fecha
+ Total
+
+
+
+
+ Sin datos
+
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/views/dashboard.ejs b/services/app/src/views/dashboard.ejs
new file mode 100644
index 0000000..fb2ae9d
--- /dev/null
+++ b/services/app/src/views/dashboard.ejs
@@ -0,0 +1,487 @@
+
+
+
Dashboard Operativo
+
+ Recargar
+
+
+
+
+
+
+
+
+
+
+
+
Ticket promedio (hoy)
+
—
+
+
+
+
+
+
+
Productos distintos (hoy)
+
—
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Basado en detalle de comandas de hoy.
+
+
+
+
+
+
+
+
+
+
+
+
Se agrupa por hora de creación.
+
+
+
+
+
+
+
+
+
+
+
+
Distribución por estado.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+ Fecha
+ Cierre
+ Estado
+ Total
+ Acción
+
+
+
+ Cargando…
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/views/estadoComandas.ejs b/services/app/src/views/estadoComandas.ejs
new file mode 100644
index 0000000..2d3615b
--- /dev/null
+++ b/services/app/src/views/estadoComandas.ejs
@@ -0,0 +1,532 @@
+
+
+
🧾 Estado de Comandas
+
➕ Nueva comanda
+
+
+
+
+
+
+
+
+
+
+
+
+
Selecciona una comanda para ver el detalle.
+
+
+
+
ID: —
+
Mesa: —
+
Total: $ 0.00
+
+
Reabrir
+
Cerrar
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/views/layouts/main.ejs b/services/app/src/views/layouts/main.ejs
new file mode 100644
index 0000000..2d8aaf9
--- /dev/null
+++ b/services/app/src/views/layouts/main.ejs
@@ -0,0 +1,16 @@
+
+
+
+ <%- include('../partials/_head') %>
+
+
+ <%- include('../partials/_navbar') %>
+
+
+ <%- body %>
+
+
+ <%- include('../partials/_sidebar') %>
+ <%- include('../partials/_footer') %>
+
+
diff --git a/services/app/src/views/partials/_footer.ejs b/services/app/src/views/partials/_footer.ejs
new file mode 100644
index 0000000..b6c9840
--- /dev/null
+++ b/services/app/src/views/partials/_footer.ejs
@@ -0,0 +1,42 @@
+
+
+
+
diff --git a/services/app/src/views/partials/_head.ejs b/services/app/src/views/partials/_head.ejs
new file mode 100644
index 0000000..10f17df
--- /dev/null
+++ b/services/app/src/views/partials/_head.ejs
@@ -0,0 +1,45 @@
+
+
+
+<%= typeof pageTitle !== "undefined" ? pageTitle : "SuiteCoffee" %>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/views/partials/_navbar.ejs b/services/app/src/views/partials/_navbar.ejs
new file mode 100644
index 0000000..486666a
--- /dev/null
+++ b/services/app/src/views/partials/_navbar.ejs
@@ -0,0 +1,31 @@
+
+
+
+
diff --git a/services/app/src/views/partials/_sidebar.ejs b/services/app/src/views/partials/_sidebar.ejs
new file mode 100644
index 0000000..be71ee2
--- /dev/null
+++ b/services/app/src/views/partials/_sidebar.ejs
@@ -0,0 +1,72 @@
+
+
+
+
diff --git a/services/app/src/views/productos.ejs b/services/app/src/views/productos.ejs
new file mode 100644
index 0000000..c67fe8b
--- /dev/null
+++ b/services/app/src/views/productos.ejs
@@ -0,0 +1,559 @@
+
+
+
🛒 Productos
+
+ Nuevo
+ Guardar
+ Materias primas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+ Nombre
+ Precio
+ Activo
+ Categoría
+
+
+
+ Cargando…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nombre
+
+
+
+ Precio
+
+
+
+ Categoría
+
+
+
+
Imagen (URL)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Materia prima
+ Cantidad
+
+
+
+
+ Sin ingredientes.
+
+
+
+
+
+
+
+
+
+
+
+
+
⚙️ Materias primas
+
+ Nuevo
+ Guardar
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+ Nombre
+ Unidad
+ Activo
+
+
+
+ Cargando…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nombre
+
+
+
+ Unidad
+
+
+
+
+
+
Proveedores (asignación)
+
+
Mantén presionadas Ctrl/⌘ para seleccionar varios.
+
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/views/reportes.ejs b/services/app/src/views/reportes.ejs
new file mode 100644
index 0000000..7d9feb7
--- /dev/null
+++ b/services/app/src/views/reportes.ejs
@@ -0,0 +1,836 @@
+<% /* Reportes - Asistencias, Tickets y Gastos */ %>
+
+
+
+
Reportes
+ —
+
+
+
+
+
+
+
+
Asistencias · Rango
+
+
+
+
+ Cargar
+ Excel
+ PDF
+
+
+
+
+
+
Año (Tickets / Gastos)
+
+
+
+ Cargar
+ Excel (Comparativo)
+ PDF (Comparativo)
+
+
+
+
+
+
+ Los Excel se generan como CSV. Los PDF se generan con “Imprimir área” del navegador.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tickets por mes
+
Cantidad
+
+
+
+
+
+
+
+
+
+ Mes Tickets Importe Ticket promedio
+
+ Sin datos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total: —
+ Compras: —
+ Renglones: —
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mes Ingresos Gastos Resultado
+
+ Sin datos
+
+
+
+
+
+ Excel
+ PDF
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/services/app/src/views/reportes.ejs.bak b/services/app/src/views/reportes.ejs.bak
new file mode 100644
index 0000000..7f5ce28
--- /dev/null
+++ b/services/app/src/views/reportes.ejs.bak
@@ -0,0 +1,402 @@
+<% /* Reportes - Asistencias y Tickets (Comandas) */ %>
+
+
+
+
Reportes
+ —
+
+
+
+
+
+
+
+
Asistencias · Rango
+
+
+
+
+
+
+
+
+ Cargar
+ Excel
+ PDF
+
+
+
+
+
+
Tickets (Comandas) · Año
+
+
+
+
+
+ Cargar
+ Excel
+ PDF
+
+
+
+
+
+
+ Los archivos Excel se generan como CSV (compatibles). Los PDF se generan con “Imprimir área” del navegador.
+
+
+
+
+
+
+
+
+
+
+
+
+ Documento
+ Nombre
+ Apellido
+ Fecha
+ Desde
+ Hasta
+ Duración
+
+
+
+ Sin datos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Tickets por mes
+
Cantidad
+
+
+
+
+
+
+
+
+
+
+ Mes
+ Tickets
+ Importe
+ Ticket promedio
+
+
+
+ Sin datos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/app/src/views/usuarios.ejs b/services/app/src/views/usuarios.ejs
new file mode 100644
index 0000000..7a558da
--- /dev/null
+++ b/services/app/src/views/usuarios.ejs
@@ -0,0 +1,1030 @@
+
+
+
+
+
Usuarios · Carga de horarios
+
+
+
+
Guardar asistencia
+
Procesar
+
Exportar CSV
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Documento
+ Nombre
+ Fecha
+ Desde
+ Hasta
+ Duración
+ Obs.
+
+
+
+ Cargá un archivo .txt y presioná “Procesar”.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+ Documento
+ Nombre
+ Modo
+ Fecha
+ Hora
+
+
+
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Documento
+
+
Ingrese documento (se limpian ceros a la izquierda).
+
+
+ Desde
+
+
+
+ Hasta
+
+
+
+ Cargar
+
+
+ —
+
+
+
+
+
+
+
+
+
+
+
+
+ #
+ Fecha
+ Hora
+ Modo
+ Origen
+
+
+
+
+ Sin datos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Fecha (jornada)
+ Desde
+ Hasta
+ Duración
+
+
+
+ Sin datos
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/auth/package-lock.json b/services/auth/package-lock.json
index 68a228c..9b364ae 100644
--- a/services/auth/package-lock.json
+++ b/services/auth/package-lock.json
@@ -11,10 +11,12 @@
"dependencies": {
"bcrypt": "^5.1.1",
"chalk": "^5.6.0",
+ "cookie-session": "^2.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"express-ejs-layouts": "^2.5.1",
+ "openid-client": "^5.6.5",
"pg": "^8.16.3",
"pg-format": "^1.0.4"
},
@@ -335,6 +337,30 @@
"node": ">= 0.6"
}
},
+ "node_modules/cookie-session": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.1.tgz",
+ "integrity": "sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookies": "0.9.1",
+ "debug": "3.2.7",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cookie-session/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
@@ -344,6 +370,19 @@
"node": ">=6.6.0"
}
},
+ "node_modules/cookies": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz",
+ "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "keygrip": "~1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@@ -967,6 +1006,39 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/jose": {
+ "version": "4.15.9",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
+ "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/keygrip": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
+ "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tsscmp": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -1217,6 +1289,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-hash": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
+ "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -1229,6 +1310,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/oidc-token-hash": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.1.tgz",
+ "integrity": "sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==",
+ "license": "MIT",
+ "engines": {
+ "node": "^10.13.0 || >=12.0.0"
+ }
+ },
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -1241,6 +1331,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -1250,6 +1349,21 @@
"wrappy": "1"
}
},
+ "node_modules/openid-client": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
+ "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
+ "license": "MIT",
+ "dependencies": {
+ "jose": "^4.15.9",
+ "lru-cache": "^6.0.0",
+ "object-hash": "^2.2.0",
+ "oidc-token-hash": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1877,6 +1991,15 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/tsscmp": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+ "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.x"
+ }
+ },
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",