From 8d5f73fc9c8bac42223ddc28659ba59b32cd36b4 Mon Sep 17 00:00:00 2001 From: Anilzer Date: Wed, 3 Jun 2026 17:36:02 +0200 Subject: [PATCH 1/5] =?UTF-8?q?Initialisation=20du=20backend=20de=20Th?= =?UTF-8?q?=C3=A9=20Tip=20Top?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 5 + .gitignore | 5 + README.md | 347 +++ package-lock.json | 2230 +++++++++++++++++ package.json | 32 + .../20260603130023_init/migration.sql | 104 + .../migration.sql | 15 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 111 + prisma/seed.js | 51 + src/app.js | 38 + src/config/prisma.js | 5 + src/controllers/admin.controller.js | 292 +++ src/controllers/auth.controller.js | 134 + src/controllers/employee.controller.js | 115 + src/controllers/game.controller.js | 120 + src/docs/swagger.js | 29 + src/middlewares/auth.middleware.js | 44 + src/middlewares/role.middleware.js | 13 + src/middlewares/validate.middleware.js | 17 + src/routes/admin.routes.js | 114 + src/routes/auth.routes.js | 69 + src/routes/employee.routes.js | 76 + src/routes/game.routes.js | 41 + src/scripts/generateTickets.js | 67 + src/validations/admin.validation.js | 14 + src/validations/auth.validation.js | 21 + src/validations/game.validation.js | 13 + 28 files changed, 4125 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prisma/migrations/20260603130023_init/migration.sql create mode 100644 prisma/migrations/20260603144928_add_grand_prize_winner/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.js create mode 100644 src/app.js create mode 100644 src/config/prisma.js create mode 100644 src/controllers/admin.controller.js create mode 100644 src/controllers/auth.controller.js create mode 100644 src/controllers/employee.controller.js create mode 100644 src/controllers/game.controller.js create mode 100644 src/docs/swagger.js create mode 100644 src/middlewares/auth.middleware.js create mode 100644 src/middlewares/role.middleware.js create mode 100644 src/middlewares/validate.middleware.js create mode 100644 src/routes/admin.routes.js create mode 100644 src/routes/auth.routes.js create mode 100644 src/routes/employee.routes.js create mode 100644 src/routes/game.routes.js create mode 100644 src/scripts/generateTickets.js create mode 100644 src/validations/admin.validation.js create mode 100644 src/validations/auth.validation.js create mode 100644 src/validations/game.validation.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3cc202e --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +PORT=5000 + +DATABASE_URL="postgresql://postgres:password@localhost:5432/the_tip_top_db" + +JWT_SECRET="your_secret_key" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24361b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +.prisma +coverage/ +dist/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..18e6160 --- /dev/null +++ b/README.md @@ -0,0 +1,347 @@ +# Thé Tip Top – Backend API + +## Présentation + +Ce projet correspond au backend du jeu-concours organisé par **Thé Tip Top** à l'occasion de l'ouverture de sa 10ᵉ boutique à Nice. + +L'application permet aux clients de participer à un jeu-concours via un code présent sur leur ticket de caisse, de consulter leurs gains et de réclamer leurs lots en boutique. + +Le backend expose une **API** **REST** sécurisée utilisée par : + +- l'application client (participants) +- l'espace employé en boutique +- l'espace administrateur +- les futurs services externes de l'écosystème Thé Tip Top + +--- + +## Fonctionnalités + +### Authentification + +- Inscription utilisateur +- Connexion sécurisée +- Authentification **JWT** +- Gestion des rôles (**CLIENT**, **EMPLOYEE**, **ADMIN**) +- Hachage des mots de passe avec Bcrypt +- Validation des données avec **Zod** + +### Jeu-concours + +- Génération automatique des lots +- Génération des tickets gagnants +- Participation via code ticket unique +- Attribution automatique des gains +- Historique des participations +- Historique des gains remportés + +### Espace Employé + +- Recherche d'un gain via un code ticket +- Vérification du statut du gain +- Validation de la remise d'un lot +- Mise à jour automatique du statut du gain + +### Espace Administrateur + +- Tableau de bord statistique +- Liste des participants +- Liste des contacts ayant accepté les communications marketing +- Création de comptes employés +- Tirage au sort du grand gagnant +- Consultation du gagnant du gros lot + +### Documentation API + +- Documentation interactive avec **Swagger UI** +- Génération automatique de la documentation via **Swagger JSDoc** +- Tests des endpoints directement depuis le navigateur + +--- + +## Architecture Technique + +### Backend + +- Node.js +- Express.js + +### Base de données + +- PostgreSQL +- **Prisma **ORM** 6** +- Prisma Client 6 +- Prisma Migrate +- Prisma Studio + +### Validation des données + +- **Zod** +- Middleware de validation personnalisé + +### Sécurité + +- **JWT** (**JSON** Web Token) +- Bcrypt +- Gestion des rôles et permissions +- Validation des entrées utilisateur + +### Documentation + +- Swagger UI +- Swagger JSDoc +- OpenAPI 3.0 + +--- + +## Installation + +### Cloner le projet + +```bash git clone cd the-tip-top-backend ``` + +### Installer les dépendances + +```bash npm install ``` + +### Variables d'environnement + +Créer un fichier `.env` à la racine du projet : + +```env **PORT**=**5000** + +DATABASE_URL="postgresql://postgres:password@localhost:**5432**/the_tip_top_db* + +JWT_SECRET=*your_secret_key" ``` + +--- + +## Base de données + +### Générer les migrations + +```bash npx prisma migrate dev ``` + +### Générer Prisma Client + +```bash npx prisma generate ``` + +### Exécuter le seed de la base + +```bash npx prisma db seed ``` + +### Générer les tickets du jeu-concours + +```bash npm run generate:tickets ``` + +### Ouvrir Prisma Studio + +```bash npx prisma studio ``` + +--- + +## Technologies utilisées + +| Technologie | Version | +| ----------- | ---------------- | +| Node.js | 20+ | +| Express.js | 5 | +| PostgreSQL | 16+ | +| Prisma ORM | 6 | +| JWT | Dernière version | +| Bcrypt | Dernière version | +| Zod | Dernière version | +| Swagger UI | Dernière version | + +--- + +## Lancement du projet + +### Développement + +```bash npm run dev ``` + +Serveur : + +```txt [http://localhost:**5000**](http://localhost:**5000**) ``` + +--- + +## Documentation API + +La documentation Swagger est accessible à l'adresse : + +```txt [http://localhost:**5000**/api-docs](http://localhost:**5000**/api-docs) ``` + +Fonctionnalités disponibles : + +- Consultation de toutes les routes +- Visualisation des schémas de données +- Test des endpoints +- Authentification via **JWT** avec le bouton **Authorize** + +--- + +## Gestion des rôles + +### CLIENT + +Peut : + +- Participer au jeu-concours +- Consulter ses gains +- Consulter son profil + +### EMPLOYEE + +Peut : + +- Rechercher un gain +- Vérifier les informations du gagnant +- Valider la remise d'un lot + +### ADMIN + +Peut : + +- Accéder aux statistiques +- Créer des employés +- Gérer les campagnes emailing +- Effectuer le tirage au sort final +- Consulter le gagnant du gros lot + +--- + +## Routes API + +### Auth + +| Méthode | Route | +| ------- | ------------------ | +| POST | /api/auth/register | +| POST | /api/auth/login | +| GET | /api/auth/me | + +### Game + +| Méthode | Route | +| ------- | --------------------- | +| POST | /api/game/participate | +| GET | /api/game/my-gains | + +### Employee + +| Méthode | Route | +| ------- | ---------------------------- | +| GET | /api/employee/ticket/ | +| PATCH | /api/employee/claim//deliver | + +### Admin + +| Méthode | Route | +| ------- | -------------------- | +| GET | /api/admin/stats | +| GET | /api/admin/users | +| GET | /api/admin/emailing | +| POST | /api/admin/employees | +| POST | /api/admin/draw | +| GET | /api/admin/draw | + +--- + +## Validation avec Zod + +Toutes les données reçues par l'**API** sont validées avant traitement grâce à **Zod**. + +Exemples : + +- Inscription utilisateur +- Connexion +- Création d'employés +- Participation au jeu-concours +- Validation des paramètres de requête + +Cette approche garantit : + +- Des données cohérentes +- Une meilleure sécurité +- Des messages d'erreur explicites +- Une maintenance facilitée + +--- + +## Modèle de données principal + +### User + +- **CLIENT** +- **EMPLOYEE** +- **ADMIN** + +### Ticket + +- Code unique +- Gain associé +- Utilisé / Non utilisé + +### Lot + +- Nom +- Description +- Valeur +- Pourcentage de répartition + +### Participation + +- Utilisateur +- Ticket +- Lot gagné + +### Claim + +- **PENDING** +- **DELIVERED** +- **CANCELLED** + +### GrandPrizeWinner + +- Gagnant du gros lot +- Date du tirage +- Récompense + +--- + +## Prisma ORM 6 + +Le projet utilise **Prisma **ORM** 6** pour : + +- La modélisation des données +- Les migrations de base de données +- Les requêtes **SQL** typées +- La génération automatique du client Prisma +- L'administration de la base via Prisma Studio + +Commandes utiles : + +```bash npx prisma migrate dev npx prisma generate npx prisma db seed npx prisma studio ``` + +--- + +## Règles métier + +- Tous les tickets sont gagnants. +- Un ticket ne peut être utilisé qu'une seule fois. +- Les gains sont associés aux tickets dès leur génération. +- Plusieurs participations n'augmentent pas les chances de remporter le gros lot. +- Un seul gagnant du gros lot peut être enregistré. +- Les gains non récupérés dans les délais peuvent être annulés. + +--- + +## Auteur + +Projet réalisé dans le cadre de la certification : + +**Expert en Stratégie et Transformation Digitale** + +Client fictif : **Thé Tip Top**. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ddb54e4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2230 @@ +{ + "name": "the-tip-top-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "the-tip-top-backend", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^6.19.3", + "bcrypt": "^6.0.0", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "prisma": "^6.19.3", + "swagger-jsdoc": "^6.3.0", + "swagger-ui-express": "^5.0.1", + "zod": "^4.4.3" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/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/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/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/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/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "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/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "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/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/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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/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-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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "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": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/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.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "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/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/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.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.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.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "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/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", + "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.1.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, + "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/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "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/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "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/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.3.0.tgz", + "integrity": "sha512-I+iQjVGV3t28pOkQUJv2MncthvOtkEactOn8R76SvSYhxgtIn7FoqfDHwQaN+GBnQdXQLrhgDXseKitmJcHMsA==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^12.1.0", + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "11.1.0", + "lodash.mergewith": "^4.6.2", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bbdb53a --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "the-tip-top-backend", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "dev": "nodemon src/app.js", + "test": "echo \"Error: no test specified\" && exit 1", + "generate:tickets": "node src/scripts/generateTickets.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@prisma/client": "^6.19.3", + "bcrypt": "^6.0.0", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "prisma": "^6.19.3", + "swagger-jsdoc": "^6.3.0", + "swagger-ui-express": "^5.0.1", + "zod": "^4.4.3" + }, + "devDependencies": { + "nodemon": "^3.1.14" + }, + "prisma": { + "seed": "node prisma/seed.js" + } +} diff --git a/prisma/migrations/20260603130023_init/migration.sql b/prisma/migrations/20260603130023_init/migration.sql new file mode 100644 index 0000000..35719f0 --- /dev/null +++ b/prisma/migrations/20260603130023_init/migration.sql @@ -0,0 +1,104 @@ +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('CLIENT', 'EMPLOYEE', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "ClaimType" AS ENUM ('STORE', 'ONLINE'); + +-- CreateEnum +CREATE TYPE "ClaimStatus" AS ENUM ('PENDING', 'DELIVERED', 'CANCELLED'); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "firstname" TEXT NOT NULL, + "lastname" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "Role" NOT NULL DEFAULT 'CLIENT', + "birthdate" TIMESTAMP(3), + "gender" TEXT, + "marketingOptIn" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Lot" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "value" DOUBLE PRECISION, + "percentage" DOUBLE PRECISION NOT NULL, + + CONSTRAINT "Lot_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Ticket" ( + "id" SERIAL NOT NULL, + "code" TEXT NOT NULL, + "isUsed" BOOLEAN NOT NULL DEFAULT false, + "usedAt" TIMESTAMP(3), + "lotId" INTEGER NOT NULL, + "userId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Ticket_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Participation" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "ticketId" INTEGER NOT NULL, + "lotId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Participation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Claim" ( + "id" SERIAL NOT NULL, + "status" "ClaimStatus" NOT NULL DEFAULT 'PENDING', + "participationId" INTEGER NOT NULL, + "validatedByEmployeeId" INTEGER, + "claimedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Claim_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Ticket_code_key" ON "Ticket"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Participation_ticketId_key" ON "Participation"("ticketId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Claim_participationId_key" ON "Claim"("participationId"); + +-- AddForeignKey +ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_lotId_fkey" FOREIGN KEY ("lotId") REFERENCES "Lot"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Participation" ADD CONSTRAINT "Participation_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Participation" ADD CONSTRAINT "Participation_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Participation" ADD CONSTRAINT "Participation_lotId_fkey" FOREIGN KEY ("lotId") REFERENCES "Lot"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Claim" ADD CONSTRAINT "Claim_participationId_fkey" FOREIGN KEY ("participationId") REFERENCES "Participation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Claim" ADD CONSTRAINT "Claim_validatedByEmployeeId_fkey" FOREIGN KEY ("validatedByEmployeeId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260603144928_add_grand_prize_winner/migration.sql b/prisma/migrations/20260603144928_add_grand_prize_winner/migration.sql new file mode 100644 index 0000000..689f6dd --- /dev/null +++ b/prisma/migrations/20260603144928_add_grand_prize_winner/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "GrandPrizeWinner" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "prize" TEXT NOT NULL DEFAULT 'Un an de thé d''une valeur de 360€', + "drawnAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GrandPrizeWinner_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "GrandPrizeWinner_userId_key" ON "GrandPrizeWinner"("userId"); + +-- AddForeignKey +ALTER TABLE "GrandPrizeWinner" ADD CONSTRAINT "GrandPrizeWinner_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..bffe6d7 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,111 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum Role { + CLIENT + EMPLOYEE + ADMIN +} + +enum ClaimType { + STORE + ONLINE +} + +enum ClaimStatus { + PENDING + DELIVERED + CANCELLED +} + +model User { + id Int @id @default(autoincrement()) + firstname String + lastname String + email String @unique + password String + role Role @default(CLIENT) + birthdate DateTime? + gender String? + marketingOptIn Boolean @default(false) + createdAt DateTime @default(now()) + + tickets Ticket[] + participations Participation[] + grandPrizeWinner GrandPrizeWinner? + claimsValidated Claim[] @relation("EmployeeClaims") +} + +model Lot { + id Int @id @default(autoincrement()) + name String + description String? + value Float? + percentage Float + + tickets Ticket[] + participations Participation[] +} + +model Ticket { + id Int @id @default(autoincrement()) + code String @unique + isUsed Boolean @default(false) + usedAt DateTime? + + lotId Int + lot Lot @relation(fields: [lotId], references: [id]) + + userId Int? + user User? @relation(fields: [userId], references: [id]) + + participation Participation? + createdAt DateTime @default(now()) +} + +model Participation { + id Int @id @default(autoincrement()) + + userId Int + user User @relation(fields: [userId], references: [id]) + + ticketId Int @unique + ticket Ticket @relation(fields: [ticketId], references: [id]) + + lotId Int + lot Lot @relation(fields: [lotId], references: [id]) + + claim Claim? + createdAt DateTime @default(now()) +} + +model Claim { + id Int @id @default(autoincrement()) + + status ClaimStatus @default(PENDING) + + participationId Int @unique + participation Participation @relation(fields: [participationId], references: [id]) + + validatedByEmployeeId Int? + validatedByEmployee User? @relation("EmployeeClaims", fields: [validatedByEmployeeId], references: [id]) + + claimedAt DateTime? + createdAt DateTime @default(now()) +} + +model GrandPrizeWinner { + id Int @id @default(autoincrement()) + + userId Int @unique + user User @relation(fields: [userId], references: [id]) + + prize String @default("Un an de thé d'une valeur de 360€") + drawnAt DateTime @default(now()) +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 0000000..206ba20 --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,51 @@ +const prisma = require("../src/config/prisma"); + +async function main() { + await prisma.lot.deleteMany(); + + await prisma.lot.createMany({ + data: [ + { + name: "Infuseur à thé", + description: "Infuseur à thé offert", + value: 5, + percentage: 60, + }, + { + name: "Boîte de 100g thé détox ou infusion", + description: "Boîte de 100g d’un thé détox ou d’une infusion", + value: 12, + percentage: 20, + }, + { + name: "Boîte de 100g thé signature", + description: "Boîte de 100g d’un thé signature Thé Tip Top", + value: 18, + percentage: 10, + }, + { + name: "Coffret découverte 39€", + description: "Coffret découverte d’une valeur de 39€", + value: 39, + percentage: 6, + }, + { + name: "Coffret découverte 69€", + description: "Coffret découverte d’une valeur de 69€", + value: 69, + percentage: 4, + }, + ], + }); + + console.log("Lots créés avec succès"); +} + +main() + .catch((error) => { + console.error("Erreur seed :", error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); \ No newline at end of file diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..895f89a --- /dev/null +++ b/src/app.js @@ -0,0 +1,38 @@ +const express = require("express"); +const cors = require("cors"); + +require("dotenv").config(); + +const authRoutes = require("./routes/auth.routes"); +const gameRoutes = require("./routes/game.routes"); +const employeeRoutes = require("./routes/employee.routes"); +const adminRoutes = require("./routes/admin.routes"); +const swaggerUi = require("swagger-ui-express"); +const swaggerSpec = require("./docs/swagger"); + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +app.use("/api/auth", authRoutes); +app.use("/api/game", gameRoutes); +app.use("/api/employee", employeeRoutes); +app.use("/api/admin", adminRoutes); +app.use( + "/api-docs", + swaggerUi.serve, + swaggerUi.setup(swaggerSpec) +); + +app.get("/", (req, res) => { + res.json({ + message: "API Thé Tip Top opérationnelle", + }); +}); + +const PORT = process.env.PORT || 5000; + +app.listen(PORT, () => { + console.log(`Serveur lancé sur http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/src/config/prisma.js b/src/config/prisma.js new file mode 100644 index 0000000..05898df --- /dev/null +++ b/src/config/prisma.js @@ -0,0 +1,5 @@ +const { PrismaClient } = require("@prisma/client"); + +const prisma = new PrismaClient(); + +module.exports = prisma; \ No newline at end of file diff --git a/src/controllers/admin.controller.js b/src/controllers/admin.controller.js new file mode 100644 index 0000000..2260d3c --- /dev/null +++ b/src/controllers/admin.controller.js @@ -0,0 +1,292 @@ +const bcrypt = require("bcrypt"); +const prisma = require("../config/prisma"); + +const getStats = async (req, res) => { + try { + const [ + ticketsTotal, + ticketsUsed, + participations, + claimsPending, + claimsDelivered, + claimsCancelled, + users, + men, + women, + ] = await Promise.all([ + prisma.ticket.count(), + prisma.ticket.count({ where: { isUsed: true } }), + prisma.participation.count(), + prisma.claim.count({ where: { status: "PENDING" } }), + prisma.claim.count({ where: { status: "DELIVERED" } }), + prisma.claim.count({ where: { status: "CANCELLED" } }), + prisma.user.count({ where: { role: "CLIENT" } }), + prisma.user.count({ where: { role: "CLIENT", gender: "Homme" } }), + prisma.user.count({ where: { role: "CLIENT", gender: "Femme" } }), + ]); + + const lotsStats = await prisma.lot.findMany({ + select: { + id: true, + name: true, + percentage: true, + _count: { + select: { + tickets: true, + participations: true, + }, + }, + }, + }); + + return res.json({ + ticketsTotal, + ticketsUsed, + ticketsRemaining: ticketsTotal - ticketsUsed, + participations, + claimsPending, + claimsDelivered, + claimsCancelled, + users, + genderStats: { + men, + women, + other: users - men - women, + }, + lotsStats, + }); + } catch (error) { + console.error("Erreur stats admin:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la récupération des statistiques.", + }); + } +}; + +const getUsers = async (req, res) => { + try { + const users = await prisma.user.findMany({ + where: { role: "CLIENT" }, + select: { + id: true, + firstname: true, + lastname: true, + email: true, + gender: true, + birthdate: true, + marketingOptIn: true, + createdAt: true, + _count: { + select: { + participations: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return res.json({ users }); + } catch (error) { + console.error("Erreur liste utilisateurs:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la récupération des utilisateurs.", + }); + } +}; + +const getEmailingContacts = async (req, res) => { + try { + const contacts = await prisma.user.findMany({ + where: { + role: "CLIENT", + marketingOptIn: true, + }, + select: { + firstname: true, + lastname: true, + email: true, + createdAt: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return res.json({ + total: contacts.length, + contacts, + }); + } catch (error) { + console.error("Erreur emailing:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la récupération des contacts emailing.", + }); + } +}; + +const createEmployee = async (req, res) => { + try { + const { firstname, lastname, email, password, birthdate, gender } = req.body; + + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + + if (existingUser) { + return res.status(409).json({ + message: "Un compte existe déjà avec cet email.", + }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + const employee = await prisma.user.create({ + data: { + firstname, + lastname, + email, + password: hashedPassword, + birthdate: birthdate ? new Date(birthdate) : null, + gender, + role: "EMPLOYEE", + marketingOptIn: false, + }, + select: { + id: true, + firstname: true, + lastname: true, + email: true, + role: true, + createdAt: true, + }, + }); + + return res.status(201).json({ + message: "Employé créé avec succès.", + employee, + }); + } catch (error) { + console.error("Erreur création employé:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la création de l'employé.", + }); + } +}; + +const drawWinner = async (req, res) => { + try { + const existingWinner = await prisma.grandPrizeWinner.findFirst({ + include: { + user: { + select: { + id: true, + firstname: true, + lastname: true, + email: true, + }, + }, + }, + }); + + if (existingWinner) { + return res.status(409).json({ + message: "Le tirage au sort a déjà été effectué.", + winner: existingWinner, + }); + } + + const participants = await prisma.user.findMany({ + where: { + role: "CLIENT", + participations: { + some: {}, + }, + }, + select: { + id: true, + firstname: true, + lastname: true, + email: true, + }, + }); + + if (participants.length === 0) { + return res.status(400).json({ + message: "Aucun participant disponible pour le tirage.", + }); + } + + const randomIndex = Math.floor(Math.random() * participants.length); + const winner = participants[randomIndex]; + + const savedWinner = await prisma.grandPrizeWinner.create({ + data: { + userId: winner.id, + }, + include: { + user: { + select: { + id: true, + firstname: true, + lastname: true, + email: true, + }, + }, + }, + }); + + return res.status(201).json({ + message: "Tirage au sort effectué avec succès.", + totalEligibleParticipants: participants.length, + winner: savedWinner, + }); + } catch (error) { + console.error("Erreur tirage au sort:", error); + return res.status(500).json({ + message: "Erreur serveur lors du tirage au sort.", + }); + } +}; + +const getGrandPrizeWinner = async (req, res) => { + try { + const winner = await prisma.grandPrizeWinner.findFirst({ + include: { + user: { + select: { + id: true, + firstname: true, + lastname: true, + email: true, + }, + }, + }, + }); + + if (!winner) { + return res.status(404).json({ + message: "Aucun gagnant du gros lot n'a encore été tiré.", + }); + } + + return res.json({ + winner, + }); + } catch (error) { + console.error("Erreur récupération gagnant:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la récupération du gagnant.", + }); + } +}; + +module.exports = { + getStats, + getUsers, + getEmailingContacts, + createEmployee, + drawWinner, + getGrandPrizeWinner, +}; \ No newline at end of file diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js new file mode 100644 index 0000000..3b867aa --- /dev/null +++ b/src/controllers/auth.controller.js @@ -0,0 +1,134 @@ +const bcrypt = require("bcrypt"); +const jwt = require("jsonwebtoken"); +const prisma = require("../config/prisma"); + +const register = async (req, res) => { + try { + const { + firstname, + lastname, + email, + password, + birthdate, + gender, + marketingOptIn, + } = req.body; + + if (!firstname || !lastname || !email || !password) { + return res.status(400).json({ + message: "Tous les champs obligatoires doivent être renseignés.", + }); + } + + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + + if (existingUser) { + return res.status(409).json({ + message: "Un compte existe déjà avec cet email.", + }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + const user = await prisma.user.create({ + data: { + firstname, + lastname, + email, + password: hashedPassword, + birthdate: birthdate ? new Date(birthdate) : null, + gender, + marketingOptIn: marketingOptIn || false, + }, + select: { + id: true, + firstname: true, + lastname: true, + email: true, + role: true, + marketingOptIn: true, + createdAt: true, + }, + }); + + return res.status(201).json({ + message: "Inscription réussie.", + user, + }); + } catch (error) { + console.error("Erreur register:", error); + return res.status(500).json({ + message: "Erreur serveur lors de l'inscription.", + }); + } +}; + +const login = async (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ + message: "Email et mot de passe requis.", + }); + } + + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user) { + return res.status(401).json({ + message: "Identifiants invalides.", + }); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return res.status(401).json({ + message: "Identifiants invalides.", + }); + } + + const token = jwt.sign( + { + id: user.id, + role: user.role, + }, + process.env.JWT_SECRET, + { expiresIn: "24h" } + ); + + return res.json({ + message: "Connexion réussie.", + token, + user: { + id: user.id, + firstname: user.firstname, + lastname: user.lastname, + email: user.email, + role: user.role, + }, + }); + } catch (error) { + console.error("Erreur login:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la connexion.", + }); + } +}; + +const me = async (req, res) => { + return res.json({ + user: req.user, + }); +}; + +module.exports = { + register, + login, + me, +}; \ No newline at end of file diff --git a/src/controllers/employee.controller.js b/src/controllers/employee.controller.js new file mode 100644 index 0000000..be501ac --- /dev/null +++ b/src/controllers/employee.controller.js @@ -0,0 +1,115 @@ +const prisma = require("../config/prisma"); + +const getGainByTicketCode = async (req, res) => { + try { + const { code } = req.params; + + const ticket = await prisma.ticket.findUnique({ + where: { code: code.toUpperCase() }, + include: { + lot: true, + user: { + select: { + id: true, + firstname: true, + lastname: true, + email: true, + }, + }, + participation: { + include: { + claim: true, + }, + }, + }, + }); + + if (!ticket) { + return res.status(404).json({ + message: "Ticket introuvable.", + }); + } + + if (!ticket.isUsed || !ticket.participation) { + return res.status(400).json({ + message: "Ce ticket n'a pas encore été utilisé par un client.", + }); + } + + return res.json({ + ticket: { + code: ticket.code, + usedAt: ticket.usedAt, + }, + client: ticket.user, + lot: { + name: ticket.lot.name, + description: ticket.lot.description, + value: ticket.lot.value, + }, + claim: { + id: ticket.participation.claim.id, + status: ticket.participation.claim.status, + claimedAt: ticket.participation.claim.claimedAt, + }, + }); + } catch (error) { + console.error("Erreur recherche gain employé:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la recherche du gain.", + }); + } +}; + +const deliverClaim = async (req, res) => { + try { + const { id } = req.params; + const employeeId = req.user.id; + + const claim = await prisma.claim.findUnique({ + where: { id: Number(id) }, + }); + + if (!claim) { + return res.status(404).json({ + message: "Réclamation introuvable.", + }); + } + + if (claim.status === "DELIVERED") { + return res.status(409).json({ + message: "Ce lot a déjà été remis.", + }); + } + + if (claim.status === "CANCELLED") { + return res.status(400).json({ + message: "Ce gain est expiré et ne peut plus être remis.", + }); + } + + const updatedClaim = await prisma.claim.update({ + where: { id: Number(id) }, + data: { + status: "DELIVERED", + claimedAt: new Date(), + validatedByEmployeeId: employeeId, + }, + }); + + return res.json({ + message: "Lot marqué comme remis avec succès.", + claim: updatedClaim, + }); + } catch (error) { + console.error("Erreur remise lot:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la remise du lot.", + }); + } +}; + +module.exports = { + getGainByTicketCode, + deliverClaim, +}; \ No newline at end of file diff --git a/src/controllers/game.controller.js b/src/controllers/game.controller.js new file mode 100644 index 0000000..5477fca --- /dev/null +++ b/src/controllers/game.controller.js @@ -0,0 +1,120 @@ +const prisma = require("../config/prisma"); + +const participate = async (req, res) => { + try { + const { code } = req.body; + const userId = req.user.id; + + const ticket = await prisma.ticket.findUnique({ + where: { code }, + include: { + lot: true, + }, + }); + + if (!ticket) { + return res.status(404).json({ + message: "Code invalide.", + }); + } + + if (ticket.isUsed) { + return res.status(409).json({ + message: "Ce code a déjà été utilisé.", + }); + } + + const result = await prisma.$transaction(async (tx) => { + const updatedTicket = await tx.ticket.update({ + where: { id: ticket.id }, + data: { + isUsed: true, + usedAt: new Date(), + userId, + }, + include: { + lot: true, + }, + }); + + const participation = await tx.participation.create({ + data: { + userId, + ticketId: updatedTicket.id, + lotId: updatedTicket.lotId, + }, + }); + + const claim = await tx.claim.create({ + data: { + participationId: participation.id, + status: "PENDING", + }, + }); + + return { + ticket: updatedTicket, + participation, + claim, + }; + }); + + return res.status(201).json({ + message: "Participation enregistrée avec succès.", + gain: { + name: result.ticket.lot.name, + description: result.ticket.lot.description, + value: result.ticket.lot.value, + }, + claim: { + id: result.claim.id, + status: result.claim.status, + }, + }); + } catch (error) { + console.error("Erreur participation:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la participation.", + }); + } +}; + +const myGains = async (req, res) => { + try { + const userId = req.user.id; + + const participations = await prisma.participation.findMany({ + where: { userId }, + include: { + ticket: true, + lot: true, + claim: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return res.json({ + gains: participations.map((participation) => ({ + participationId: participation.id, + ticketCode: participation.ticket.code, + lot: participation.lot.name, + description: participation.lot.description, + value: participation.lot.value, + claimStatus: participation.claim?.status, + wonAt: participation.createdAt, + })), + }); + } catch (error) { + console.error("Erreur historique gains:", error); + return res.status(500).json({ + message: "Erreur serveur lors de la récupération des gains.", + }); + } +}; + +module.exports = { + participate, + myGains, +}; \ No newline at end of file diff --git a/src/docs/swagger.js b/src/docs/swagger.js new file mode 100644 index 0000000..a9809e3 --- /dev/null +++ b/src/docs/swagger.js @@ -0,0 +1,29 @@ +const swaggerJsdoc = require("swagger-jsdoc"); + +const options = { + definition: { + openapi: "3.0.0", + info: { + title: "Thé Tip Top API", + version: "1.0.0", + description: "API du jeu concours Thé Tip Top", + }, + servers: [ + { + url: "http://localhost:5000", + }, + ], + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + }, + }, + }, + }, + apis: ["./src/routes/*.js"], +}; + +module.exports = swaggerJsdoc(options); \ No newline at end of file diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js new file mode 100644 index 0000000..e3d07b4 --- /dev/null +++ b/src/middlewares/auth.middleware.js @@ -0,0 +1,44 @@ +const jwt = require("jsonwebtoken"); +const prisma = require("../config/prisma"); + +const authMiddleware = async (req, res, next) => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ + message: "Token manquant ou invalide.", + }); + } + + const token = authHeader.split(" ")[1]; + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + const user = await prisma.user.findUnique({ + where: { id: decoded.id }, + select: { + id: true, + firstname: true, + lastname: true, + email: true, + role: true, + }, + }); + + if (!user) { + return res.status(401).json({ + message: "Utilisateur introuvable.", + }); + } + + req.user = user; + next(); + } catch (error) { + return res.status(401).json({ + message: "Token invalide ou expiré.", + }); + } +}; + +module.exports = authMiddleware; \ No newline at end of file diff --git a/src/middlewares/role.middleware.js b/src/middlewares/role.middleware.js new file mode 100644 index 0000000..b1740cd --- /dev/null +++ b/src/middlewares/role.middleware.js @@ -0,0 +1,13 @@ +const roleMiddleware = (...allowedRoles) => { + return (req, res, next) => { + if (!req.user || !allowedRoles.includes(req.user.role)) { + return res.status(403).json({ + message: "Accès refusé.", + }); + } + + next(); + }; +}; + +module.exports = roleMiddleware; \ No newline at end of file diff --git a/src/middlewares/validate.middleware.js b/src/middlewares/validate.middleware.js new file mode 100644 index 0000000..9890ba9 --- /dev/null +++ b/src/middlewares/validate.middleware.js @@ -0,0 +1,17 @@ +const validate = (schema) => { + return (req, res, next) => { + const result = schema.safeParse(req.body); + + if (!result.success) { + return res.status(400).json({ + message: "Données invalides", + errors: result.error.flatten().fieldErrors, + }); + } + + req.body = result.data; + next(); + }; +}; + +module.exports = validate; \ No newline at end of file diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js new file mode 100644 index 0000000..002d31d --- /dev/null +++ b/src/routes/admin.routes.js @@ -0,0 +1,114 @@ +const express = require("express"); +const { + getStats, + getUsers, + getEmailingContacts, + createEmployee, + drawWinner, + getGrandPrizeWinner, +} = require("../controllers/admin.controller"); + +const authMiddleware = require("../middlewares/auth.middleware"); +const roleMiddleware = require("../middlewares/role.middleware"); +const validate = require("../middlewares/validate.middleware"); +const { createEmployeeSchema } = require("../validations/admin.validation"); + +const router = express.Router(); + +router.use(authMiddleware); +router.use(roleMiddleware("ADMIN")); + +/** + * @swagger + * /api/admin/stats: + * get: + * summary: Tableau de bord administrateur + * tags: + * - Admin + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Statistiques du jeu concours + */ + +router.get("/stats", getStats); + +/** + * @swagger + * /api/admin/users: + * get: + * summary: Liste des clients inscrits + * tags: + * - Admin + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Liste des utilisateurs clients + */ +router.get("/users", getUsers); + +/** + * @swagger + * /api/admin/emailing: + * get: + * summary: Liste des contacts ayant accepté l'emailing + * tags: + * - Admin + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Contacts marketing opt-in + */ +router.get("/emailing", getEmailingContacts); + +/** + * @swagger + * /api/admin/employees: + * post: + * summary: Création d'un employé + * tags: + * - Admin + * security: + * - bearerAuth: [] + * responses: + * 201: + * description: Employé créé + */ +router.post("/employees", validate(createEmployeeSchema), createEmployee); + +/** + * @swagger + * /api/admin/draw: + * post: + * summary: Tirage au sort du grand gagnant + * tags: + * - Admin + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Gagnant tiré au sort + */ +router.post("/draw", drawWinner); + +/** + * @swagger + * /api/admin/draw: + * get: + * summary: Consulter le gagnant du gros lot + * tags: + * - Admin + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Gagnant du gros lot + * 404: + * description: Aucun gagnant encore tiré + */ +router.get("/draw", getGrandPrizeWinner); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js new file mode 100644 index 0000000..04925e9 --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,69 @@ +const express = require("express"); +const { register, login, me } = require("../controllers/auth.controller"); +const authMiddleware = require("../middlewares/auth.middleware"); +const validate = require("../middlewares/validate.middleware"); +const { registerSchema, loginSchema } = require("../validations/auth.validation"); + +const router = express.Router(); + +/** + * @swagger + * /api/auth/register: + * post: + * summary: Inscription d'un utilisateur + * tags: + * - Auth + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * firstname: + * type: string + * lastname: + * type: string + * email: + * type: string + * password: + * type: string + * responses: + * 201: + * description: Utilisateur créé + */ +router.post("/register", validate(registerSchema), register); + +/** + * @swagger + * /api/auth/login: + * post: + * summary: Connexion utilisateur + * tags: + * - Auth + * requestBody: + * required: true + * responses: + * 200: + * description: Connexion réussie + */ + +router.post("/login", validate(loginSchema), login); + +/** + * @swagger + * /api/auth/me: + * get: + * summary: Informations de l'utilisateur connecté + * tags: + * - Auth + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Profil utilisateur + */ +router.get("/me", authMiddleware, me); + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/employee.routes.js b/src/routes/employee.routes.js new file mode 100644 index 0000000..c5b203c --- /dev/null +++ b/src/routes/employee.routes.js @@ -0,0 +1,76 @@ +const express = require("express"); +const { + getGainByTicketCode, + deliverClaim, +} = require("../controllers/employee.controller"); + +const authMiddleware = require("../middlewares/auth.middleware"); +const roleMiddleware = require("../middlewares/role.middleware"); + +const router = express.Router(); + +/** + * @swagger + * /api/employee/ticket/{code}: + * get: + * summary: Rechercher le gain associé à un ticket + * tags: + * - Employee + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: code + * required: true + * schema: + * type: string + * description: Code du ticket à 10 caractères + * responses: + * 200: + * description: Gain trouvé + * 400: + * description: Ticket non encore utilisé par un client + * 404: + * description: Ticket introuvable + */ +router.get( + "/ticket/:code", + authMiddleware, + roleMiddleware("EMPLOYEE", "ADMIN"), + getGainByTicketCode +); + +/** + * @swagger + * /api/employee/claim/{id}/deliver: + * patch: + * summary: Marquer un lot comme remis en boutique + * tags: + * - Employee + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ID de la réclamation + * responses: + * 200: + * description: Lot remis avec succès + * 400: + * description: Gain expiré ou non valide + * 404: + * description: Réclamation introuvable + * 409: + * description: Lot déjà remis + */ +router.patch( + "/claim/:id/deliver", + authMiddleware, + roleMiddleware("EMPLOYEE", "ADMIN"), + deliverClaim +); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/game.routes.js b/src/routes/game.routes.js new file mode 100644 index 0000000..26a5b03 --- /dev/null +++ b/src/routes/game.routes.js @@ -0,0 +1,41 @@ +const express = require("express"); +const { participate, myGains } = require("../controllers/game.controller"); +const authMiddleware = require("../middlewares/auth.middleware"); +const validate = require("../middlewares/validate.middleware"); +const { participateSchema } = require("../validations/game.validation"); + +const router = express.Router(); + +/** + * @swagger + * /api/game/participate: + * post: + * summary: Participer au jeu concours + * tags: + * - Game + * security: + * - bearerAuth: [] + * responses: + * 201: + * description: Participation enregistrée + */ + +router.post("/participate", authMiddleware, validate(participateSchema), participate); + +/** + * @swagger + * /api/game/my-gains: + * get: + * summary: Historique des gains du participant + * tags: + * - Game + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Liste des gains + */ + +router.get("/my-gains", authMiddleware, myGains); + +module.exports = router; \ No newline at end of file diff --git a/src/scripts/generateTickets.js b/src/scripts/generateTickets.js new file mode 100644 index 0000000..b0b5f16 --- /dev/null +++ b/src/scripts/generateTickets.js @@ -0,0 +1,67 @@ +const prisma = require("../config/prisma"); + +function generateCode(length = 10) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let code = ""; + + for (let i = 0; i < length; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return code; +} + +async function generateTickets(total = 500000) { + const lots = await prisma.lot.findMany({ + orderBy: { id: "asc" }, + }); + + if (lots.length !== 5) { + throw new Error("Les 5 lots doivent être créés avant de générer les tickets."); + } + + await prisma.ticket.deleteMany(); + + const repartition = [ + { lotId: lots[0].id, count: Math.floor(total * 0.6) }, + { lotId: lots[1].id, count: Math.floor(total * 0.2) }, + { lotId: lots[2].id, count: Math.floor(total * 0.1) }, + { lotId: lots[3].id, count: Math.floor(total * 0.06) }, + { lotId: lots[4].id, count: total - (Math.floor(total * 0.6) + Math.floor(total * 0.2) + Math.floor(total * 0.1) + Math.floor(total * 0.06)) }, + ]; + + const usedCodes = new Set(); + const tickets = []; + + for (const item of repartition) { + for (let i = 0; i < item.count; i++) { + let code; + + do { + code = generateCode(); + } while (usedCodes.has(code)); + + usedCodes.add(code); + + tickets.push({ + code, + lotId: item.lotId, + }); + } + } + + await prisma.ticket.createMany({ + data: tickets, + }); + + console.log(`---- ${tickets.length} tickets générés avec succès.----`); +} + +generateTickets() + .catch((error) => { + console.error("Erreur génération tickets :", error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); \ No newline at end of file diff --git a/src/validations/admin.validation.js b/src/validations/admin.validation.js new file mode 100644 index 0000000..6ea9852 --- /dev/null +++ b/src/validations/admin.validation.js @@ -0,0 +1,14 @@ +const { z } = require("zod"); + +const createEmployeeSchema = z.object({ + firstname: z.string().min(2), + lastname: z.string().min(2), + email: z.string().email(), + password: z.string().min(8), + birthdate: z.string().optional(), + gender: z.string().optional(), +}); + +module.exports = { + createEmployeeSchema, +}; \ No newline at end of file diff --git a/src/validations/auth.validation.js b/src/validations/auth.validation.js new file mode 100644 index 0000000..4061a73 --- /dev/null +++ b/src/validations/auth.validation.js @@ -0,0 +1,21 @@ +const { z } = require("zod"); + +const registerSchema = z.object({ + firstname: z.string().min(2, "Le prénom doit contenir au moins 2 caractères"), + lastname: z.string().min(2, "Le nom doit contenir au moins 2 caractères"), + email: z.string().email("Email invalide"), + password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères"), + birthdate: z.string().optional(), + gender: z.string().optional(), + marketingOptIn: z.boolean().optional(), +}); + +const loginSchema = z.object({ + email: z.string().email("Email invalide"), + password: z.string().min(1, "Mot de passe requis"), +}); + +module.exports = { + registerSchema, + loginSchema, +}; \ No newline at end of file diff --git a/src/validations/game.validation.js b/src/validations/game.validation.js new file mode 100644 index 0000000..d402cd5 --- /dev/null +++ b/src/validations/game.validation.js @@ -0,0 +1,13 @@ +const { z } = require("zod"); + +const participateSchema = z.object({ + code: z + .string() + .min(10, "Le code doit contenir 10 caractères") + .max(10, "Le code doit contenir 10 caractères") + .toUpperCase(), +}); + +module.exports = { + participateSchema, +}; \ No newline at end of file From d15c7898e1c0394ac5139d5e5cbf8a586bb8057c Mon Sep 17 00:00:00 2001 From: Anilzer Date: Wed, 3 Jun 2026 17:39:06 +0200 Subject: [PATCH 2/5] =?UTF-8?q?Mise=20=C3=A0=20jour=20du=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 18e6160..9cf5dee 100644 --- a/README.md +++ b/README.md @@ -108,11 +108,12 @@ Le backend expose une **API** **REST** sécurisée utilisée par : Créer un fichier `.env` à la racine du projet : -```env **PORT**=**5000** -DATABASE_URL="postgresql://postgres:password@localhost:**5432**/the_tip_top_db* +```env **PORT**=**5000**``` -JWT_SECRET=*your_secret_key" ``` +```DATABASE_URL="postgresql://postgres:password@localhost:**5432**/the_tip_top_db*``` + +```JWT_SECRET=*your_secret_key" ``` --- From 9beeed1384872993114afe3bb4c653d7fa130816 Mon Sep 17 00:00:00 2001 From: Anilzer Date: Wed, 3 Jun 2026 17:45:37 +0200 Subject: [PATCH 3/5] =?UTF-8?q?Mise=20=C3=A0=20jour=202=20du=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9cf5dee..f385b6f 100644 --- a/README.md +++ b/README.md @@ -98,11 +98,15 @@ Le backend expose une **API** **REST** sécurisée utilisée par : ### Cloner le projet -```bash git clone cd the-tip-top-backend ``` +```bash +git clone cd the-tip-top-backend +``` ### Installer les dépendances -```bash npm install ``` +```bash +npm install +``` ### Variables d'environnement @@ -121,23 +125,33 @@ Créer un fichier `.env` à la racine du projet : ### Générer les migrations -```bash npx prisma migrate dev ``` +```bash +npx prisma migrate dev +``` ### Générer Prisma Client -```bash npx prisma generate ``` +```bash +npx prisma generate + ``` ### Exécuter le seed de la base -```bash npx prisma db seed ``` +```bash +npx prisma db seed +``` ### Générer les tickets du jeu-concours -```bash npm run generate:tickets ``` +```bash +npm run generate:tickets +``` ### Ouvrir Prisma Studio -```bash npx prisma studio ``` +```bash +npx prisma studio +``` --- @@ -160,7 +174,10 @@ Créer un fichier `.env` à la racine du projet : ### Développement -```bash npm run dev ``` +```bash +npm run dev + +``` Serveur : @@ -324,7 +341,9 @@ Le projet utilise **Prisma **ORM** 6** pour : Commandes utiles : -```bash npx prisma migrate dev npx prisma generate npx prisma db seed npx prisma studio ``` +```bash +npx prisma migrate dev npx prisma generate npx prisma db seed npx prisma studio +``` --- @@ -341,8 +360,6 @@ Commandes utiles : ## Auteur -Projet réalisé dans le cadre de la certification : - -**Expert en Stratégie et Transformation Digitale** +Projet réalisé dans le cadre de la certification : **Expert en Stratégie et Transformation Digitale** Client fictif : **Thé Tip Top**. \ No newline at end of file From 4666c454ea6fea226d41aab2552ac8bd9b170697 Mon Sep 17 00:00:00 2001 From: Anilzer Date: Wed, 3 Jun 2026 17:46:24 +0200 Subject: [PATCH 4/5] =?UTF-8?q?Mise=20=C3=A0=20jour=203=20du=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f385b6f..52a6600 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,8 @@ Le backend expose une **API** **REST** sécurisée utilisée par : ### Cloner le projet ```bash -git clone cd the-tip-top-backend +git clone +cd the-tip-top-backend ``` ### Installer les dépendances From f5da9a1477be4dd3bdd517d238519342e004fdc8 Mon Sep 17 00:00:00 2001 From: Anilzer Date: Wed, 3 Jun 2026 18:21:15 +0200 Subject: [PATCH 5/5] =?UTF-8?q?Mise=20=C3=A0=20jour=204=20du=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 52a6600..cfe298e 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,8 @@ npx prisma studio ### Développement +Le projet utilise **Nodemon** afin de redémarrer automatiquement le serveur lors des modifications du code. + ```bash npm run dev