From a73ab28faebffea0f83c045030a1d5f9347850d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 4 Jul 2026 20:41:27 +0000 Subject: [PATCH] =?UTF-8?q?Ajout=20de=20l'application=20Presencia=20(gesti?= =?UTF-8?q?on=20des=20pr=C3=A9sences=20multi-soci=C3=A9t=C3=A9s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Application complète : API Node/Express + PostgreSQL, frontend HTML/JS mono-page, et déploiement Docker sur ports non standards (8781/4790/6543). - Cadres : calendrier mensuel, saisie de présence par demi-journée (présent/absent/congé/RTT), validation de fin de mois qui verrouille leurs propres saisies. - Admin : gestion des sociétés et utilisateurs (rattachement société <-> cadre), consultation de tous les plannings, validation d'une société entière une fois tous ses cadres validés, réouverture en cas de correction, export PDF et Excel par société/mois. --- .env.example | 18 + .gitignore | 3 + README.md | 79 + backend/.env.example | 14 + backend/Dockerfile | 13 + backend/migrations/001_init.sql | 59 + backend/package-lock.json | 2926 +++++++++++++++++++++++++++++ backend/package.json | 21 + backend/src/bootstrap.js | 70 + backend/src/db.js | 14 + backend/src/index.js | 46 + backend/src/middleware/auth.js | 60 + backend/src/routes/attendance.js | 151 ++ backend/src/routes/auth.js | 66 + backend/src/routes/companies.js | 57 + backend/src/routes/export.js | 81 + backend/src/routes/users.js | 111 ++ backend/src/routes/validations.js | 146 ++ backend/src/utils/excel.js | 84 + backend/src/utils/pdf.js | 94 + backend/src/utils/status.js | 26 + docker-compose.yml | 49 + frontend/Dockerfile | 6 + frontend/nginx.conf | 20 + frontend/public/css/style.css | 338 ++++ frontend/public/index.html | 200 ++ frontend/public/js/app.js | 740 ++++++++ 27 files changed, 5492 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/migrations/001_init.sql create mode 100644 backend/package-lock.json create mode 100644 backend/package.json create mode 100644 backend/src/bootstrap.js create mode 100644 backend/src/db.js create mode 100644 backend/src/index.js create mode 100644 backend/src/middleware/auth.js create mode 100644 backend/src/routes/attendance.js create mode 100644 backend/src/routes/auth.js create mode 100644 backend/src/routes/companies.js create mode 100644 backend/src/routes/export.js create mode 100644 backend/src/routes/users.js create mode 100644 backend/src/routes/validations.js create mode 100644 backend/src/utils/excel.js create mode 100644 backend/src/utils/pdf.js create mode 100644 backend/src/utils/status.js create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf create mode 100644 frontend/public/css/style.css create mode 100644 frontend/public/index.html create mode 100644 frontend/public/js/app.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d68b44f --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# Ports d'accès (volontairement non standards) +FRONTEND_HOST_PORT=8781 +BACKEND_HOST_PORT=4790 +DB_HOST_PORT=6543 + +# Base de données +POSTGRES_USER=presencia +POSTGRES_PASSWORD=change-me-strong-password +POSTGRES_DB=presencia + +# Sécurité +JWT_SECRET=change-me-to-a-long-random-string +COOKIE_SECURE=false + +# Compte administrateur créé au premier démarrage +ADMIN_EMAIL=admin@presencia.local +ADMIN_PASSWORD=ChangeMe123! +ADMIN_NAME=Administrateur diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e8157a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ec16d1 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# Presencia + +Application de gestion des présences par demi-journée, multi-sociétés, avec validation mensuelle en deux étapes (cadre puis administrateur) et export PDF/Excel par société. + +## Fonctionnalités + +- **Connexion uniquement** : aucune inscription publique. Tous les comptes (admin et cadres) sont créés par un administrateur. +- **Multi-sociétés** : chaque cadre est rattaché à une société. L'administrateur voit toutes les sociétés et tous les plannings ; un cadre ne voit que le sien. +- **Planning mensuel** : vue calendrier, clic sur une demi-journée (matin/après-midi) pour choisir : Présent, Absent, Congé, RTT. +- **Validation de fin de mois en deux temps** : + 1. Chaque cadre **valide son propre mois** (verrouille ses saisies). + 2. Une fois **tous les cadres d'une société** validés, l'administrateur **valide la société** pour ce mois (verrouillage complet). L'administrateur peut réouvrir un mois cadre ou une société en cas de correction nécessaire. +- **Export** PDF et Excel par société et par mois (détail par cadre, jour et demi-journée). +- **Docker** : Postgres + API Node/Express + frontend Nginx, sur des ports non standards. + +## Démarrage + +```bash +cp .env.example .env +# éditer .env : mot de passe DB, JWT_SECRET, identifiants admin par défaut +docker compose up -d --build +``` + +Ports par défaut (modifiables dans `.env`) : + +| Service | Port hôte | +|-----------|-----------| +| Frontend | 8781 | +| API | 4790 | +| Postgres | 6543 | + +Ouvrir http://localhost:8781 + +Un compte administrateur est créé automatiquement au premier démarrage avec les identifiants définis par `ADMIN_EMAIL` / `ADMIN_PASSWORD` dans `.env` (par défaut `admin@presencia.local` / `ChangeMe123!`). **Changez ce mot de passe après la première connexion** (aucune page de changement de mot de passe en libre-service n'est fournie côté cadre ; un administrateur peut réinitialiser le mot de passe de n'importe quel compte depuis l'onglet Utilisateurs). + +## Utilisation + +### En tant qu'administrateur + +- **Sociétés** : créer / renommer / supprimer les sociétés. +- **Utilisateurs** : créer des comptes cadre ou admin, attribuer une société à un cadre, réinitialiser un mot de passe, activer/désactiver un compte. +- **Plannings** : consulter (et corriger si besoin) le planning de n'importe quel cadre. +- **Validation & export** : pour une société et un mois donnés, suivre la validation de chaque cadre, valider la société une fois tous les cadres validés, réouvrir si besoin, télécharger le PDF ou l'Excel récapitulatif. + +### En tant que cadre + +- Renseigner sa présence par demi-journée sur le mois en cours (ou les mois précédents/suivants). +- En fin de mois, cliquer sur **"Valider mon mois"** : les saisies sont alors verrouillées et transmises pour validation à l'administrateur. Si une correction est nécessaire après coup, il faut qu'un administrateur réouvre le mois. + +## Architecture technique + +``` +backend/ API Node.js / Express, PostgreSQL (pg), auth par cookie JWT httpOnly +frontend/ Page HTML unique (vanilla JS), servie par Nginx qui proxifie /api vers le backend +docker-compose.yml +``` + +### Modèle de données (PostgreSQL) + +- `companies` — sociétés +- `users` — comptes (rôle `admin` ou `cadre`, société associée pour les cadres) +- `attendance_entries` — une ligne par utilisateur / date / demi-journée (AM ou PM) / statut +- `month_locks` — validation du mois par le cadre (`cadre_validated`) +- `company_month_validations` — validation du mois par l'administrateur pour une société entière (`admin_validated`) + +Règles de verrouillage : +- Un cadre ne peut plus modifier ses saisies une fois son mois validé (`month_locks.cadre_validated = true`), tant qu'un administrateur ne l'a pas réouvert. +- Personne (y compris l'administrateur) ne peut modifier les saisies d'une société dont le mois est validé (`company_month_validations.admin_validated = true`), tant qu'il n'a pas été réouvert. +- L'administrateur ne peut valider une société pour un mois donné que si **tous** les cadres actifs de cette société ont déjà validé leur mois. + +## Développement local (sans Docker) + +```bash +cd backend && npm install +# variables d'env : voir backend/.env.example (pointer PGHOST vers un Postgres local) +npm start +``` + +Le frontend est un simple dossier de fichiers statiques (`frontend/public`) ; en développement, servez-le avec n'importe quel serveur statique qui proxifie `/api` vers `http://localhost:4790` (voir `frontend/nginx.conf` pour la configuration de référence). diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..4a427ff --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,14 @@ +PORT=4790 + +PGHOST=db +PGPORT=5432 +PGUSER=presencia +PGPASSWORD=presencia +PGDATABASE=presencia + +JWT_SECRET=change-me-to-a-long-random-string +COOKIE_SECURE=false + +ADMIN_EMAIL=admin@presencia.local +ADMIN_PASSWORD=ChangeMe123! +ADMIN_NAME=Administrateur diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..d984c81 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev + +COPY . . + +ENV PORT=4790 +EXPOSE 4790 + +CMD ["node", "src/index.js"] diff --git a/backend/migrations/001_init.sql b/backend/migrations/001_init.sql new file mode 100644 index 0000000..afbe8b0 --- /dev/null +++ b/backend/migrations/001_init.sql @@ -0,0 +1,59 @@ +-- Presencia schema + +CREATE TYPE user_role AS ENUM ('admin', 'cadre'); +CREATE TYPE half_day_period AS ENUM ('AM', 'PM'); +CREATE TYPE attendance_status AS ENUM ('present', 'absent', 'conge', 'rtt'); + +CREATE TABLE companies ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + full_name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + role user_role NOT NULL DEFAULT 'cadre', + company_id INTEGER REFERENCES companies(id) ON DELETE SET NULL, + active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_users_company ON users(company_id); + +CREATE TABLE attendance_entries ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + entry_date DATE NOT NULL, + period half_day_period NOT NULL, + status attendance_status NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(user_id, entry_date, period) +); + +CREATE INDEX idx_attendance_user_date ON attendance_entries(user_id, entry_date); + +-- Cadre self-validation of a given month (locks that month's entries for the cadre) +CREATE TABLE month_locks ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + year INTEGER NOT NULL, + month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12), + cadre_validated BOOLEAN NOT NULL DEFAULT false, + cadre_validated_at TIMESTAMPTZ, + UNIQUE(user_id, year, month) +); + +-- Admin validation of a whole company for a given month (requires all cadres validated first) +CREATE TABLE company_month_validations ( + id SERIAL PRIMARY KEY, + company_id INTEGER NOT NULL REFERENCES companies(id) ON DELETE CASCADE, + year INTEGER NOT NULL, + month INTEGER NOT NULL CHECK (month BETWEEN 1 AND 12), + admin_validated BOOLEAN NOT NULL DEFAULT false, + admin_validated_at TIMESTAMPTZ, + admin_validated_by INTEGER REFERENCES users(id), + UNIQUE(company_id, year, month) +); diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..f2edc2c --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2926 @@ +{ + "name": "presencia-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "presencia-backend", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "exceljs": "^4.4.0", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "pdfkit": "^0.15.0", + "pg": "^8.12.0" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.3.17.tgz", + "integrity": "sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "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" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.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" + } + }, + "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" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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/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" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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" + } + }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fontkit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", + "integrity": "sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.3.13", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "deep-equal": "^2.0.5", + "dfa": "^1.2.0", + "restructure": "^2.0.1", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.3.1", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "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" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/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/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "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" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "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" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pdfkit": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.15.2.tgz", + "integrity": "sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^1.8.1", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.0.2", + "png-js": "^1.0.0" + } + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "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" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "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", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/png-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz", + "integrity": "sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==", + "dependencies": { + "browserify-zlib": "^0.2.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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" + } + }, + "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" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "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", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/restructure": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", + "integrity": "sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "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/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "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" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "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" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "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" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..e76d8db --- /dev/null +++ b/backend/package.json @@ -0,0 +1,21 @@ +{ + "name": "presencia-backend", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "exceljs": "^4.4.0", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "pdfkit": "^0.15.0", + "pg": "^8.12.0" + } +} diff --git a/backend/src/bootstrap.js b/backend/src/bootstrap.js new file mode 100644 index 0000000..6819e24 --- /dev/null +++ b/backend/src/bootstrap.js @@ -0,0 +1,70 @@ +const fs = require('fs'); +const path = require('path'); +const bcrypt = require('bcryptjs'); +const db = require('./db'); + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function waitForDb(retries = 30, delayMs = 2000) { + for (let i = 0; i < retries; i++) { + try { + await db.query('SELECT 1'); + return; + } catch (err) { + console.log(`En attente de la base de données... (${i + 1}/${retries})`); + await sleep(delayMs); + } + } + throw new Error('Impossible de se connecter à la base de données'); +} + +async function runMigrations() { + const { rows } = await db.query( + `SELECT EXISTS ( + SELECT FROM information_schema.tables WHERE table_name = 'companies' + ) AS exists` + ); + if (rows[0].exists) { + console.log('Schéma déjà initialisé.'); + return; + } + const sqlPath = path.join(__dirname, '..', 'migrations', '001_init.sql'); + const sql = fs.readFileSync(sqlPath, 'utf8'); + console.log('Initialisation du schéma de base de données...'); + await db.query(sql); + console.log('Schéma créé.'); +} + +async function seedAdmin() { + const { rows } = await db.query( + "SELECT id FROM users WHERE role = 'admin' LIMIT 1" + ); + if (rows.length > 0) return; + + const email = process.env.ADMIN_EMAIL || 'admin@presencia.local'; + const password = process.env.ADMIN_PASSWORD || 'ChangeMe123!'; + const fullName = process.env.ADMIN_NAME || 'Administrateur'; + + const hash = await bcrypt.hash(password, 10); + await db.query( + `INSERT INTO users (full_name, email, password_hash, role, company_id, active) + VALUES ($1, $2, $3, 'admin', NULL, true)`, + [fullName, email, hash] + ); + console.log('========================================================'); + console.log(' Compte administrateur créé :'); + console.log(` Email : ${email}`); + console.log(` Mot de passe : ${password}`); + console.log(' Merci de le changer après la première connexion.'); + console.log('========================================================'); +} + +async function bootstrap() { + await waitForDb(); + await runMigrations(); + await seedAdmin(); +} + +module.exports = bootstrap; diff --git a/backend/src/db.js b/backend/src/db.js new file mode 100644 index 0000000..434eb7d --- /dev/null +++ b/backend/src/db.js @@ -0,0 +1,14 @@ +const { Pool } = require('pg'); + +const pool = new Pool({ + host: process.env.PGHOST || 'db', + port: parseInt(process.env.PGPORT || '5432', 10), + user: process.env.PGUSER || 'presencia', + password: process.env.PGPASSWORD || 'presencia', + database: process.env.PGDATABASE || 'presencia', +}); + +module.exports = { + query: (text, params) => pool.query(text, params), + pool, +}; diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000..ec8aec7 --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,46 @@ +require('dotenv').config(); +const express = require('express'); +const cookieParser = require('cookie-parser'); +const cors = require('cors'); + +const bootstrap = require('./bootstrap'); +const authRoutes = require('./routes/auth'); +const companyRoutes = require('./routes/companies'); +const userRoutes = require('./routes/users'); +const attendanceRoutes = require('./routes/attendance'); +const validationRoutes = require('./routes/validations'); +const exportRoutes = require('./routes/export'); + +const app = express(); +const PORT = process.env.PORT || 4790; + +app.use(express.json()); +app.use(cookieParser()); +if (process.env.CORS_ORIGIN) { + app.use(cors({ origin: process.env.CORS_ORIGIN, credentials: true })); +} + +app.get('/api/health', (req, res) => res.json({ ok: true })); + +app.use('/api/auth', authRoutes); +app.use('/api/companies', companyRoutes); +app.use('/api/users', userRoutes); +app.use('/api/attendance', attendanceRoutes); +app.use('/api/validations', validationRoutes); +app.use('/api/export', exportRoutes); + +app.use((err, req, res, next) => { + console.error(err); + res.status(500).json({ error: 'Erreur interne du serveur' }); +}); + +bootstrap() + .then(() => { + app.listen(PORT, () => { + console.log(`Presencia API démarrée sur le port ${PORT}`); + }); + }) + .catch((err) => { + console.error('Échec du démarrage :', err); + process.exit(1); + }); diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js new file mode 100644 index 0000000..7225991 --- /dev/null +++ b/backend/src/middleware/auth.js @@ -0,0 +1,60 @@ +const jwt = require('jsonwebtoken'); + +const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-me'; +const COOKIE_NAME = 'presencia_token'; + +function signToken(user) { + return jwt.sign( + { + id: user.id, + role: user.role, + companyId: user.company_id, + fullName: user.full_name, + email: user.email, + }, + JWT_SECRET, + { expiresIn: '12h' } + ); +} + +function setAuthCookie(res, token) { + res.cookie(COOKIE_NAME, token, { + httpOnly: true, + sameSite: 'lax', + secure: process.env.COOKIE_SECURE === 'true', + maxAge: 12 * 60 * 60 * 1000, + path: '/', + }); +} + +function clearAuthCookie(res) { + res.clearCookie(COOKIE_NAME, { path: '/' }); +} + +function requireAuth(req, res, next) { + const token = req.cookies && req.cookies[COOKIE_NAME]; + if (!token) return res.status(401).json({ error: 'Non authentifié' }); + try { + const payload = jwt.verify(token, JWT_SECRET); + req.user = payload; + next(); + } catch (err) { + return res.status(401).json({ error: 'Session invalide ou expirée' }); + } +} + +function requireAdmin(req, res, next) { + if (!req.user || req.user.role !== 'admin') { + return res.status(403).json({ error: 'Accès réservé aux administrateurs' }); + } + next(); +} + +module.exports = { + signToken, + setAuthCookie, + clearAuthCookie, + requireAuth, + requireAdmin, + COOKIE_NAME, +}; diff --git a/backend/src/routes/attendance.js b/backend/src/routes/attendance.js new file mode 100644 index 0000000..97b5ecd --- /dev/null +++ b/backend/src/routes/attendance.js @@ -0,0 +1,151 @@ +const express = require('express'); +const db = require('../db'); +const { requireAuth } = require('../middleware/auth'); + +const router = express.Router(); +router.use(requireAuth); + +const STATUSES = ['present', 'absent', 'conge', 'rtt']; +const PERIODS = ['AM', 'PM']; + +async function getTargetUser(req, requestedUserId) { + // Cadre can only ever act on themselves. Admin may act on any user. + if (req.user.role === 'admin' && requestedUserId) { + const { rows } = await db.query( + 'SELECT id, role, company_id FROM users WHERE id = $1', + [requestedUserId] + ); + return rows[0] || null; + } + return { id: req.user.id, role: req.user.role, company_id: req.user.companyId }; +} + +async function getMonthLock(userId, year, month) { + const { rows } = await db.query( + 'SELECT cadre_validated, cadre_validated_at FROM month_locks WHERE user_id = $1 AND year = $2 AND month = $3', + [userId, year, month] + ); + return rows[0] || { cadre_validated: false, cadre_validated_at: null }; +} + +async function getCompanyValidation(companyId, year, month) { + if (!companyId) return { admin_validated: false, admin_validated_at: null }; + const { rows } = await db.query( + 'SELECT admin_validated, admin_validated_at FROM company_month_validations WHERE company_id = $1 AND year = $2 AND month = $3', + [companyId, year, month] + ); + return rows[0] || { admin_validated: false, admin_validated_at: null }; +} + +router.get('/', async (req, res) => { + const year = parseInt(req.query.year, 10); + const month = parseInt(req.query.month, 10); + if (!year || !month) return res.status(400).json({ error: 'year et month requis' }); + + const requestedUserId = req.query.user_id; + if (requestedUserId && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Accès refusé' }); + } + const target = await getTargetUser(req, requestedUserId); + if (!target) return res.status(404).json({ error: 'Utilisateur introuvable' }); + + const start = `${year}-${String(month).padStart(2, '0')}-01`; + const { rows } = await db.query( + `SELECT entry_date, period, status + FROM attendance_entries + WHERE user_id = $1 + AND entry_date >= $2::date + AND entry_date < ($2::date + INTERVAL '1 month') + ORDER BY entry_date, period`, + [target.id, start] + ); + + const lock = await getMonthLock(target.id, year, month); + const companyValidation = await getCompanyValidation(target.company_id, year, month); + + res.json({ + userId: target.id, + entries: rows.map((r) => ({ + date: r.entry_date.toISOString().slice(0, 10), + period: r.period, + status: r.status, + })), + cadreValidated: lock.cadre_validated, + cadreValidatedAt: lock.cadre_validated_at, + companyValidated: companyValidation.admin_validated, + companyValidatedAt: companyValidation.admin_validated_at, + editable: + req.user.role === 'admin' + ? !companyValidation.admin_validated + : !lock.cadre_validated && !companyValidation.admin_validated, + }); +}); + +router.put('/', async (req, res) => { + const { date, period, status, user_id: requestedUserId } = req.body || {}; + if (!date || !PERIODS.includes(period) || !STATUSES.includes(status)) { + return res.status(400).json({ error: 'Paramètres invalides' }); + } + if (requestedUserId && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Accès refusé' }); + } + const target = await getTargetUser(req, requestedUserId); + if (!target) return res.status(404).json({ error: 'Utilisateur introuvable' }); + + const d = new Date(date + 'T00:00:00Z'); + const year = d.getUTCFullYear(); + const month = d.getUTCMonth() + 1; + + const lock = await getMonthLock(target.id, year, month); + const companyValidation = await getCompanyValidation(target.company_id, year, month); + + if (companyValidation.admin_validated) { + return res.status(423).json({ error: 'Ce mois a été validé par l’administrateur et est verrouillé' }); + } + if (req.user.role !== 'admin' && lock.cadre_validated) { + return res.status(423).json({ error: 'Vous avez déjà validé ce mois. Contactez un administrateur pour le modifier.' }); + } + + await db.query( + `INSERT INTO attendance_entries (user_id, entry_date, period, status, updated_at) + VALUES ($1, $2, $3, $4, now()) + ON CONFLICT (user_id, entry_date, period) + DO UPDATE SET status = EXCLUDED.status, updated_at = now()`, + [target.id, date, period, status] + ); + + res.json({ ok: true }); +}); + +router.delete('/', async (req, res) => { + const { date, period, user_id: requestedUserId } = req.body || {}; + if (!date || !PERIODS.includes(period)) { + return res.status(400).json({ error: 'Paramètres invalides' }); + } + if (requestedUserId && req.user.role !== 'admin') { + return res.status(403).json({ error: 'Accès refusé' }); + } + const target = await getTargetUser(req, requestedUserId); + if (!target) return res.status(404).json({ error: 'Utilisateur introuvable' }); + + const d = new Date(date + 'T00:00:00Z'); + const year = d.getUTCFullYear(); + const month = d.getUTCMonth() + 1; + const lock = await getMonthLock(target.id, year, month); + const companyValidation = await getCompanyValidation(target.company_id, year, month); + + if (companyValidation.admin_validated) { + return res.status(423).json({ error: 'Ce mois a été validé par l’administrateur et est verrouillé' }); + } + if (req.user.role !== 'admin' && lock.cadre_validated) { + return res.status(423).json({ error: 'Vous avez déjà validé ce mois. Contactez un administrateur pour le modifier.' }); + } + + await db.query( + 'DELETE FROM attendance_entries WHERE user_id = $1 AND entry_date = $2 AND period = $3', + [target.id, date, period] + ); + res.json({ ok: true }); +}); + +module.exports = router; diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js new file mode 100644 index 0000000..1b14935 --- /dev/null +++ b/backend/src/routes/auth.js @@ -0,0 +1,66 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const db = require('../db'); +const { signToken, setAuthCookie, clearAuthCookie, requireAuth } = require('../middleware/auth'); + +const router = express.Router(); + +router.post('/login', async (req, res) => { + const { email, password } = req.body || {}; + if (!email || !password) { + return res.status(400).json({ error: 'Email et mot de passe requis' }); + } + const { rows } = await db.query( + `SELECT u.id, u.full_name, u.email, u.password_hash, u.role, u.active, + u.company_id, c.name AS company_name + FROM users u + LEFT JOIN companies c ON c.id = u.company_id + WHERE lower(u.email) = lower($1)`, + [email] + ); + const user = rows[0]; + if (!user || !user.active) { + return res.status(401).json({ error: 'Identifiants incorrects' }); + } + const ok = await bcrypt.compare(password, user.password_hash); + if (!ok) { + return res.status(401).json({ error: 'Identifiants incorrects' }); + } + const token = signToken(user); + setAuthCookie(res, token); + res.json({ + id: user.id, + fullName: user.full_name, + email: user.email, + role: user.role, + companyId: user.company_id, + companyName: user.company_name, + }); +}); + +router.post('/logout', (req, res) => { + clearAuthCookie(res); + res.json({ ok: true }); +}); + +router.get('/me', requireAuth, async (req, res) => { + const { rows } = await db.query( + `SELECT u.id, u.full_name, u.email, u.role, u.company_id, c.name AS company_name + FROM users u + LEFT JOIN companies c ON c.id = u.company_id + WHERE u.id = $1`, + [req.user.id] + ); + if (!rows[0]) return res.status(401).json({ error: 'Non authentifié' }); + const u = rows[0]; + res.json({ + id: u.id, + fullName: u.full_name, + email: u.email, + role: u.role, + companyId: u.company_id, + companyName: u.company_name, + }); +}); + +module.exports = router; diff --git a/backend/src/routes/companies.js b/backend/src/routes/companies.js new file mode 100644 index 0000000..55c6ac9 --- /dev/null +++ b/backend/src/routes/companies.js @@ -0,0 +1,57 @@ +const express = require('express'); +const db = require('../db'); +const { requireAuth, requireAdmin } = require('../middleware/auth'); + +const router = express.Router(); +router.use(requireAuth, requireAdmin); + +router.get('/', async (req, res) => { + const { rows } = await db.query( + `SELECT c.id, c.name, c.created_at, + COUNT(u.id) FILTER (WHERE u.role = 'cadre' AND u.active) AS active_cadre_count + FROM companies c + LEFT JOIN users u ON u.company_id = c.id + GROUP BY c.id + ORDER BY c.name` + ); + res.json(rows); +}); + +router.post('/', async (req, res) => { + const { name } = req.body || {}; + if (!name || !name.trim()) return res.status(400).json({ error: 'Nom requis' }); + try { + const { rows } = await db.query( + 'INSERT INTO companies (name) VALUES ($1) RETURNING id, name, created_at', + [name.trim()] + ); + res.status(201).json(rows[0]); + } catch (err) { + if (err.code === '23505') return res.status(409).json({ error: 'Cette société existe déjà' }); + throw err; + } +}); + +router.put('/:id', async (req, res) => { + const { name } = req.body || {}; + if (!name || !name.trim()) return res.status(400).json({ error: 'Nom requis' }); + try { + const { rows } = await db.query( + 'UPDATE companies SET name = $1 WHERE id = $2 RETURNING id, name, created_at', + [name.trim(), req.params.id] + ); + if (!rows[0]) return res.status(404).json({ error: 'Société introuvable' }); + res.json(rows[0]); + } catch (err) { + if (err.code === '23505') return res.status(409).json({ error: 'Cette société existe déjà' }); + throw err; + } +}); + +router.delete('/:id', async (req, res) => { + const { rowCount } = await db.query('DELETE FROM companies WHERE id = $1', [req.params.id]); + if (!rowCount) return res.status(404).json({ error: 'Société introuvable' }); + res.json({ ok: true }); +}); + +module.exports = router; diff --git a/backend/src/routes/export.js b/backend/src/routes/export.js new file mode 100644 index 0000000..5616c03 --- /dev/null +++ b/backend/src/routes/export.js @@ -0,0 +1,81 @@ +const express = require('express'); +const db = require('../db'); +const { requireAuth, requireAdmin } = require('../middleware/auth'); +const { buildCompanyWorkbook } = require('../utils/excel'); +const { buildCompanyPdf } = require('../utils/pdf'); + +const router = express.Router(); +router.use(requireAuth, requireAdmin); + +async function loadCompanyData(companyId, year, month) { + const { rows: companyRows } = await db.query('SELECT id, name FROM companies WHERE id = $1', [companyId]); + const company = companyRows[0]; + if (!company) return null; + + const { rows: cadreRows } = await db.query( + `SELECT id, full_name FROM users WHERE company_id = $1 AND role = 'cadre' AND active = true ORDER BY full_name`, + [companyId] + ); + + const start = `${year}-${String(month).padStart(2, '0')}-01`; + const cadres = []; + for (const c of cadreRows) { + const { rows: entries } = await db.query( + `SELECT entry_date, period, status FROM attendance_entries + WHERE user_id = $1 AND entry_date >= $2::date AND entry_date < ($2::date + INTERVAL '1 month')`, + [c.id, start] + ); + const map = new Map(); + for (const e of entries) { + map.set(`${e.entry_date.toISOString().slice(0, 10)}:${e.period}`, e.status); + } + cadres.push({ id: c.id, fullName: c.full_name, entries: map }); + } + + return { company, cadres }; +} + +router.get('/excel/:companyId', async (req, res) => { + const year = parseInt(req.query.year, 10); + const month = parseInt(req.query.month, 10); + if (!year || !month) return res.status(400).json({ error: 'year et month requis' }); + + const data = await loadCompanyData(req.params.companyId, year, month); + if (!data) return res.status(404).json({ error: 'Société introuvable' }); + + const workbook = await buildCompanyWorkbook({ + companyName: data.company.name, + year, + month, + cadres: data.cadres, + }); + + const filename = `presences_${data.company.name.replace(/\s+/g, '_')}_${year}-${String(month).padStart(2, '0')}.xlsx`; + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + await workbook.xlsx.write(res); + res.end(); +}); + +router.get('/pdf/:companyId', async (req, res) => { + const year = parseInt(req.query.year, 10); + const month = parseInt(req.query.month, 10); + if (!year || !month) return res.status(400).json({ error: 'year et month requis' }); + + const data = await loadCompanyData(req.params.companyId, year, month); + if (!data) return res.status(404).json({ error: 'Société introuvable' }); + + const filename = `presences_${data.company.name.replace(/\s+/g, '_')}_${year}-${String(month).padStart(2, '0')}.pdf`; + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + + const doc = buildCompanyPdf({ + companyName: data.company.name, + year, + month, + cadres: data.cadres, + }); + doc.pipe(res); +}); + +module.exports = router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js new file mode 100644 index 0000000..eab574c --- /dev/null +++ b/backend/src/routes/users.js @@ -0,0 +1,111 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const db = require('../db'); +const { requireAuth, requireAdmin } = require('../middleware/auth'); + +const router = express.Router(); +router.use(requireAuth, requireAdmin); + +router.get('/', async (req, res) => { + const { company_id } = req.query; + const params = []; + let where = ''; + if (company_id) { + params.push(company_id); + where = `WHERE u.company_id = $${params.length}`; + } + const { rows } = await db.query( + `SELECT u.id, u.full_name, u.email, u.role, u.company_id, u.active, u.created_at, + c.name AS company_name + FROM users u + LEFT JOIN companies c ON c.id = u.company_id + ${where} + ORDER BY u.full_name`, + params + ); + res.json(rows); +}); + +router.post('/', async (req, res) => { + const { full_name, email, password, role, company_id } = req.body || {}; + if (!full_name || !email || !password || !role) { + return res.status(400).json({ error: 'Champs requis manquants' }); + } + if (!['admin', 'cadre'].includes(role)) { + return res.status(400).json({ error: 'Rôle invalide' }); + } + if (role === 'cadre' && !company_id) { + return res.status(400).json({ error: 'Une société doit être attribuée au cadre' }); + } + try { + const hash = await bcrypt.hash(password, 10); + const { rows } = await db.query( + `INSERT INTO users (full_name, email, password_hash, role, company_id, active) + VALUES ($1, $2, $3, $4, $5, true) + RETURNING id, full_name, email, role, company_id, active, created_at`, + [full_name.trim(), email.trim().toLowerCase(), hash, role, role === 'admin' ? null : company_id] + ); + res.status(201).json(rows[0]); + } catch (err) { + if (err.code === '23505') return res.status(409).json({ error: 'Cet email existe déjà' }); + throw err; + } +}); + +router.put('/:id', async (req, res) => { + const { full_name, email, role, company_id, active } = req.body || {}; + if (!full_name || !email || !role) { + return res.status(400).json({ error: 'Champs requis manquants' }); + } + if (!['admin', 'cadre'].includes(role)) { + return res.status(400).json({ error: 'Rôle invalide' }); + } + if (role === 'cadre' && !company_id) { + return res.status(400).json({ error: 'Une société doit être attribuée au cadre' }); + } + try { + const { rows } = await db.query( + `UPDATE users SET full_name = $1, email = $2, role = $3, company_id = $4, active = $5 + WHERE id = $6 + RETURNING id, full_name, email, role, company_id, active, created_at`, + [ + full_name.trim(), + email.trim().toLowerCase(), + role, + role === 'admin' ? null : company_id, + active !== false, + req.params.id, + ] + ); + if (!rows[0]) return res.status(404).json({ error: 'Utilisateur introuvable' }); + res.json(rows[0]); + } catch (err) { + if (err.code === '23505') return res.status(409).json({ error: 'Cet email existe déjà' }); + throw err; + } +}); + +router.put('/:id/password', async (req, res) => { + const { password } = req.body || {}; + if (!password || password.length < 6) { + return res.status(400).json({ error: 'Mot de passe trop court (6 caractères minimum)' }); + } + const hash = await bcrypt.hash(password, 10); + const { rowCount } = await db.query('UPDATE users SET password_hash = $1 WHERE id = $2', [ + hash, + req.params.id, + ]); + if (!rowCount) return res.status(404).json({ error: 'Utilisateur introuvable' }); + res.json({ ok: true }); +}); + +router.delete('/:id', async (req, res) => { + if (String(req.user.id) === String(req.params.id)) { + return res.status(400).json({ error: 'Vous ne pouvez pas supprimer votre propre compte' }); + } + const { rowCount } = await db.query('DELETE FROM users WHERE id = $1', [req.params.id]); + if (!rowCount) return res.status(404).json({ error: 'Utilisateur introuvable' }); + res.json({ ok: true }); +}); + +module.exports = router; diff --git a/backend/src/routes/validations.js b/backend/src/routes/validations.js new file mode 100644 index 0000000..ba4dc14 --- /dev/null +++ b/backend/src/routes/validations.js @@ -0,0 +1,146 @@ +const express = require('express'); +const db = require('../db'); +const { requireAuth, requireAdmin } = require('../middleware/auth'); + +const router = express.Router(); +router.use(requireAuth); + +function parseYearMonth(req) { + const year = parseInt(req.body?.year ?? req.query?.year, 10); + const month = parseInt(req.body?.month ?? req.query?.month, 10); + if (!year || !month || month < 1 || month > 12) return null; + return { year, month }; +} + +// Cadre validates their own month. +router.post('/cadre', async (req, res) => { + const ym = parseYearMonth(req); + if (!ym) return res.status(400).json({ error: 'year/month invalides' }); + + // Admin can validate on behalf of a cadre (e.g. corrections), otherwise self only. + const targetUserId = req.user.role === 'admin' && req.body.user_id ? req.body.user_id : req.user.id; + + const { rows: userRows } = await db.query('SELECT id, company_id FROM users WHERE id = $1', [targetUserId]); + const target = userRows[0]; + if (!target) return res.status(404).json({ error: 'Utilisateur introuvable' }); + + const { rows: cv } = await db.query( + 'SELECT admin_validated FROM company_month_validations WHERE company_id = $1 AND year = $2 AND month = $3', + [target.company_id, ym.year, ym.month] + ); + if (cv[0]?.admin_validated) { + return res.status(423).json({ error: 'La société a déjà été validée par l’administrateur pour ce mois' }); + } + + await db.query( + `INSERT INTO month_locks (user_id, year, month, cadre_validated, cadre_validated_at) + VALUES ($1, $2, $3, true, now()) + ON CONFLICT (user_id, year, month) + DO UPDATE SET cadre_validated = true, cadre_validated_at = now()`, + [target.id, ym.year, ym.month] + ); + res.json({ ok: true }); +}); + +// Admin reopens a cadre's month (allows corrections again). +router.post('/cadre/reopen', requireAdmin, async (req, res) => { + const ym = parseYearMonth(req); + const { user_id } = req.body || {}; + if (!ym || !user_id) return res.status(400).json({ error: 'Paramètres invalides' }); + + await db.query( + `INSERT INTO month_locks (user_id, year, month, cadre_validated, cadre_validated_at) + VALUES ($1, $2, $3, false, NULL) + ON CONFLICT (user_id, year, month) + DO UPDATE SET cadre_validated = false, cadre_validated_at = NULL`, + [user_id, ym.year, ym.month] + ); + res.json({ ok: true }); +}); + +// Admin: status of every cadre in a company for a given month. +router.get('/company/:companyId', requireAdmin, async (req, res) => { + const year = parseInt(req.query.year, 10); + const month = parseInt(req.query.month, 10); + const companyId = req.params.companyId; + if (!year || !month) return res.status(400).json({ error: 'year et month requis' }); + + const { rows: cadres } = await db.query( + `SELECT u.id, u.full_name, u.email, + ml.cadre_validated, ml.cadre_validated_at + FROM users u + LEFT JOIN month_locks ml ON ml.user_id = u.id AND ml.year = $2 AND ml.month = $3 + WHERE u.company_id = $1 AND u.role = 'cadre' AND u.active = true + ORDER BY u.full_name`, + [companyId, year, month] + ); + + const { rows: cv } = await db.query( + `SELECT admin_validated, admin_validated_at, admin_validated_by + FROM company_month_validations WHERE company_id = $1 AND year = $2 AND month = $3`, + [companyId, year, month] + ); + + res.json({ + cadres: cadres.map((c) => ({ + id: c.id, + fullName: c.full_name, + email: c.email, + cadreValidated: !!c.cadre_validated, + cadreValidatedAt: c.cadre_validated_at, + })), + allCadresValidated: cadres.length > 0 && cadres.every((c) => c.cadre_validated), + companyValidated: !!cv[0]?.admin_validated, + companyValidatedAt: cv[0]?.admin_validated_at || null, + }); +}); + +// Admin validates the whole company for the month (requires every cadre validated). +router.post('/company/:companyId', requireAdmin, async (req, res) => { + const ym = parseYearMonth(req); + const companyId = req.params.companyId; + if (!ym) return res.status(400).json({ error: 'year/month invalides' }); + + const { rows: cadres } = await db.query( + `SELECT u.id, u.full_name, ml.cadre_validated + FROM users u + LEFT JOIN month_locks ml ON ml.user_id = u.id AND ml.year = $2 AND ml.month = $3 + WHERE u.company_id = $1 AND u.role = 'cadre' AND u.active = true`, + [companyId, ym.year, ym.month] + ); + + const pending = cadres.filter((c) => !c.cadre_validated); + if (pending.length > 0) { + return res.status(409).json({ + error: 'Tous les cadres doivent valider leur mois avant validation de la société', + pending: pending.map((c) => c.full_name), + }); + } + + await db.query( + `INSERT INTO company_month_validations (company_id, year, month, admin_validated, admin_validated_at, admin_validated_by) + VALUES ($1, $2, $3, true, now(), $4) + ON CONFLICT (company_id, year, month) + DO UPDATE SET admin_validated = true, admin_validated_at = now(), admin_validated_by = $4`, + [companyId, ym.year, ym.month, req.user.id] + ); + res.json({ ok: true }); +}); + +// Admin reopens a company's month validation. +router.post('/company/:companyId/reopen', requireAdmin, async (req, res) => { + const ym = parseYearMonth(req); + const companyId = req.params.companyId; + if (!ym) return res.status(400).json({ error: 'year/month invalides' }); + + await db.query( + `INSERT INTO company_month_validations (company_id, year, month, admin_validated, admin_validated_at, admin_validated_by) + VALUES ($1, $2, $3, false, NULL, NULL) + ON CONFLICT (company_id, year, month) + DO UPDATE SET admin_validated = false, admin_validated_at = NULL, admin_validated_by = NULL`, + [companyId, ym.year, ym.month] + ); + res.json({ ok: true }); +}); + +module.exports = router; diff --git a/backend/src/utils/excel.js b/backend/src/utils/excel.js new file mode 100644 index 0000000..70b4634 --- /dev/null +++ b/backend/src/utils/excel.js @@ -0,0 +1,84 @@ +const ExcelJS = require('exceljs'); +const { STATUS_CODES, STATUS_COLORS, daysInMonth } = require('./status'); + +const MONTH_NAMES = [ + 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', + 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre', +]; + +/** + * cadres: [{ id, fullName, entries: Map('YYYY-MM-DD:AM'|'PM' -> status) }] + */ +async function buildCompanyWorkbook({ companyName, year, month, cadres }) { + const workbook = new ExcelJS.Workbook(); + workbook.creator = 'Presencia'; + workbook.created = new Date(); + + const sheet = workbook.addWorksheet('Synthèse', { + views: [{ state: 'frozen', xSplit: 1, ySplit: 3 }], + }); + + const nbDays = daysInMonth(year, month); + const titleRow = sheet.addRow([`Présences — ${companyName} — ${MONTH_NAMES[month - 1]} ${year}`]); + sheet.mergeCells(1, 1, 1, 1 + nbDays * 2); + titleRow.font = { bold: true, size: 14 }; + titleRow.height = 22; + + const dayHeaderRow = sheet.getRow(2); + dayHeaderRow.getCell(1).value = 'Cadre'; + for (let d = 1; d <= nbDays; d++) { + const col = 2 + (d - 1) * 2; + dayHeaderRow.getCell(col).value = d; + sheet.mergeCells(2, col, 2, col + 1); + dayHeaderRow.getCell(col).alignment = { horizontal: 'center' }; + dayHeaderRow.getCell(col).font = { bold: true }; + } + + const periodHeaderRow = sheet.getRow(3); + periodHeaderRow.getCell(1).value = ''; + for (let d = 1; d <= nbDays; d++) { + const col = 2 + (d - 1) * 2; + periodHeaderRow.getCell(col).value = 'AM'; + periodHeaderRow.getCell(col + 1).value = 'PM'; + periodHeaderRow.getCell(col).font = { italic: true, size: 9 }; + periodHeaderRow.getCell(col + 1).font = { italic: true, size: 9 }; + periodHeaderRow.getCell(col).alignment = { horizontal: 'center' }; + periodHeaderRow.getCell(col + 1).alignment = { horizontal: 'center' }; + } + + sheet.getColumn(1).width = 26; + for (let d = 1; d <= nbDays; d++) { + sheet.getColumn(2 + (d - 1) * 2).width = 5; + sheet.getColumn(3 + (d - 1) * 2).width = 5; + } + + for (const cadre of cadres) { + const row = sheet.addRow([cadre.fullName]); + for (let d = 1; d <= nbDays; d++) { + const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(d).padStart(2, '0')}`; + const col = 2 + (d - 1) * 2; + ['AM', 'PM'].forEach((period, idx) => { + const status = cadre.entries.get(`${dateStr}:${period}`); + const cell = row.getCell(col + idx); + cell.alignment = { horizontal: 'center' }; + if (status) { + cell.value = STATUS_CODES[status]; + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: STATUS_COLORS[status] }, + }; + cell.font = { color: { argb: 'FFFFFFFF' }, bold: true, size: 9 }; + } + }); + } + } + + const legendRowIdx = sheet.rowCount + 2; + sheet.getCell(legendRowIdx, 1).value = 'Légende : P = Présent · A = Absent · CP = Congé · RTT = RTT'; + sheet.getCell(legendRowIdx, 1).font = { italic: true, size: 9 }; + + return workbook; +} + +module.exports = { buildCompanyWorkbook, MONTH_NAMES }; diff --git a/backend/src/utils/pdf.js b/backend/src/utils/pdf.js new file mode 100644 index 0000000..194e9bb --- /dev/null +++ b/backend/src/utils/pdf.js @@ -0,0 +1,94 @@ +const PDFDocument = require('pdfkit'); +const { STATUS_LABELS, daysInMonth } = require('./status'); +const { MONTH_NAMES } = require('./excel'); + +const STATUS_HEX = { + present: '#2E7D32', + absent: '#C62828', + conge: '#EF6C00', + rtt: '#1565C0', +}; + +/** + * cadres: [{ id, fullName, entries: Map('YYYY-MM-DD:AM'|'PM' -> status) }] + * Returns a PDFDocument stream (caller pipes it to res). + */ +function buildCompanyPdf({ companyName, year, month, cadres }) { + const doc = new PDFDocument({ margin: 40, size: 'A4' }); + const nbDays = daysInMonth(year, month); + + doc.fontSize(18).fillColor('#111').text('Presencia — Récapitulatif de présences', { align: 'left' }); + doc.moveDown(0.2); + doc.fontSize(12).fillColor('#444').text(`${companyName} — ${MONTH_NAMES[month - 1]} ${year}`); + doc.moveDown(1); + + cadres.forEach((cadre, idx) => { + if (idx > 0) doc.addPage(); + else doc.moveDown(0.5); + + doc.fontSize(14).fillColor('#111').text(cadre.fullName, { underline: true }); + doc.moveDown(0.5); + + const counts = { present: 0, absent: 0, conge: 0, rtt: 0 }; + + const startX = doc.x; + let y = doc.y; + const rowHeight = 16; + const colDay = 30; + const colAM = 90; + const colPM = 90; + + doc.fontSize(9).fillColor('#fff'); + doc.rect(startX, y, colDay + colAM + colPM, rowHeight).fill('#333'); + doc.fillColor('#fff').text('Jour', startX + 4, y + 4, { width: colDay - 8 }); + doc.text('Matin (AM)', startX + colDay + 4, y + 4, { width: colAM - 8 }); + doc.text('Après-midi (PM)', startX + colDay + colAM + 4, y + 4, { width: colPM - 8 }); + y += rowHeight; + + for (let d = 1; d <= nbDays; d++) { + const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(d).padStart(2, '0')}`; + const am = cadre.entries.get(`${dateStr}:AM`); + const pm = cadre.entries.get(`${dateStr}:PM`); + if (am) counts[am]++; + if (pm) counts[pm]++; + + if (y > 760) { + doc.addPage(); + y = 40; + } + + doc.rect(startX, y, colDay + colAM + colPM, rowHeight).strokeColor('#ddd').stroke(); + doc.fillColor('#111').fontSize(9).text(String(d), startX + 4, y + 4, { width: colDay - 8 }); + + doc.fillColor(am ? STATUS_HEX[am] : '#999').text( + am ? STATUS_LABELS[am] : '—', + startX + colDay + 4, + y + 4, + { width: colAM - 8 } + ); + doc.fillColor(pm ? STATUS_HEX[pm] : '#999').text( + pm ? STATUS_LABELS[pm] : '—', + startX + colDay + colAM + 4, + y + 4, + { width: colPM - 8 } + ); + y += rowHeight; + } + + y += 10; + if (y > 740) { + doc.addPage(); + y = 40; + } + doc.fontSize(10).fillColor('#111').text( + `Total demi-journées — Présent: ${counts.present} · Absent: ${counts.absent} · Congé: ${counts.conge} · RTT: ${counts.rtt}`, + startX, + y + ); + }); + + doc.end(); + return doc; +} + +module.exports = { buildCompanyPdf }; diff --git a/backend/src/utils/status.js b/backend/src/utils/status.js new file mode 100644 index 0000000..a4cb011 --- /dev/null +++ b/backend/src/utils/status.js @@ -0,0 +1,26 @@ +const STATUS_LABELS = { + present: 'Présent', + absent: 'Absent', + conge: 'Congé', + rtt: 'RTT', +}; + +const STATUS_CODES = { + present: 'P', + absent: 'A', + conge: 'CP', + rtt: 'RTT', +}; + +const STATUS_COLORS = { + present: 'FF2E7D32', + absent: 'FFC62828', + conge: 'FFEF6C00', + rtt: 'FF1565C0', +}; + +function daysInMonth(year, month) { + return new Date(year, month, 0).getDate(); +} + +module.exports = { STATUS_LABELS, STATUS_CODES, STATUS_COLORS, daysInMonth }; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..daaf249 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +services: + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-presencia} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-presencia} + POSTGRES_DB: ${POSTGRES_DB:-presencia} + volumes: + - presencia_pgdata:/var/lib/postgresql/data + ports: + - "${DB_HOST_PORT:-6543}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-presencia}"] + interval: 5s + timeout: 5s + retries: 10 + + backend: + build: ./backend + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + PORT: 4790 + PGHOST: db + PGPORT: 5432 + PGUSER: ${POSTGRES_USER:-presencia} + PGPASSWORD: ${POSTGRES_PASSWORD:-presencia} + PGDATABASE: ${POSTGRES_DB:-presencia} + JWT_SECRET: ${JWT_SECRET:-change-me-to-a-long-random-string} + COOKIE_SECURE: ${COOKIE_SECURE:-false} + ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@presencia.local} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-ChangeMe123!} + ADMIN_NAME: ${ADMIN_NAME:-Administrateur} + ports: + - "${BACKEND_HOST_PORT:-4790}:4790" + + frontend: + build: ./frontend + restart: unless-stopped + depends_on: + - backend + ports: + - "${FRONTEND_HOST_PORT:-8781}:80" + +volumes: + presencia_pgdata: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..93dca21 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,6 @@ +FROM nginx:1.27-alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY public /usr/share/nginx/html + +EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..fa2e011 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:4790/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/frontend/public/css/style.css b/frontend/public/css/style.css new file mode 100644 index 0000000..8f76808 --- /dev/null +++ b/frontend/public/css/style.css @@ -0,0 +1,338 @@ +:root { + --color-primary: #3949ab; + --color-primary-dark: #2c3593; + --color-primary-light: #e8eaf6; + --color-bg: #f4f5f9; + --color-surface: #ffffff; + --color-border: #e2e4ec; + --color-text: #1c1f2a; + --color-muted: #6b7080; + + --color-present: #2e7d32; + --color-present-bg: #e6f4ea; + --color-absent: #c62828; + --color-absent-bg: #fbe9e9; + --color-conge: #ef6c00; + --color-conge-bg: #fdf0e2; + --color-rtt: #1565c0; + --color-rtt-bg: #e6eefb; + --color-empty: #c7c9d3; + + --radius: 10px; + --shadow: 0 1px 3px rgba(20, 22, 40, 0.08), 0 1px 2px rgba(20, 22, 40, 0.06); + --shadow-lg: 0 12px 32px rgba(20, 22, 40, 0.16); +} + +* { box-sizing: border-box; } + +/* Any element toggled via the `hidden` attribute must stay hidden even if a + class on it (e.g. display:flex) would otherwise override the UA default. */ +[hidden] { display: none !important; } + +html, body { + margin: 0; + padding: 0; + height: 100%; + background: var(--color-bg); + color: var(--color-text); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +button { font-family: inherit; cursor: pointer; } +input, select { font-family: inherit; } + +.brand { display: flex; align-items: center; gap: 8px; } +.brand-mark { + color: var(--color-primary); + font-size: 22px; +} +.brand-name { + font-weight: 700; + font-size: 20px; + letter-spacing: -0.02em; + color: var(--color-text); +} + +/* ---------- Login ---------- */ +.login-screen { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #3949ab 0%, #1a237e 100%); + padding: 20px; +} +.login-card { + background: var(--color-surface); + border-radius: 16px; + box-shadow: var(--shadow-lg); + padding: 40px 36px; + width: 100%; + max-width: 380px; +} +.login-card .brand { justify-content: center; margin-bottom: 6px; } +.login-subtitle { + text-align: center; + color: var(--color-muted); + margin: 0 0 28px; + font-size: 14px; +} +.field { display: block; margin-bottom: 16px; } +.field span { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; color: var(--color-text); } +.field input { + width: 100%; + padding: 11px 12px; + border: 1px solid var(--color-border); + border-radius: 8px; + font-size: 14px; + background: #fbfbfd; + transition: border-color .15s, box-shadow .15s; +} +.field input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-primary-light); + background: #fff; +} +.login-hint { text-align: center; color: var(--color-muted); font-size: 12px; margin: 20px 0 0; } +.form-error { + background: var(--color-absent-bg); + color: var(--color-absent); + font-size: 13px; + padding: 10px 12px; + border-radius: 8px; + margin-bottom: 14px; +} + +/* ---------- Buttons ---------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + border: none; + border-radius: 8px; + padding: 10px 16px; + font-size: 14px; + font-weight: 600; + transition: background .15s, box-shadow .15s, color .15s, opacity .15s; +} +.btn-primary { background: var(--color-primary); color: #fff; } +.btn-primary:hover { background: var(--color-primary-dark); } +.btn-primary:disabled { opacity: .5; cursor: not-allowed; } +.btn-outline { background: #fff; color: var(--color-primary); border: 1px solid var(--color-primary); } +.btn-outline:hover { background: var(--color-primary-light); } +.btn-outline:disabled { opacity: .4; cursor: not-allowed; } +.btn-ghost { background: transparent; color: var(--color-muted); } +.btn-ghost:hover { background: #eceef4; color: var(--color-text); } +.btn-danger { background: #fff; color: var(--color-absent); border: 1px solid var(--color-absent); } +.btn-danger:hover { background: var(--color-absent-bg); } +.btn-sm { padding: 6px 12px; font-size: 13px; } +.btn-block { width: 100%; } +.icon-btn { + border: none; background: #eef0f6; color: var(--color-text); + width: 32px; height: 32px; border-radius: 8px; font-size: 18px; line-height: 1; +} +.icon-btn:hover { background: var(--color-primary-light); color: var(--color-primary); } + +/* ---------- App shell ---------- */ +.app { min-height: 100vh; display: flex; flex-direction: column; } +.topbar { + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + padding: 12px 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + flex-wrap: wrap; +} +.topbar-nav { display: flex; gap: 4px; flex-wrap: wrap; } +.nav-btn { + border: none; background: transparent; padding: 8px 14px; border-radius: 8px; + font-size: 14px; font-weight: 600; color: var(--color-muted); +} +.nav-btn:hover { background: #eef0f6; color: var(--color-text); } +.nav-btn.active { background: var(--color-primary-light); color: var(--color-primary); } + +.topbar-user { display: flex; align-items: center; gap: 12px; } +.user-chip { text-align: right; } +.user-name { display: block; font-weight: 600; font-size: 14px; } +.user-role-badge { + display: inline-block; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; + color: var(--color-primary); font-weight: 700; +} + +.main { flex: 1; padding: 28px 32px 60px; max-width: 1180px; width: 100%; margin: 0 auto; } +.view-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; } +.view-header h1 { font-size: 22px; margin: 0; } +h1 { font-size: 22px; margin: 0 0 4px; } +h2 { font-size: 17px; margin: 0 0 4px; } +.muted { color: var(--color-muted); font-size: 13px; margin: 0; } + +.card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 20px; + margin-bottom: 20px; +} +.selector-card { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; } +.selector-card label { font-size: 13px; font-weight: 600; color: var(--color-text); display: flex; flex-direction: column; gap: 6px; } +.selector-card select { + padding: 9px 10px; border-radius: 8px; border: 1px solid var(--color-border); font-size: 14px; min-width: 180px; +} + +/* ---------- Calendar ---------- */ +.calendar-header { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; } +.calendar-title-group h1 { margin-bottom: 8px; } +.month-nav { display: flex; align-items: center; gap: 12px; } +.month-label { font-size: 16px; font-weight: 700; min-width: 160px; text-align: center; text-transform: capitalize; } + +.validation-banner { + padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; + background: #eef0f6; color: var(--color-muted); max-width: 360px; +} +.validation-banner.locked { background: var(--color-rtt-bg); color: var(--color-rtt); } +.validation-banner.validated { background: var(--color-present-bg); color: var(--color-present); } + +.legend { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: 16px; font-size: 13px; color: var(--color-muted); } +.legend-item { display: flex; align-items: center; gap: 6px; } +.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; } +.dot-present { background: var(--color-present); } +.dot-absent { background: var(--color-absent); } +.dot-conge { background: var(--color-conge); } +.dot-rtt { background: var(--color-rtt); } +.dot-empty { background: var(--color-empty); } + +.calendar-weekdays { + display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; + font-size: 12px; font-weight: 700; color: var(--color-muted); text-transform: uppercase; + padding: 0 2px; margin-bottom: 6px; +} +.calendar-grid { + display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; +} +.day-cell { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 10px; + min-height: 84px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} +.day-cell.empty { background: transparent; border: none; } +.day-cell.weekend { background: #fafafc; } +.day-number { font-size: 13px; font-weight: 700; color: var(--color-text); } +.half-day-row { display: flex; gap: 4px; flex: 1; } +.half-day { + flex: 1; + border-radius: 6px; + border: 1px dashed var(--color-border); + background: #fafbfd; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + color: var(--color-muted); + display: flex; + align-items: center; + justify-content: center; + min-height: 34px; + transition: transform .1s, box-shadow .1s; +} +.half-day:not(.readonly):hover { transform: translateY(-1px); box-shadow: var(--shadow); } +.half-day.readonly { cursor: default; } +.half-day.st-present { background: var(--color-present-bg); color: var(--color-present); border-style: solid; border-color: transparent; } +.half-day.st-absent { background: var(--color-absent-bg); color: var(--color-absent); border-style: solid; border-color: transparent; } +.half-day.st-conge { background: var(--color-conge-bg); color: var(--color-conge); border-style: solid; border-color: transparent; } +.half-day.st-rtt { background: var(--color-rtt-bg); color: var(--color-rtt); border-style: solid; border-color: transparent; } + +.calendar-actions { margin-top: 22px; display: flex; justify-content: flex-end; } + +/* ---------- Tables ---------- */ +.data-table { width: 100%; border-collapse: collapse; font-size: 14px; } +.data-table th { + text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; + color: var(--color-muted); padding: 10px 12px; border-bottom: 2px solid var(--color-border); +} +.data-table td { padding: 12px; border-bottom: 1px solid var(--color-border); vertical-align: middle; } +.data-table tr:last-child td { border-bottom: none; } +.filter-row { margin-bottom: 14px; } +.filter-row select { padding: 7px 10px; border-radius: 8px; border: 1px solid var(--color-border); } + +.badge { display: inline-block; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; } +.badge-admin { background: var(--color-primary-light); color: var(--color-primary); } +.badge-cadre { background: #eef0f6; color: var(--color-muted); } +.badge-active { background: var(--color-present-bg); color: var(--color-present); } +.badge-inactive { background: var(--color-absent-bg); color: var(--color-absent); } +.badge-validated { background: var(--color-present-bg); color: var(--color-present); } +.badge-pending { background: #fdf0e2; color: var(--color-conge); } + +.row-actions { display: flex; gap: 8px; } + +/* ---------- Validation summary ---------- */ +.validation-summary-header { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; } +.validation-summary-actions { display: flex; gap: 10px; } +.export-row { display: flex; align-items: center; gap: 12px; margin-top: 18px; font-size: 14px; font-weight: 600; color: var(--color-muted); } + +/* ---------- Popover ---------- */ +.popover { + position: absolute; + background: var(--color-surface); + border-radius: 10px; + box-shadow: var(--shadow-lg); + border: 1px solid var(--color-border); + padding: 8px; + z-index: 60; + min-width: 160px; +} +.popover-title { font-size: 12px; font-weight: 700; color: var(--color-muted); padding: 4px 8px 8px; } +.popover-option { + display: flex; align-items: center; gap: 8px; width: 100%; + border: none; background: transparent; padding: 9px 8px; border-radius: 6px; + font-size: 14px; text-align: left; font-weight: 600; color: var(--color-text); +} +.popover-option:hover { background: #eef0f6; } +.popover-clear { color: var(--color-muted); } + +/* ---------- Modal ---------- */ +.modal-backdrop { + position: fixed; inset: 0; background: rgba(20, 22, 40, .45); + display: flex; align-items: center; justify-content: center; z-index: 100; padding: 20px; +} +.modal { + background: var(--color-surface); border-radius: 14px; box-shadow: var(--shadow-lg); + padding: 26px; width: 100%; max-width: 420px; +} +.modal h2 { margin-bottom: 18px; } +.modal .field { margin-bottom: 14px; } +.modal .field input, .modal .field select { + width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--color-border); font-size: 14px; +} +.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } + +/* ---------- Toasts ---------- */ +.toast-container { + position: fixed; top: 18px; right: 18px; z-index: 200; + display: flex; flex-direction: column; gap: 10px; +} +.toast { + background: var(--color-text); color: #fff; padding: 12px 18px; border-radius: 8px; + font-size: 14px; box-shadow: var(--shadow-lg); max-width: 320px; opacity: 0; transform: translateY(-8px); + animation: toast-in .2s forwards; +} +.toast.toast-error { background: var(--color-absent); } +.toast.toast-success { background: var(--color-present); } +@keyframes toast-in { to { opacity: 1; transform: translateY(0); } } + +.view[hidden] { display: none; } + +@media (max-width: 720px) { + .main { padding: 20px 14px 50px; } + .day-cell { min-height: 70px; } + .half-day { font-size: 9px; min-height: 28px; } + .selector-card { flex-direction: column; align-items: stretch; } +} diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..a2749c3 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,200 @@ + + + + + +Presencia — Gestion des présences + + + + +
+ + +
+ +
+ + + + + + + + + + + + + diff --git a/frontend/public/js/app.js b/frontend/public/js/app.js new file mode 100644 index 0000000..1d8343c --- /dev/null +++ b/frontend/public/js/app.js @@ -0,0 +1,740 @@ +(() => { + 'use strict'; + + const MONTH_NAMES = [ + 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', + 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre', + ]; + const STATUS_LABELS = { present: 'Présent', absent: 'Absent', conge: 'Congé', rtt: 'RTT' }; + + const state = { + user: null, + companies: [], + cal: { year: null, month: null, targetUserId: null, targetUserName: null, data: null }, + currentView: 'calendar', + }; + + // ---------- API helper ---------- + async function api(path, options = {}) { + const res = await fetch(`/api${path}`, { + method: options.method || 'GET', + headers: options.body ? { 'Content-Type': 'application/json' } : undefined, + body: options.body ? JSON.stringify(options.body) : undefined, + credentials: 'include', + }); + let data = null; + try { data = await res.json(); } catch (e) { /* no body */ } + if (!res.ok) { + const err = new Error((data && data.error) || `Erreur ${res.status}`); + err.status = res.status; + err.payload = data; + throw err; + } + return data; + } + + function toast(message, type = 'info') { + const container = document.getElementById('toast-container'); + const el = document.createElement('div'); + el.className = `toast toast-${type}`; + el.textContent = message; + container.appendChild(el); + setTimeout(() => el.remove(), 4000); + } + + function fmtDate(iso) { + if (!iso) return ''; + const d = new Date(iso); + return d.toLocaleDateString('fr-FR') + ' à ' + d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); + } + + // ---------- Auth ---------- + async function checkSession() { + try { + const me = await api('/auth/me'); + onLogin(me); + } catch (e) { + showLogin(); + } + } + + function showLogin() { + document.getElementById('view-login').hidden = false; + document.getElementById('app').hidden = true; + } + + async function onLogin(user) { + state.user = user; + document.getElementById('view-login').hidden = true; + document.getElementById('app').hidden = false; + document.getElementById('user-name').textContent = user.fullName; + document.getElementById('user-role').textContent = user.role === 'admin' ? 'Administrateur' : (user.companyName || 'Cadre'); + + const now = new Date(); + state.cal.year = now.getFullYear(); + state.cal.month = now.getMonth() + 1; + state.cal.targetUserId = null; + state.cal.targetUserName = null; + + if (user.role === 'admin') { + document.getElementById('admin-nav').hidden = false; + await loadCompaniesForSelects(); + switchView('companies'); + } else { + document.getElementById('admin-nav').hidden = true; + switchView('calendar'); + await loadCalendar(); + } + } + + document.getElementById('login-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const email = document.getElementById('login-email').value.trim(); + const password = document.getElementById('login-password').value; + const errBox = document.getElementById('login-error'); + errBox.hidden = true; + try { + const user = await api('/auth/login', { method: 'POST', body: { email, password } }); + await onLogin(user); + } catch (err) { + errBox.textContent = err.message || 'Connexion impossible'; + errBox.hidden = false; + } + }); + + document.getElementById('logout-btn').addEventListener('click', async () => { + await api('/auth/logout', { method: 'POST' }); + state.user = null; + location.reload(); + }); + + // ---------- View switching ---------- + const VIEWS = ['calendar', 'companies', 'users', 'plannings', 'validation']; + function switchView(name) { + state.currentView = name; + VIEWS.forEach((v) => { + document.getElementById(`view-${v}`).hidden = v !== name; + }); + document.querySelectorAll('.nav-btn').forEach((btn) => { + btn.classList.toggle('active', btn.dataset.view === name); + }); + if (name === 'companies') loadCompaniesView(); + if (name === 'users') loadUsersView(); + if (name === 'plannings') loadPlanningsView(); + if (name === 'validation') loadValidationSelectors(); + } + + document.getElementById('admin-nav').addEventListener('click', (e) => { + const btn = e.target.closest('.nav-btn'); + if (!btn) return; + if (btn.dataset.view === 'calendar') { + state.cal.targetUserId = null; + state.cal.targetUserName = null; + document.getElementById('calendar-owner-name').textContent = 'Mon planning'; + document.getElementById('calendar-actions').hidden = false; + switchView('calendar'); + loadCalendar(); + return; + } + switchView(btn.dataset.view); + }); + + // ================= CALENDAR ================= + const monthLabel = document.getElementById('month-label'); + const calGrid = document.getElementById('calendar-grid'); + const validationBanner = document.getElementById('validation-banner'); + const validateBtn = document.getElementById('validate-month-btn'); + + document.getElementById('prev-month').addEventListener('click', () => shiftMonth(-1)); + document.getElementById('next-month').addEventListener('click', () => shiftMonth(1)); + + function shiftMonth(delta) { + let { year, month } = state.cal; + month += delta; + if (month < 1) { month = 12; year -= 1; } + if (month > 12) { month = 1; year += 1; } + state.cal.year = year; + state.cal.month = month; + loadCalendar(); + } + + async function loadCalendar() { + const { year, month, targetUserId } = state.cal; + monthLabel.textContent = `${MONTH_NAMES[month - 1]} ${year}`; + const qs = new URLSearchParams({ year, month }); + if (targetUserId) qs.set('user_id', targetUserId); + try { + const data = await api(`/attendance?${qs.toString()}`); + state.cal.data = data; + renderCalendar(data); + } catch (err) { + toast(err.message, 'error'); + } + } + + function renderCalendar(data) { + const { year, month, targetUserId } = state.cal; + const entryMap = new Map(); + data.entries.forEach((e) => entryMap.set(`${e.date}:${e.period}`, e.status)); + + // banner + const isAdmin = state.user.role === 'admin'; + validationBanner.className = 'validation-banner'; + if (data.companyValidated) { + validationBanner.classList.add('validated'); + validationBanner.textContent = `Société validée par l'administrateur le ${fmtDate(data.companyValidatedAt)} — verrouillé`; + } else if (data.cadreValidated) { + validationBanner.classList.add('locked'); + validationBanner.textContent = `Mois validé le ${fmtDate(data.cadreValidatedAt)} — en attente de validation société`; + } else { + validationBanner.textContent = 'Mois non validé'; + } + + // validate button only shown on own calendar (cadre role, not admin browsing someone else) + const actionsBox = document.getElementById('calendar-actions'); + if (!isAdmin && !targetUserId) { + actionsBox.hidden = false; + validateBtn.disabled = data.cadreValidated || data.companyValidated; + validateBtn.textContent = data.cadreValidated ? 'Mois déjà validé' : 'Valider mon mois'; + } else { + actionsBox.hidden = true; + } + + const firstOfMonth = new Date(year, month - 1, 1); + const daysInMonth = new Date(year, month, 0).getDate(); + let firstWeekday = firstOfMonth.getDay(); // 0=Sun + firstWeekday = firstWeekday === 0 ? 7 : firstWeekday; // 1=Mon..7=Sun + const leading = firstWeekday - 1; + + calGrid.innerHTML = ''; + for (let i = 0; i < leading; i++) { + const empty = document.createElement('div'); + empty.className = 'day-cell empty'; + calGrid.appendChild(empty); + } + + const editable = data.editable; + for (let d = 1; d <= daysInMonth; d++) { + const dateObj = new Date(year, month - 1, d); + const iso = `${year}-${String(month).padStart(2, '0')}-${String(d).padStart(2, '0')}`; + const isWeekend = dateObj.getDay() === 0 || dateObj.getDay() === 6; + + const cell = document.createElement('div'); + cell.className = 'day-cell' + (isWeekend ? ' weekend' : ''); + + const num = document.createElement('div'); + num.className = 'day-number'; + num.textContent = d; + cell.appendChild(num); + + const row = document.createElement('div'); + row.className = 'half-day-row'; + + ['AM', 'PM'].forEach((period) => { + const status = entryMap.get(`${iso}:${period}`); + const half = document.createElement('button'); + half.type = 'button'; + half.className = 'half-day' + (status ? ` st-${status}` : '') + (editable ? '' : ' readonly'); + half.textContent = period; + half.dataset.date = iso; + half.dataset.period = period; + half.dataset.status = status || ''; + if (!editable) half.disabled = true; + row.appendChild(half); + }); + + cell.appendChild(row); + calGrid.appendChild(cell); + } + } + + // ---- Half-day popover ---- + const popover = document.getElementById('status-popover'); + let popoverTarget = null; + + calGrid.addEventListener('click', (e) => { + const half = e.target.closest('.half-day'); + if (!half || half.disabled) return; + popoverTarget = half; + const label = half.dataset.period === 'AM' ? 'Matin' : 'Après-midi'; + document.getElementById('popover-title').textContent = `${half.dataset.date} — ${label}`; + const rect = half.getBoundingClientRect(); + popover.style.top = `${window.scrollY + rect.bottom + 6}px`; + popover.style.left = `${window.scrollX + rect.left}px`; + popover.hidden = false; + }); + + document.addEventListener('click', (e) => { + if (popover.hidden) return; + if (e.target.closest('.popover') || e.target.closest('.half-day')) return; + popover.hidden = true; + }); + + popover.addEventListener('click', async (e) => { + const opt = e.target.closest('.popover-option'); + if (!opt || !popoverTarget) return; + const status = opt.dataset.status; + const { date, period } = popoverTarget.dataset; + popover.hidden = true; + try { + if (status) { + const body = { date, period, status }; + if (state.cal.targetUserId) body.user_id = state.cal.targetUserId; + await api('/attendance', { method: 'PUT', body }); + } else { + const body = { date, period }; + if (state.cal.targetUserId) body.user_id = state.cal.targetUserId; + await api('/attendance', { method: 'DELETE', body }); + } + await loadCalendar(); + } catch (err) { + toast(err.message, 'error'); + } + }); + + validateBtn.addEventListener('click', async () => { + if (!confirm('Confirmer la validation de votre mois ? Vous ne pourrez plus le modifier ensuite.')) return; + try { + await api('/validations/cadre', { method: 'POST', body: { year: state.cal.year, month: state.cal.month } }); + toast('Mois validé avec succès', 'success'); + await loadCalendar(); + } catch (err) { + toast(err.message, 'error'); + } + }); + + // ================= COMPANIES (admin) ================= + async function loadCompaniesForSelects() { + try { + state.companies = await api('/companies'); + } catch (err) { + toast(err.message, 'error'); + } + } + + async function loadCompaniesView() { + await loadCompaniesForSelects(); + const tbody = document.getElementById('companies-tbody'); + tbody.innerHTML = ''; + state.companies.forEach((c) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${escapeHtml(c.name)} + ${c.active_cadre_count} + ${new Date(c.created_at).toLocaleDateString('fr-FR')} + + + + `; + tbody.appendChild(tr); + }); + } + + document.getElementById('companies-tbody').addEventListener('click', async (e) => { + const btn = e.target.closest('button'); + if (!btn) return; + const id = btn.dataset.id; + const company = state.companies.find((c) => String(c.id) === id); + if (btn.dataset.action === 'edit') openCompanyModal(company); + if (btn.dataset.action === 'delete') { + if (!confirm(`Supprimer la société "${company.name}" ? Les cadres associés perdront leur société.`)) return; + try { + await api(`/companies/${id}`, { method: 'DELETE' }); + toast('Société supprimée', 'success'); + loadCompaniesView(); + } catch (err) { + toast(err.message, 'error'); + } + } + }); + + document.getElementById('add-company-btn').addEventListener('click', () => openCompanyModal(null)); + + function openCompanyModal(company) { + const isEdit = !!company; + openModal(` +

${isEdit ? 'Modifier la société' : 'Nouvelle société'}

+
+ + +
+ `); + document.getElementById('company-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const name = document.getElementById('company-name').value.trim(); + try { + if (isEdit) await api(`/companies/${company.id}`, { method: 'PUT', body: { name } }); + else await api('/companies', { method: 'POST', body: { name } }); + closeModal(); + toast('Société enregistrée', 'success'); + loadCompaniesView(); + } catch (err) { + toast(err.message, 'error'); + } + }); + } + + // ================= USERS (admin) ================= + async function loadUsersView() { + if (!state.companies.length) await loadCompaniesForSelects(); + const companySelect = document.getElementById('users-filter-company'); + companySelect.innerHTML = '' + + state.companies.map((c) => ``).join(''); + await refreshUsersTable(); + } + + document.getElementById('users-filter-company').addEventListener('change', refreshUsersTable); + + async function refreshUsersTable() { + const companyId = document.getElementById('users-filter-company').value; + const qs = companyId ? `?company_id=${companyId}` : ''; + try { + const users = await api(`/users${qs}`); + state.users = users; + const tbody = document.getElementById('users-tbody'); + tbody.innerHTML = ''; + users.forEach((u) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${escapeHtml(u.full_name)} + ${escapeHtml(u.email)} + ${u.role === 'admin' ? 'Admin' : 'Cadre'} + ${u.company_name ? escapeHtml(u.company_name) : '—'} + ${u.active ? 'Actif' : 'Inactif'} + + + + + `; + tbody.appendChild(tr); + }); + } catch (err) { + toast(err.message, 'error'); + } + } + + document.getElementById('users-tbody').addEventListener('click', async (e) => { + const btn = e.target.closest('button'); + if (!btn) return; + const id = btn.dataset.id; + const user = state.users.find((u) => String(u.id) === id); + if (btn.dataset.action === 'edit') openUserModal(user); + if (btn.dataset.action === 'password') openPasswordModal(user); + if (btn.dataset.action === 'delete') { + if (!confirm(`Supprimer l'utilisateur "${user.full_name}" ?`)) return; + try { + await api(`/users/${id}`, { method: 'DELETE' }); + toast('Utilisateur supprimé', 'success'); + refreshUsersTable(); + } catch (err) { + toast(err.message, 'error'); + } + } + }); + + document.getElementById('add-user-btn').addEventListener('click', () => openUserModal(null)); + + function companyOptions(selectedId) { + return state.companies.map((c) => + `` + ).join(''); + } + + function openUserModal(user) { + const isEdit = !!user; + openModal(` +

${isEdit ? 'Modifier l\'utilisateur' : 'Nouvel utilisateur'}

+
+ + + ${!isEdit ? ` + ` : ''} + + + ${isEdit ? ` + ` : ''} + +
+ `); + + const roleSelect = document.getElementById('user-role'); + const companyField = document.getElementById('user-company-field'); + function toggleCompanyField() { + companyField.style.display = roleSelect.value === 'admin' ? 'none' : 'block'; + } + roleSelect.addEventListener('change', toggleCompanyField); + toggleCompanyField(); + + document.getElementById('user-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const body = { + full_name: document.getElementById('user-fullname').value.trim(), + email: document.getElementById('user-email').value.trim(), + role: roleSelect.value, + company_id: roleSelect.value === 'admin' ? null : document.getElementById('user-company').value, + }; + if (!isEdit) body.password = document.getElementById('user-password').value; + if (isEdit) body.active = document.getElementById('user-active').value === 'true'; + try { + if (isEdit) await api(`/users/${user.id}`, { method: 'PUT', body }); + else await api('/users', { method: 'POST', body }); + closeModal(); + toast('Utilisateur enregistré', 'success'); + refreshUsersTable(); + } catch (err) { + toast(err.message, 'error'); + } + }); + } + + function openPasswordModal(user) { + openModal(` +

Nouveau mot de passe

+

Pour ${escapeHtml(user.full_name)}

+
+ + +
+ `); + document.getElementById('password-form').addEventListener('submit', async (e) => { + e.preventDefault(); + const password = document.getElementById('new-password').value; + try { + await api(`/users/${user.id}/password`, { method: 'PUT', body: { password } }); + closeModal(); + toast('Mot de passe mis à jour', 'success'); + } catch (err) { + toast(err.message, 'error'); + } + }); + } + + // ================= PLANNINGS (admin browse) ================= + async function loadPlanningsView() { + if (!state.companies.length) await loadCompaniesForSelects(); + const companySelect = document.getElementById('plannings-company-select'); + companySelect.innerHTML = state.companies.map((c) => ``).join(''); + await refreshPlanningsCadres(); + } + + document.getElementById('plannings-company-select').addEventListener('change', refreshPlanningsCadres); + + async function refreshPlanningsCadres() { + const companyId = document.getElementById('plannings-company-select').value; + const cadreSelect = document.getElementById('plannings-cadre-select'); + if (!companyId) { cadreSelect.innerHTML = ''; return; } + try { + const users = await api(`/users?company_id=${companyId}`); + const cadres = users.filter((u) => u.role === 'cadre'); + cadreSelect.innerHTML = cadres.map((u) => ``).join('') || + ''; + } catch (err) { + toast(err.message, 'error'); + } + } + + document.getElementById('plannings-view-btn').addEventListener('click', async () => { + const cadreSelect = document.getElementById('plannings-cadre-select'); + const userId = cadreSelect.value; + if (!userId) { toast('Sélectionnez un cadre', 'error'); return; } + const name = cadreSelect.options[cadreSelect.selectedIndex].textContent; + state.cal.targetUserId = userId; + state.cal.targetUserName = name; + document.getElementById('calendar-owner-name').textContent = `Planning de ${name}`; + switchView('calendar'); + await loadCalendar(); + }); + + // ================= VALIDATION & EXPORT (admin) ================= + function loadValidationSelectors() { + if (!state.companies.length) return loadCompaniesForSelects().then(loadValidationSelectors); + const companySelect = document.getElementById('validation-company-select'); + if (!companySelect.dataset.filled) { + companySelect.innerHTML = state.companies.map((c) => ``).join(''); + companySelect.dataset.filled = '1'; + } + const monthSelect = document.getElementById('validation-month-select'); + if (!monthSelect.dataset.filled) { + monthSelect.innerHTML = MONTH_NAMES.map((m, i) => ``).join(''); + monthSelect.value = new Date().getMonth() + 1; + monthSelect.dataset.filled = '1'; + } + const yearSelect = document.getElementById('validation-year-select'); + if (!yearSelect.dataset.filled) { + const currentYear = new Date().getFullYear(); + const years = []; + for (let y = currentYear - 2; y <= currentYear + 1; y++) years.push(y); + yearSelect.innerHTML = years.map((y) => ``).join(''); + yearSelect.value = currentYear; + yearSelect.dataset.filled = '1'; + } + } + + document.getElementById('validation-load-btn').addEventListener('click', loadValidationSummary); + + let currentValidationCompanyId = null; + let currentValidationYear = null; + let currentValidationMonth = null; + + async function loadValidationSummary() { + const companyId = document.getElementById('validation-company-select').value; + const year = document.getElementById('validation-year-select').value; + const month = document.getElementById('validation-month-select').value; + currentValidationCompanyId = companyId; + currentValidationYear = year; + currentValidationMonth = month; + + try { + const data = await api(`/validations/company/${companyId}?year=${year}&month=${month}`); + const company = state.companies.find((c) => String(c.id) === companyId); + document.getElementById('validation-summary-card').hidden = false; + document.getElementById('validation-company-title').textContent = + `${company ? company.name : ''} — ${MONTH_NAMES[month - 1]} ${year}`; + + const statusText = document.getElementById('validation-status-text'); + if (data.companyValidated) { + statusText.textContent = `Société validée le ${fmtDate(data.companyValidatedAt)}`; + } else if (data.allCadresValidated) { + statusText.textContent = 'Tous les cadres ont validé — prêt pour validation société'; + } else { + const pending = data.cadres.filter((c) => !c.cadreValidated).map((c) => c.fullName); + statusText.textContent = pending.length + ? `En attente de validation : ${pending.join(', ')}` + : 'Aucun cadre actif dans cette société'; + } + + document.getElementById('validate-company-btn').disabled = data.companyValidated || !data.allCadresValidated; + document.getElementById('reopen-company-btn').disabled = !data.companyValidated; + + const tbody = document.getElementById('validation-cadres-tbody'); + tbody.innerHTML = ''; + data.cadres.forEach((c) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${escapeHtml(c.fullName)} + ${c.cadreValidated ? 'Validé' : 'En attente'} + ${c.cadreValidatedAt ? fmtDate(c.cadreValidatedAt) : '—'} + + + `; + tbody.appendChild(tr); + }); + } catch (err) { + toast(err.message, 'error'); + } + } + + document.getElementById('validation-cadres-tbody').addEventListener('click', async (e) => { + const btn = e.target.closest('button[data-action="reopen-cadre"]'); + if (!btn) return; + if (!confirm('Réouvrir le mois de ce cadre pour permettre des corrections ?')) return; + try { + await api('/validations/cadre/reopen', { + method: 'POST', + body: { user_id: btn.dataset.id, year: currentValidationYear, month: currentValidationMonth }, + }); + toast('Mois réouvert pour ce cadre', 'success'); + loadValidationSummary(); + } catch (err) { + toast(err.message, 'error'); + } + }); + + document.getElementById('validate-company-btn').addEventListener('click', async () => { + if (!confirm('Valider définitivement cette société pour ce mois ? Les plannings seront verrouillés.')) return; + try { + await api(`/validations/company/${currentValidationCompanyId}`, { + method: 'POST', + body: { year: currentValidationYear, month: currentValidationMonth }, + }); + toast('Société validée', 'success'); + loadValidationSummary(); + } catch (err) { + toast(err.message, 'error'); + } + }); + + document.getElementById('reopen-company-btn').addEventListener('click', async () => { + if (!confirm('Réouvrir la validation de cette société pour ce mois ?')) return; + try { + await api(`/validations/company/${currentValidationCompanyId}/reopen`, { + method: 'POST', + body: { year: currentValidationYear, month: currentValidationMonth }, + }); + toast('Validation société réouverte', 'success'); + loadValidationSummary(); + } catch (err) { + toast(err.message, 'error'); + } + }); + + document.getElementById('export-pdf-btn').addEventListener('click', () => downloadExport('pdf')); + document.getElementById('export-excel-btn').addEventListener('click', () => downloadExport('excel')); + + function downloadExport(kind) { + if (!currentValidationCompanyId) return; + const url = `/api/export/${kind}/${currentValidationCompanyId}?year=${currentValidationYear}&month=${currentValidationMonth}`; + const a = document.createElement('a'); + a.href = url; + a.download = ''; + document.body.appendChild(a); + a.click(); + a.remove(); + } + + // ================= Modal helpers ================= + const modalBackdrop = document.getElementById('modal-backdrop'); + const modalBox = document.getElementById('modal-box'); + + function openModal(html) { + modalBox.innerHTML = html; + modalBackdrop.hidden = false; + } + function closeModal() { + modalBackdrop.hidden = true; + modalBox.innerHTML = ''; + } + modalBackdrop.addEventListener('click', (e) => { + if (e.target === modalBackdrop || e.target.closest('[data-close]')) closeModal(); + }); + + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str == null ? '' : String(str); + return div.innerHTML; + } + + // ---------- Boot ---------- + checkSession(); +})();