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

- -

Herramientas

- -

Redes & Volúmenes

- -

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

- -
-

Requisitos

- -
-

Inicio rápido

-

Opción A — Gestor interactivo (recomendado)

-
    -
  1. Clona el repo y entra al directorio:
    git clone https://gitea.mateosaldain.uy/msaldain/SuiteCoffee.git -cd SuiteCoffee -
    -
  2. -
  3. (Opcional) Crea/copía tus archivos .env para app y auth en ./services/<service>/.env.development (ver sección de variables).
  4. -
  5. Ejecuta el gestor:
    python3 suitecoffee.py -
    - -
  6. -
  7. Accede: - -
  8. -
-
-

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)

- -
-

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)

- -

Servicio auth (autenticació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.

- -
-

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:

- -

Ejemplos básicos

-
# Listar ayuda -python3 backup_compose_volumes.py --help - -# Respaldar volúmenes asociados a "suitecoffee_dev" en ./backups -python3 backup_compose_volumes.py --project suitecoffee_dev --output ./backups - -# Restaurar un archivo a un volumen -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

-
# Ver estado (menú interactivo) -python3 suitecoffee.py - -# Levantar DEV/PROD por menú (con o sin --force-recreate) -python3 suitecoffee.py - -# Levantar herramientas (también desde menú) -docker compose -f compose.tools.yaml --profile npm -p suitecoffee up -d -docker compose -f compose.tools.yaml --profile dbeaver -p suitecoffee up -d - -# Inspeccionar servicios/volúmenes que Compose detecta desde los YAML -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

- -
-

Sugerencias de mejora

- -
- - - 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

- - - - -
IDNombreVisible
- - - - 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
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
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 + + + +
+

Dashboard

+
+ /api/* +
+ +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
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 + + + +
+

🧾 Estado de Comandas

+
+ ➕ Nueva comanda +
+ +
+ +
+
+ Listado +
+ +
+
+
+ + +
+
+
+
+ + +
+
+ Detalle +
+ +
+
+
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

- - - - - - -
IDNombrePrecioActivoID 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

- - - - -
IDNombre
- - - - 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

- - - - - - - - - -
IDDocumentoNombreApellidoCorreoTeléfonoNacimientoActivo
- - - - 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/* +
+ +
+ +
+
+
+ Productos +
0 ítems
+
+
+
+
+ +
+
+ +
+
+ +
+ +
Cargando…
+
+
+
+
+ + +
+
+
Detalles
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ La fecha se completa automáticamente y los estados/activos usan sus valores por defecto. +
+
+
+ +
+
Carrito
+
+
Aún no agregaste productos.
+
+
+
Ítems: 0
+
Total: $ 0.00
+
+ + +
+
+ +
+
+
+ + + 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 compra
+
+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+
Renglones
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + +
TipoÍtemCantidadPrecioSubtotal
Sin renglones
+
+
+ +
+ + +
+
+
+
+ + +
+
+ Compras recientes + +
+
+
+ + + + + + + + + + + + + +
#ProveedorFechaTotal
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

+
+ + +
+
+ + +
+
+
+
+
Comandas activas
+
+
+
+
+
+
+
+
Ventas hoy
+
+
+
+
+
+
+
+
Ticket promedio (hoy)
+
+
+
+
+
+
+
+
Productos distintos (hoy)
+
+
+
+
+
+ + +
+
+
+
Top 5 productos (hoy)
+
+
+ +
+
Basado en detalle de comandas de hoy.
+
+
+
+ +
+
+
Comandas por hora (últimas 12 h)
+
+
+ +
+
Se agrupa por hora de creación.
+
+
+
+ +
+
+
Estados de comandas (hoy)
+
+
+ +
+
Distribución por estado.
+
+
+
+ + +
+
+
+ Últimas 10 comandas +
+
+
+
+ + + + + + + + + + + + + + +
#FechaCierre EstadoTotalAcció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 +
+ +
+ +
+
+
+ Listado +
+ + +
+
+
+
+
+ +
+
+ +
+
+ +
+
Cargando…
+
+
+
+
+ + +
+
+
+ Detalle + +
+ +
+
Selecciona una comanda para ver el detalle.
+
+ +
+
ID:
+
Mesa:
+
Total: $ 0.00
+
+ + +
+ +
+
+
+
+
+
+ + + + + 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 @@ + +
+
+
Opciones
+ +
+
+ +
+
+
+ + 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

+
+ + + + +
+
+ +
+ +
+
+
+ Listado +
+ + +
+
+
+
+ + + + + + + + + + + + + +
#NombrePrecioActivoCategoría
Cargando…
+
+
+
+
+ + +
+
+
Ficha
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+ + +
+
+
+
+
+ + +
+
+ Receta (materias primas por unidad) + +
+
+
+ + + + + + + + + + + +
Materia primaCantidad
Sin ingredientes.
+
+
+ +
+
+
+ + +
+
+

⚙️ Materias primas

+
+ + +
+
+ +
+ +
+
+
+ Listado +
+ + +
+
+
+
+ + + + + + + + + + + + +
#NombreUnidadActivo
Cargando…
+
+
+
+
+ + +
+
+
Ficha
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+ + +
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

+ +
+ + +
+
+
+
+ +
+
+
+
+ + + +
+
+
+ +
+ +
+
+
+ + + +
+
+
+
+ +
+ Los Excel se generan como CSV. Los PDF se generan con “Imprimir área” del navegador. +
+
+
+ + + + + + + + + + + + + + + + +
+ + + + \ 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

+ +
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+ +
+ +
+
+ +
+
+ + + +
+
+
+
+ +
+ Los archivos Excel se generan como CSV (compatibles). Los PDF se generan con “Imprimir área” del navegador. +
+
+
+ + + + + + + +
+ + + + 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

+
+ + + + + +
+
+ + + + +
+ +
+
+
+ Resumen diario (últimos 30 días) +
+
+
+
+
+
+
+ + +
+
+
Carga manual de horario
+
+
+
+ + +
Ingresá el documento (se quitan ceros a la izquierda).
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+
+
+
+
+ + +
+
+
+ Resumen por usuario y día (pares de fichadas) +
Sin datos
+
+
+
+ + + + + + + + + + + + + + + +
DocumentoNombreFechaDesdeHastaDuraciónObs.
Cargá un archivo .txt y presioná “Procesar”.
+
+
+ +
+
+ + +
+
+
Registros crudos
+
+
+ + + + + + + + + + + + + + +
#DocumentoNombreModoFechaHora
+
+
+ +
+
+ +
+
+
Administrar asistencia
+
+
+
+ + +
Ingrese documento (se limpian ceros a la izquierda).
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
Marcas (crudo)
+
+
+ + + + + + + + + + + + + + +
#FechaHoraModoOrigen
Sin datos
+
+
+ +
+
+ +
+
+
Intervalos (pares)
+
+
+ + + + + + + + + + + + +
Fecha (jornada)DesdeHastaDuració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",