diff --git a/.gitignore b/.gitignore index 09057bb..175b613 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,10 @@ yarn-error.log* .pub-cache/ .pub/ /build/ +**/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake # Coverage coverage/ @@ -52,3 +56,4 @@ Xcf/** # Release notes CHANGELOG.md Ressources/ +.gitea-token diff --git a/Archives/ChatGPT Image 25 avr. 2025, 16_45_30.png b/Archives/ChatGPT Image 25 avr. 2025, 16_45_30.png deleted file mode 100644 index aa3d433..0000000 Binary files a/Archives/ChatGPT Image 25 avr. 2025, 16_45_30.png and /dev/null differ diff --git a/Archives/ChatGPT Image 30 avr. 2025, 11_48_00.png b/Archives/ChatGPT Image 30 avr. 2025, 11_48_00.png deleted file mode 100644 index 08e4045..0000000 Binary files a/Archives/ChatGPT Image 30 avr. 2025, 11_48_00.png and /dev/null differ diff --git a/Archives/ChatGPT Image 30 avr. 2025, 11_48_14.png b/Archives/ChatGPT Image 30 avr. 2025, 11_48_14.png deleted file mode 100644 index 50c2794..0000000 Binary files a/Archives/ChatGPT Image 30 avr. 2025, 11_48_14.png and /dev/null differ diff --git a/Archives/Cinq Styles de _P'titsPas_.png b/Archives/Cinq Styles de _P'titsPas_.png deleted file mode 100644 index 0d4428b..0000000 Binary files a/Archives/Cinq Styles de _P'titsPas_.png and /dev/null differ diff --git a/Archives/P'tisPas_logo.png b/Archives/P'tisPas_logo.png deleted file mode 100644 index 9ec3873..0000000 Binary files a/Archives/P'tisPas_logo.png and /dev/null differ diff --git a/Archives/P'tisPas_logo_trans.png b/Archives/P'tisPas_logo_trans.png deleted file mode 100644 index 512fabd..0000000 Binary files a/Archives/P'tisPas_logo_trans.png and /dev/null differ diff --git a/Archives/P'titsPas et pierres colorées.png b/Archives/P'titsPas et pierres colorées.png deleted file mode 100644 index d1bab2a..0000000 Binary files a/Archives/P'titsPas et pierres colorées.png and /dev/null differ diff --git a/Archives/P'titsPas_icone.png b/Archives/P'titsPas_icone.png deleted file mode 100644 index 8c9fe45..0000000 Binary files a/Archives/P'titsPas_icone.png and /dev/null differ diff --git a/Archives/Pas à Pas en Couleurs tordu.png b/Archives/Pas à Pas en Couleurs tordu.png deleted file mode 100644 index b2c4c5f..0000000 Binary files a/Archives/Pas à Pas en Couleurs tordu.png and /dev/null differ diff --git a/Archives/Pas à Pas en Pastel encre.png b/Archives/Pas à Pas en Pastel encre.png deleted file mode 100644 index 6a796b8..0000000 Binary files a/Archives/Pas à Pas en Pastel encre.png and /dev/null differ diff --git a/Archives/Taches_encres_01.png b/Archives/Taches_encres_01.png deleted file mode 100644 index 0f5a904..0000000 Binary files a/Archives/Taches_encres_01.png and /dev/null differ diff --git a/Archives/Taches_encres_02.png b/Archives/Taches_encres_02.png deleted file mode 100644 index 90ef4bd..0000000 Binary files a/Archives/Taches_encres_02.png and /dev/null differ diff --git a/Archives/Taches_encres_03.png b/Archives/Taches_encres_03.png deleted file mode 100644 index 704ebf0..0000000 Binary files a/Archives/Taches_encres_03.png and /dev/null differ diff --git a/Archives/champs_login.png b/Archives/champs_login.png deleted file mode 100644 index 2b88b6d..0000000 Binary files a/Archives/champs_login.png and /dev/null differ diff --git a/Archives/champs_login_2.png b/Archives/champs_login_2.png deleted file mode 100644 index 9257d1e..0000000 Binary files a/Archives/champs_login_2.png and /dev/null differ diff --git a/Archives/echantillons pierres.png b/Archives/echantillons pierres.png deleted file mode 100644 index 58cf2e1..0000000 Binary files a/Archives/echantillons pierres.png and /dev/null differ diff --git a/Archives/icon.png b/Archives/icon.png deleted file mode 100644 index 8c9fe45..0000000 Binary files a/Archives/icon.png and /dev/null differ diff --git a/Archives/logo.png b/Archives/logo.png deleted file mode 100644 index eedb716..0000000 Binary files a/Archives/logo.png and /dev/null differ diff --git a/Archives/logo02.png b/Archives/logo02.png deleted file mode 100644 index a4e9c88..0000000 Binary files a/Archives/logo02.png and /dev/null differ diff --git a/Archives/logo03.png b/Archives/logo03.png deleted file mode 100644 index 9267c2a..0000000 Binary files a/Archives/logo03.png and /dev/null differ diff --git a/Archives/logo04.png b/Archives/logo04.png deleted file mode 100644 index 338f91e..0000000 Binary files a/Archives/logo04.png and /dev/null differ diff --git a/Archives/page_login.png b/Archives/page_login.png deleted file mode 100644 index ed157e8..0000000 Binary files a/Archives/page_login.png and /dev/null differ diff --git a/Archives/page_login_2.png b/Archives/page_login_2.png deleted file mode 100644 index ac2dbc7..0000000 Binary files a/Archives/page_login_2.png and /dev/null differ diff --git a/Archives/page_login_3.png b/Archives/page_login_3.png deleted file mode 100644 index 8ee06c2..0000000 Binary files a/Archives/page_login_3.png and /dev/null differ diff --git a/Archives/page_login_4.1.png b/Archives/page_login_4.1.png deleted file mode 100644 index 28362a4..0000000 Binary files a/Archives/page_login_4.1.png and /dev/null differ diff --git a/Archives/paper.png b/Archives/paper.png deleted file mode 100644 index 2cfe532..0000000 Binary files a/Archives/paper.png and /dev/null differ diff --git a/Archives/paper2.png b/Archives/paper2.png deleted file mode 100644 index 3103d8f..0000000 Binary files a/Archives/paper2.png and /dev/null differ diff --git a/Archives/pierres.png b/Archives/pierres.png deleted file mode 100644 index f07a774..0000000 Binary files a/Archives/pierres.png and /dev/null differ diff --git a/Archives/propositions.png b/Archives/propositions.png deleted file mode 100644 index c1e884e..0000000 Binary files a/Archives/propositions.png and /dev/null differ diff --git a/Xcf/P'tisPas_logo_trans.xcf b/Xcf/P'tisPas_logo_trans.xcf deleted file mode 100644 index c4cc7af..0000000 Binary files a/Xcf/P'tisPas_logo_trans.xcf and /dev/null differ diff --git a/Xcf/P'titsPas et pierres colorées.xcf b/Xcf/P'titsPas et pierres colorées.xcf deleted file mode 100644 index 39ace4a..0000000 Binary files a/Xcf/P'titsPas et pierres colorées.xcf and /dev/null differ diff --git a/Xcf/P'titsPas_icone.xcf b/Xcf/P'titsPas_icone.xcf deleted file mode 100644 index e65abeb..0000000 Binary files a/Xcf/P'titsPas_icone.xcf and /dev/null differ diff --git a/Xcf/P'titsPas_logo.xcf b/Xcf/P'titsPas_logo.xcf deleted file mode 100644 index f4e8571..0000000 Binary files a/Xcf/P'titsPas_logo.xcf and /dev/null differ diff --git a/api-contracts/README.md b/api-contracts/README.md new file mode 100644 index 0000000..544ff97 --- /dev/null +++ b/api-contracts/README.md @@ -0,0 +1,87 @@ +# 📜 API Contracts - PtitsPas + +Ce dossier contient les **contrats d'API** qui définissent les interfaces entre les différentes couches de l'application. + +## 🎯 Objectif + +Garantir que **Frontend**, **Backend** et **Database** respectent des contrats stricts, permettant de les rendre **interchangeables** sans casser l'application. + +--- + +## 📁 Structure + +``` +api-contracts/ +├── frontend-backend/ # Contrat Frontend ↔ Backend (HTTP REST) +│ ├── openapi.yaml # Spécification OpenAPI 3.0 (source de vérité) +│ └── generated/ # Code généré automatiquement +│ ├── dart/ # Client API pour Flutter +│ └── typescript/ # Types pour NestJS +│ +└── backend-database/ # Contrat Backend ↔ Database (ORM/SQL) + ├── schema.prisma # Schéma Prisma (ou TypeORM entities) + └── migrations/ # Migrations SQL versionnées +``` + +--- + +## 🔄 Workflow de Génération + +### 1. Frontend ↔ Backend + +**Source de vérité :** `frontend-backend/openapi.yaml` + +**Génération du client Dart (Flutter) :** +```bash +cd api-contracts/frontend-backend +docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \ + -i /local/openapi.yaml \ + -g dart-dio \ + -o /local/generated/dart +``` + +**Génération des types TypeScript (NestJS) :** +```bash +cd api-contracts/frontend-backend +npx openapi-typescript openapi.yaml --output generated/typescript/api.types.ts +``` + +--- + +### 2. Backend ↔ Database + +**Source de vérité :** `backend-database/schema.prisma` + +**Génération du client Prisma :** +```bash +cd api-contracts/backend-database +npx prisma generate +``` + +**Génération des migrations SQL :** +```bash +cd api-contracts/backend-database +npx prisma migrate dev --name +``` + +--- + +## ✅ Avantages + +- **Frontend interchangeable** : React, Vue, Angular → il suffit de régénérer le client API +- **Backend interchangeable** : Python, Go, Java → tant qu'il respecte `openapi.yaml` +- **Database read-only en prod** : User PostgreSQL `app_user` (pas de DDL) +- **Cohérence garantie** : Types générés = pas d'erreur de typage +- **Documentation auto** : OpenAPI = documentation interactive (Swagger UI) + +--- + +## 📚 Documentation + +- [OpenAPI 3.0 Spec](https://swagger.io/specification/) +- [Prisma Schema](https://www.prisma.io/docs/concepts/components/prisma-schema) +- [openapi-generator](https://openapi-generator.tech/) +- [openapi-typescript](https://github.com/drwpow/openapi-typescript) + + + diff --git a/api-contracts/backend-database/README.md b/api-contracts/backend-database/README.md new file mode 100644 index 0000000..d50c4e3 --- /dev/null +++ b/api-contracts/backend-database/README.md @@ -0,0 +1,27 @@ +# 💾 Backend ↔ Database Contract + +Ce dossier contient le **contrat de données** entre le Backend et la Base de Données. + +## 📋 Contenu + +- **`schema.prisma`** : Schéma de base de données (à créer) +- **`migrations/`** : Migrations SQL versionnées (actuellement dans `/database/migrations/`) + +## 🔄 Migration Future + +À terme, les migrations SQL de `/database/migrations/` seront gérées ici avec Prisma : + +```bash +# Générer une migration +npx prisma migrate dev --name add_user_phone + +# Appliquer en production +npx prisma migrate deploy +``` + +## 📚 Référence + +- [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate) + + + diff --git a/api-contracts/frontend-backend/openapi.yaml b/api-contracts/frontend-backend/openapi.yaml new file mode 100644 index 0000000..6cd9b42 --- /dev/null +++ b/api-contracts/frontend-backend/openapi.yaml @@ -0,0 +1,179 @@ +openapi: 3.0.0 +info: + title: PtitsPas API + version: 1.0.0 + description: | + API REST pour l'application PtitsPas. + Ce contrat définit l'interface entre le Frontend (Flutter) et le Backend (NestJS). + contact: + name: PtitsPas Team + email: admin@ptits-pas.fr + +servers: + - url: https://app.ptits-pas.fr/api + description: Production + - url: http://localhost:3000/api + description: Développement local + +paths: + /auth/login: + post: + summary: Authentification utilisateur + operationId: loginUser + tags: + - Authentication + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + - password + properties: + email: + type: string + format: email + example: admin@ptits-pas.fr + password: + type: string + format: password + example: "4dm1n1strateur" + responses: + '200': + description: Authentification réussie + content: + application/json: + schema: + type: object + properties: + access_token: + type: string + description: Token JWT d'accès + refresh_token: + type: string + description: Token JWT de rafraîchissement + user: + $ref: '#/components/schemas/User' + '401': + description: Identifiants invalides + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /users: + get: + summary: Liste des utilisateurs + operationId: listUsers + tags: + - Users + security: + - bearerAuth: [] + parameters: + - name: role + in: query + schema: + $ref: '#/components/schemas/RoleType' + - name: statut + in: query + schema: + $ref: '#/components/schemas/StatutUtilisateurType' + responses: + '200': + description: Liste des utilisateurs + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '401': + description: Non authentifié + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + +components: + schemas: + User: + type: object + required: + - id + - email + - role + - statut + properties: + id: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" + email: + type: string + format: email + example: "parent@ptits-pas.fr" + prenom: + type: string + example: "Jean" + nom: + type: string + example: "Dupont" + role: + $ref: '#/components/schemas/RoleType' + statut: + $ref: '#/components/schemas/StatutUtilisateurType' + telephone: + type: string + example: "0612345678" + adresse: + type: string + photo_url: + type: string + format: uri + cree_le: + type: string + format: date-time + + RoleType: + type: string + enum: + - parent + - assistante_maternelle + - gestionnaire + - administrateur + - super_admin + + StatutUtilisateurType: + type: string + enum: + - en_attente + - actif + - suspendu + + Error: + type: object + required: + - message + - statusCode + properties: + message: + type: string + example: "Identifiants invalides" + statusCode: + type: integer + example: 401 + error: + type: string + example: "Unauthorized" + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: Token JWT obtenu via /auth/login + + + diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..002d786 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,23 @@ +# Fichier: .env.example +# Copier ce fichier vers .env et adapter les valeurs selon votre environnement + +# Configuration de la base de données PostgreSQL +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_USER=admin +POSTGRES_PASSWORD=admin123 +POSTGRES_DB=ptitpas_db + +# Configuration PgAdmin (accessible sur http://localhost:8080) +PGADMIN_DEFAULT_EMAIL=admin@localhost +PGADMIN_DEFAULT_PASSWORD=admin123 + +# Configuration de l'API +API_PORT=3000 + +# Secrets pour l'authentification JWT +JWT_SECRET=dev-jwt-secret-key-change-me +JWT_EXPIRATION_TIME=7d + +# Environnement +NODE_ENV=development diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..410379a --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,64 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +.env + + +# Tests bdd +.vscode/ +BDD.sql +migrations/ +src/seed/ diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b1d64fd --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,42 @@ +FROM node:22-alpine AS builder + +WORKDIR /app + +# Copier les fichiers de configuration +COPY package*.json ./ +COPY tsconfig*.json ./ +COPY nest-cli.json ./ + +# Installer TOUTES les dépendances (dev + prod pour le build) +RUN npm install && npm cache clean --force + +# Copier le code source +COPY src ./src + +# Builder l'application +RUN npm run build + +# Stage production +FROM node:22-alpine AS production + +WORKDIR /app + +# Installer seulement les dépendances de production +COPY package*.json ./ +RUN npm install --only=production && npm cache clean --force + +# Copier le build depuis le stage builder +COPY --from=builder /app/dist ./dist + +# Créer un utilisateur non-root +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nestjs -u 1001 + +# Créer le dossier uploads et donner les permissions +RUN mkdir -p /app/uploads/photos && chown -R nestjs:nodejs /app/uploads + +USER nestjs + +EXPOSE 3000 + +CMD ["node", "dist/main"] diff --git a/backend/README-DEV.md b/backend/README-DEV.md new file mode 100644 index 0000000..562655d --- /dev/null +++ b/backend/README-DEV.md @@ -0,0 +1,63 @@ +# 🚀 Guide de développement local + +## Prérequis +- Docker et Docker Compose installés +- Git + +## 🏃‍♂️ Démarrage rapide + +### 1. Cloner le projet +```bash +git clone +cd ptitspas-backend +``` + +### 2. Configuration de l'environnement +```bash +# Copier le fichier d'exemple +cp .env.example .env + +# Optionnel : adapter les valeurs dans .env selon vos besoins +``` + +### 3. Lancer l'application +```bash +# Démarrer tous les services (PostgreSQL + PgAdmin + Backend) +docker compose -f docker-compose.dev.yml up -d + +# Voir les logs +docker compose -f docker-compose.dev.yml logs -f +``` + +## 🌐 Accès aux services + +- **Backend API** : http://localhost:3000 +- **PgAdmin** : http://localhost:8080 + - Email : admin@localhost + - Mot de passe : admin123 +- **PostgreSQL** : localhost:5432 + - Utilisateur : admin + - Mot de passe : admin123 + - Base : ptitpas_db + +## 🛠️ Commandes utiles + +```bash +# Arrêter les services +docker compose -f docker-compose.dev.yml down + +# Rebuild le backend après modification du Dockerfile +docker compose -f docker-compose.dev.yml up --build backend + +# Voir l'état des services +docker compose -f docker-compose.dev.yml ps + +# Accéder au container backend +docker exec -it ptitspas-backend-dev sh +``` + +## 📝 Notes de développement + +- Les modifications du code source sont automatiquement prises en compte (hot reload) +- Les données PostgreSQL sont persistantes via le volume `postgres_dev_data` +- Le fichier `.env` n'est pas versionné pour des raisons de sécurité diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..96c0ca4 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,158 @@ +# P'titsPas API Backend ✨ + +Ce dépôt contient le code source de l'API backend pour la plateforme **P'titsPas**. L'API est construite avec NestJS et est responsable de toute la logique métier, de la gestion des données et de l'authentification des utilisateurs. + +--- + +## 📚 Table des matières + +- [Technologies utilisées](#-technologies-utilisées) +- [Prérequis](#-prérequis) +- [Installation](#-installation) +- [Lancement de l'application](#-lancement-de-lapplication) +- [Scripts principaux](#-scripts-principaux) +- [Tests](#-tests) +- [Gestion des migrations](#-gestion-des-migrations) +- [Documentation de l'API](#-documentation-de-lapi) + +--- + +## 🛠️ Technologies utilisées + +- **Framework**: [NestJS](https://nestjs.com/) (TypeScript) +- **Base de données**: [PostgreSQL](https://www.postgresql.org/) +- **ORM**: [TypeORM](https://typeorm.io/) +- **Authentification**: JWT avec [Passport.js](http://www.passportjs.org/) +- **Stockage Fichiers**: [MinIO](https://min.io/) (Compatible S3) +- **Tâches Asynchrones**: [Redis](https://redis.io/) avec [BullMQ](https://bullmq.io/) +- **Conteneurisation**: [Docker](https://www.docker.com/) + +--- + +## 📋 Prérequis + +Avant de commencer, assurez-vous d'avoir installé les outils suivants sur votre machine : + +- [Node.js](https://nodejs.org/) (v18 ou supérieure) +- [npm](https://www.npmjs.com/) ou [pnpm](https://pnpm.io/) +- [Docker](https://www.docker.com/products/docker-desktop/) et Docker Compose +- [Git](https://git-scm.com/) + +--- + +## 🚀 Installation + +1. **Clonez le dépôt :** + + ```bash + git clone https://github.com/votre-username/ptitspas-backend.git + cd ptitspas-backend + ``` + +2. **Créez le fichier d'environnement :** + Copiez le fichier d'exemple `.env.example` et renommez-le en `.env`. Ce fichier est ignoré par Git et contiendra vos secrets locaux. + + ```bash + cp .env.example .env + ``` + + ➡️ **Important :** Ouvrez le fichier `.env` et remplissez les variables (identifiants de la base de données, secrets JWT, etc.). + +3. **Installez les dépendances du projet :** + ```bash + npm install + ``` + +--- + +## ▶️ Lancement de l'application + +### Méthode recommandée : avec Docker + +Cette méthode lance l'ensemble des services nécessaires (API, base de données, MinIO, Redis) dans des conteneurs isolés. + +```bash +docker-compose up --build +``` + +L'API sera accessible à l'adresse `http://localhost:3000` (ou le port que vous avez configuré dans votre `.env`). + +### Méthode locale (pour le développement) + +Cette méthode ne lance que le serveur NestJS. Assurez-vous que les autres services (PostgreSQL, Redis, etc.) sont déjà en cours d'exécution (par exemple, via Docker). + +``` +npm run start:dev +``` + +Le serveur redémarrera automatiquement à chaque modification de fichier. + +--- + +## ⚙️ Scripts principaux + +| Commande | Description | +| :------------------ | :-------------------------------------------------------------------- | +| `npm run start:dev` | Lance le serveur en mode développement avec rechargement automatique. | +| `npm run build` | Compile le projet TypeScript en JavaScript. | +| `npm start` | Lance l'application depuis les fichiers compilés (mode production). | +| `npm run lint` | Analyse le code pour détecter les erreurs de style et de syntaxe. | + +--- + +## 🧪 Tests + +Pour lancer les tests, utilisez les commandes suivantes : + +| Commande | Description | +| :----------------- | :--------------------------------------------------------------- | +| `npm test` | Lance les tests unitaires. | +| `npm run test:e2e` | Lance les tests de bout en bout (end-to-end). | +| `npm run test:cov` | Lance tous les tests et génère un rapport de couverture de code. | + +--- + +## 🗄️ Gestion des migrations + +La structure de la base de données est gérée par des fichiers de migration TypeORM. + +1. **Générer une nouvelle migration :** + Après avoir modifié une entité TypeORM, générez automatiquement le fichier de migration correspondant. + + ```bash + npm run migration:generate -- src/database/migrations/NomDeLaMigration + ``` + +2. **Appliquer les migrations :** + Pour mettre à jour le schéma de votre base de données avec les nouvelles migrations. + ```bash + npm run migration:run + ``` + +--- + +## 📖 Documentation de l'API + +Une fois l'application lancée, la documentation de l'API générée avec **Swagger (OpenAPI)** est disponible à l'adresse suivante : + +➡️ **[http://localhost:3000/api-docs](http://localhost:3000/api-docs)** + +Cette interface vous permet d'explorer et de tester toutes les routes de l'API directement depuis votre navigateur. + +Excellente idée. C'est un élément crucial qui définit les droits et les devoirs liés à votre code. + +En me basant sur la section `10.5 Propriété intellectuelle et licence de l’application` de votre cahier des charges, j'ai rédigé une section "Licence" qui reflète précisément le statut propriétaire de votre projet. + +Voici le `README.md` complet et mis à jour. + +--- + +## 📜 Licence + +Ce projet est distribué sous une **licence propriétaire**. + +Le code source, la marque "P'titsPas" et la documentation associée sont la propriété exclusive de l'éditeur, Julien MARTIN. + +Toute reproduction, distribution, modification ou utilisation du code source est strictement interdite sans un accord écrit préalable de l'auteur. Les clients et partenaires autorisés disposent d'une licence d'utilisation non-exclusive et non-transférable, conformément aux termes de leur contrat. + +Pour toute question relative à l'utilisation ou à l'acquisition d'une licence, veuillez contacter l'auteur. diff --git a/backend/docker-compose.dev.yml b/backend/docker-compose.dev.yml new file mode 100644 index 0000000..0da2713 --- /dev/null +++ b/backend/docker-compose.dev.yml @@ -0,0 +1,72 @@ +# Docker Compose pour développement local +# Usage: docker compose -f docker-compose.dev.yml up -d + +services: + # Base de données PostgreSQL + postgres: + image: postgres:17 + container_name: ptitspas-postgres-dev + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123} + POSTGRES_DB: ${POSTGRES_DB:-ptitpas_db} + ports: + - "5433:5432" + volumes: + # Si le fichier d'init existe dans le dépôt database + - ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql + - postgres_dev_data:/var/lib/postgresql/data + + networks: + - ptitspas_dev + + # Interface d'administration DB + pgadmin: + image: dpage/pgadmin4 + container_name: ptitspas-pgadmin-dev + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@ptits-pas.fr} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin123} + ports: + - "8080:80" + depends_on: + - postgres + networks: + - ptitspas_dev + + # Backend NestJS + backend: + build: + context: . + dockerfile: Dockerfile + container_name: ptitspas-backend-dev + restart: unless-stopped + environment: + POSTGRES_HOST: ${POSTGRES_HOST:-postgres} + POSTGRES_PORT: ${POSTGRES_PORT:-5432} + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123} + POSTGRES_DB: ${POSTGRES_DB:-ptitpas_db} + API_PORT: ${API_PORT:-3000} + JWT_SECRET: ${JWT_SECRET:-dev-jwt-secret-key} + JWT_EXPIRATION_TIME: ${JWT_EXPIRATION_TIME:-7d} + NODE_ENV: ${NODE_ENV:-development} + ports: + - "3000:3000" + depends_on: + - postgres + volumes: + # Pour le hot reload en développement + - ./src:/app/src + - /app/node_modules + networks: + - ptitspas_dev + +volumes: + postgres_dev_data: + +networks: + ptitspas_dev: + driver: bridge diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs new file mode 100644 index 0000000..caebf6e --- /dev/null +++ b/backend/eslint.config.mjs @@ -0,0 +1,34 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn' + }, + }, +); \ No newline at end of file diff --git a/backend/nest-cli.json b/backend/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/backend/package-lock.json b/backend/package-lock.json index 17b230d..3c2f567 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,41 +1,754 @@ { - "name": "petitspas-backend", - "version": "1.0.0", + "name": "ptitspas-ynov-back", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "petitspas-backend", - "version": "1.0.0", + "name": "ptitspas-ynov-back", + "version": "0.0.1", + "license": "UNLICENSED", "dependencies": { - "@nestjs/common": "^11.1.0", - "@prisma/client": "^6.7.0", - "@types/jsonwebtoken": "^9.0.9", - "bcrypt": "^5.1.1", - "cors": "^2.8.5", - "express": "^4.18.2", - "helmet": "^7.1.0", - "jsonwebtoken": "^9.0.2", - "morgan": "^1.10.0" + "@nestjs/common": "^11.1.6", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/mapped-types": "^2.1.0", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.0", + "@nestjs/typeorm": "^11.0.0", + "@sentry/nestjs": "^10.10.0", + "bcrypt": "^6.0.0", + "bcryptjs": "^3.0.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "joi": "^18.0.0", + "mapped-types": "^0.0.1", + "passport-jwt": "^4.0.1", + "pg": "^8.16.3", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1", + "typeorm": "^0.3.26" }, "devDependencies": { - "@types/bcrypt": "^5.0.2", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/helmet": "^4.0.0", - "@types/morgan": "^1.9.9", - "@types/node": "^20.11.19", - "prisma": "^6.7.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.10", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/bcrypt": "^6.0.0", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/node": "^22.10.7", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^6.0.2", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^16.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", "ts-node": "^10.9.2", - "ts-node-dev": "^2.0.0", - "typescript": "^5.3.3" + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.15.tgz", + "integrity": "sha512-pU2RZYX6vhd7uLSdLwPnuBcr0mXJSjp3EgOXKsrlQFQZevc+Qs+2JdXgIElnOT/aDqtRtriDmLlSbtdE8n3ZbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "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/@angular-devkit/core/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.15.tgz", + "integrity": "sha512-kNOJ+3vekJJCQKWihNmxBkarJzNW09kP5a9E1SRNiQVNOUEeSwcRR0qYotM65nx821gNzjjhJXnAZ8OazWldrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.15", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.17", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.15.tgz", + "integrity": "sha512-1ESFmFGMpGQmalDB3t2EtmWDGv6gOFYBMxmHO2f1KI/UDl8UmZnCGL4mD3EWo8Hv0YIsZ9wOH9Q7ZHNYjeSpzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.15", + "@angular-devkit/schematics": "19.2.15", + "@inquirer/prompts": "7.3.2", + "ansi-colors": "4.1.3", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@inquirer/prompts": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" } }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -44,457 +757,1323 @@ "node": ">=12" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.2.tgz", + "integrity": "sha512-1jkwm1WY9VIb6WhdANRmWDkXQUcIRpxqZpSdS+HD9vhoVL3zwoFvP8doQNEgT6k3VST0Ewu50wZnXIceRYp5tw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.2.0.tgz", + "integrity": "sha512-fdSw07FLJEU5vbpOPzXo5c6xmMGDzbZE2+niuDHX5N6mc6V0Ebso/q3xiHra4D73+PMsC8MJmcaZKuAAoaQsSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.14.tgz", + "integrity": "sha512-5yR4IBfe0kXe59r1YCTG8WXkUbl7Z35HK87Sw+WUyGD8wNUx7JvY7laahzeytyE1oLn74bQnL7hstctQxisQ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.15", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", + "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.15.tgz", + "integrity": "sha512-wst31XT8DnGOSS4nNJDIklGKnf+8shuauVrWzgKegWUe28zfCftcWZ2vktGdzJgcylWSS2SrDnYUb6alZcwnCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.17.tgz", + "integrity": "sha512-PSqy9VmJx/VbE3CT453yOfNa+PykpKg/0SYP7odez1/NWBGuDXgPhp4AeGYYKjhLn5lUUavVS/JbeYMPdH50Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "dev": true, + "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", - "cpu": [ - "arm" - ], + "node_modules/@inquirer/input": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.2.1.tgz", + "integrity": "sha512-tVC+O1rBl0lJpoUZv4xY+WGWY8V5b0zxU1XDsMsIHYregdh7bN5X5QnIONNBAl0K765FYlAfNHS2Bhn7SSOVow==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", - "cpu": [ - "arm64" - ], + "node_modules/@inquirer/number": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.17.tgz", + "integrity": "sha512-GcvGHkyIgfZgVnnimURdOueMk0CztycfC8NZTiIY9arIAkeOgt6zG57G+7vC59Jns3UX27LMkPKnKWAOF5xEYg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", - "cpu": [ - "x64" - ], + "node_modules/@inquirer/password": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.17.tgz", + "integrity": "sha512-DJolTnNeZ00E1+1TW+8614F7rOJJCM4y4BAGQ3Gq6kQIG+OJ4zr3GLjIjVVJCbKsk2jmkmv6v2kQuN/vriHdZA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", - "cpu": [ - "arm64" - ], + "node_modules/@inquirer/prompts": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.8.0.tgz", + "integrity": "sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@inquirer/checkbox": "^4.2.0", + "@inquirer/confirm": "^5.1.14", + "@inquirer/editor": "^4.2.15", + "@inquirer/expand": "^4.0.17", + "@inquirer/input": "^4.2.1", + "@inquirer/number": "^3.0.17", + "@inquirer/password": "^4.0.17", + "@inquirer/rawlist": "^4.1.5", + "@inquirer/search": "^3.1.0", + "@inquirer/select": "^4.3.1" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", - "cpu": [ - "x64" - ], + "node_modules/@inquirer/rawlist": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.5.tgz", + "integrity": "sha512-R5qMyGJqtDdi4Ht521iAkNqyB6p2UPuZUbMifakg1sWtu24gc2Z8CJuw8rP081OckNDMgtDCuLe42Q2Kr3BolA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", - "cpu": [ - "arm64" - ], + "node_modules/@inquirer/search": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz", + "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "yoctocolors-cjs": "^2.1.2" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", - "cpu": [ - "x64" - ], + "node_modules/@inquirer/select": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.3.1.tgz", + "integrity": "sha512-Gfl/5sqOF5vS/LIrSndFgOh7jgoe0UXEizDqahFRkq5aJBLegZ6WjuMh/hVEJwlFQjyLq1z9fRtvUMkb7jM1LA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@inquirer/core": "^10.1.15", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", - "cpu": [ - "arm" - ], + "node_modules/@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", - "cpu": [ - "arm64" - ], + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": "20 || >=22" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", - "cpu": [ - "ia32" - ], + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, "engines": { - "node": ">=18" + "node": "20 || >=22" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", - "cpu": [ - "mips64el" - ], - "dev": true, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", - "cpu": [ - "s390x" - ], + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", - "cpu": [ - "x64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", - "cpu": [ - "arm64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", - "cpu": [ - "x64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", - "cpu": [ - "arm64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "p-locate": "^4.1.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", - "cpu": [ - "x64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", - "cpu": [ - "x64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "p-limit": "^2.2.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", - "cpu": [ - "arm64" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", - "cpu": [ - "ia32" - ], + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", - "cpu": [ - "x64" - ], + "node_modules/@jest/console": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", + "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "slash": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", + "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.5", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.5", + "jest-config": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-resolve-dependencies": "30.0.5", + "jest-runner": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "jest-watcher": "30.0.5", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.0.5", + "jest-snapshot": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", + "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", + "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", + "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", + "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", + "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.5", + "@jest/types": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", + "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.0.5", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", + "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@lukeed/csprng": { @@ -506,49 +2085,249 @@ "node": ">=8" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "license": "BSD-3-Clause", + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" } }, - "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", + "node_modules/@nestjs/cli": { + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.10.tgz", + "integrity": "sha512-4waDT0yGWANg0pKz4E47+nUrqIJv/UqrZ5wLPkCqc7oMGRMWKAaw1NDZ9rKsaqhqvxb2LfI5+uXOWr4yi94DOQ==", + "dev": true, + "license": "MIT", "dependencies": { - "glob": "^7.1.3" + "@angular-devkit/core": "19.2.15", + "@angular-devkit/schematics": "19.2.15", + "@angular-devkit/schematics-cli": "19.2.15", + "@inquirer/prompts": "7.8.0", + "@nestjs/schematics": "^11.0.1", + "ansis": "4.1.0", + "chokidar": "4.0.3", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", + "glob": "11.0.3", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.8.3", + "webpack": "5.100.2", + "webpack-node-externals": "3.0.0" }, "bin": { - "rimraf": "bin.js" + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 20.11" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "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": { - "url": "https://github.com/sponsors/isaacs" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@nestjs/cli/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nestjs/cli/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@nestjs/cli/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nestjs/cli/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.100.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.2.tgz", + "integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.2", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } } }, "node_modules/@nestjs/common": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.0.tgz", - "integrity": "sha512-8MrajltjtIN6eW9cTpv+1IZogqz2Zsrc8YDt0LwQPUq8cSq0j50DETdQpPsNMeib+p9avkV41+NrzGk1z2o5Wg==", + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz", + "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==", "license": "MIT", "dependencies": { - "file-type": "20.4.1", + "file-type": "21.0.0", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", @@ -559,8 +2338,8 @@ "url": "https://opencollective.com/nest" }, "peerDependencies": { - "class-transformer": "*", - "class-validator": "*", + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, @@ -573,89 +2352,1068 @@ } } }, - "node_modules/@prisma/client": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz", - "integrity": "sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" }, "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.5.tgz", + "integrity": "sha512-Qr25MEY9t8VsMETy7eXQ0cNXqu0lzuFrrTr+f+1G57ABCtV5Pogm7n9bF71OU2bnkDD32Bi4hQLeFR90cku3Tw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.2.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" }, "peerDependenciesMeta": { - "prisma": { + "@nestjs/microservices": { "optional": true }, - "typescript": { + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { "optional": true } } }, - "node_modules/@prisma/config": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.7.0.tgz", - "integrity": "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/@nestjs/jwt": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", + "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "license": "MIT", "dependencies": { - "esbuild": ">=0.12 <1", - "esbuild-register": "3.6.0" + "@types/jsonwebtoken": "9.0.7", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" } }, - "node_modules/@prisma/debug": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.7.0.tgz", - "integrity": "sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w==", - "devOptional": true, - "license": "Apache-2.0" + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } }, - "node_modules/@prisma/engines": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.7.0.tgz", - "integrity": "sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw==", - "devOptional": true, + "node_modules/@nestjs/platform-express": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz", + "integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==", + "license": "MIT", + "dependencies": { + "cors": "2.8.5", + "express": "5.1.0", + "multer": "2.0.2", + "path-to-regexp": "8.2.0", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.7.tgz", + "integrity": "sha512-t8dNYYMwEeEsrlwc2jbkfwCfXczq4AeNEgx1KVQuJ6wYibXk0ZbXbPdfp8scnEAaQv1grpncNV5gWgzi7ZwbvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.2.15", + "@angular-devkit/schematics": "19.2.15", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/swagger": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.0.tgz", + "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "8.2.0", + "swagger-ui-dist": "5.21.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.5.tgz", + "integrity": "sha512-ZYRYF750SefmuIo7ZqPlHDcin1OHh6My0OkOfGEFjrD9mJ0vMVIpwMTOOkpzCfCcpqUuxeHBuecpiIn+NLrQbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", + "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.1.0.tgz", + "integrity": "sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.1.0.tgz", + "integrity": "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", + "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.203.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.50.0.tgz", + "integrity": "sha512-kwNs/itehHG/qaQBcVrLNcvXVPW0I4FCOVtw3LHMLdYIqD7GJ6Yv2nX+a4YHjzbzIeRYj8iyMp0Bl7tlkidq5w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.47.0.tgz", + "integrity": "sha512-pjenvjR6+PMRb6/4X85L4OtkQCootgb/Jzh/l/Utu3SJHBid1F+gk9sTGU2FWuhhEfV6P7MZ7BmCdHXQjgJ42g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.21.1.tgz", + "integrity": "sha512-hNAm/bwGawLM8VDjKR0ZUDJ/D/qKR3s6lA5NV+btNaPVm2acqhPcT47l2uCVi+70lng2mywfQncor9v8/ykuyw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.52.0.tgz", + "integrity": "sha512-W7pizN0Wh1/cbNhhTf7C62NpyYw7VfCFTYg0DYieSTrtPBT1vmoSZei19wfKLnrMsz3sHayCg0HxCVL2c+cz5w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.23.0.tgz", + "integrity": "sha512-Puan+QopWHA/KNYvDfOZN6M/JtF6buXEyD934vrb8WhsX1/FuM7OtoMlQyIqAadnE8FqqDL4KDPiEfCQH6pQcQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.47.0.tgz", + "integrity": "sha512-UfHqf3zYK+CwDwEtTjaD12uUqGGTswZ7ofLBEdQ4sEJp9GHSSJMQ2hT3pgBxyKADzUdoxQAv/7NqvL42ZI+Qbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.51.0.tgz", + "integrity": "sha512-LchkOu9X5DrXAnPI1+Z06h/EH/zC7D6sA86hhPrk3evLlsJTz0grPrkL/yUJM9Ty0CL/y2HSvmWQCjbJEz/ADg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.50.0.tgz", + "integrity": "sha512-5xGusXOFQXKacrZmDbpHQzqYD1gIkrMWuwvlrEPkYOsjUqGUjl1HbxCsn5Y9bUXOCgP1Lj6A4PcKt1UiJ2MujA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.203.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.203.0.tgz", + "integrity": "sha512-y3uQAcCOAwnO6vEuNVocmpVzG3PER6/YZqbPbbffDdJ9te5NkHEkfSMNzlC3+v7KlE+WinPGc3N7MR30G1HY2g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.0.1", + "@opentelemetry/instrumentation": "0.203.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.51.0.tgz", + "integrity": "sha512-9IUws0XWCb80NovS+17eONXsw1ZJbHwYYMXiwsfR9TSurkLV5UNbRSKb9URHO+K+pIJILy9wCxvyiOneMr91Ig==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/redis-common": "^0.38.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.13.0.tgz", + "integrity": "sha512-FPQyJsREOaGH64hcxlzTsIEQC4DYANgTwHjiB7z9lldmvua1LRMVn3/FfBlzXoqF179B0VGYviz6rn75E9wsDw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.48.0.tgz", + "integrity": "sha512-V5wuaBPv/lwGxuHjC6Na2JFRjtPgstw19jTFl1B1b6zvaX8zVDYUDaR5hL7glnQtUSCMktPttQsgK4dhXpddcA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.33.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.51.0.tgz", + "integrity": "sha512-XNLWeMTMG1/EkQBbgPYzCeBD0cwOrfnn8ao4hWgLv0fNCFQu1kCsJYygz2cvKuCs340RlnG4i321hX7R8gj3Rg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.48.0.tgz", + "integrity": "sha512-KUW29wfMlTPX1wFz+NNrmE7IzN7NWZDrmFWHM/VJcmFEuQGnnBuTIdsP55CnBDxKgQ/qqYFp4udQFNtjeFosPw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.56.0.tgz", + "integrity": "sha512-YG5IXUUmxX3Md2buVMvxm9NWlKADrnavI36hbJsihqqvBGsWnIfguf0rUP5Srr0pfPqhQjUP+agLMsvu0GmUpA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.50.0.tgz", + "integrity": "sha512-Am8pk1Ct951r4qCiqkBcGmPIgGhoDiFcRtqPSLbJrUZqEPUsigjtMjoWDRLG1Ki1NHgOF7D0H7d+suWz1AAizw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.49.0.tgz", + "integrity": "sha512-QU9IUNqNsrlfE3dJkZnFHqLjlndiU39ll/YAAEvWE40sGOCi9AtOF6rmEGzJ1IswoZ3oyePV7q2MP8SrhJfVAA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.27" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.50.0.tgz", + "integrity": "sha512-PoOMpmq73rOIE3nlTNLf3B1SyNYGsp7QXHYKmeTZZnJ2Ou7/fdURuOhWOI0e6QZ5gSem18IR1sJi6GOULBQJ9g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.41.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-nestjs-core": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.49.0.tgz", + "integrity": "sha512-1R/JFwdmZIk3T/cPOCkVvFQeKYzbbUvDxVH3ShXamUwBlGkdEu5QJitlRMyVNZaHkKZKWgYrBarGQsqcboYgaw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.30.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.55.0.tgz", + "integrity": "sha512-yfJ5bYE7CnkW/uNsnrwouG/FR7nmg09zdk2MSs7k0ZOMkDDAE3WBGpVFFApGgNu2U+gtzLgEzOQG4I/X+60hXw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.41.0", + "@types/pg": "8.15.4", + "@types/pg-pool": "2.0.6" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.51.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.51.0.tgz", + "integrity": "sha512-uL/GtBA0u72YPPehwOvthAe+Wf8k3T+XQPBssJmTYl6fzuZjNq8zTfxVFhl9nRFjFVEe+CtiYNT0Q3AyqW1Z0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/redis-common": "^0.38.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.22.0.tgz", + "integrity": "sha512-XrrNSUCyEjH1ax9t+Uo6lv0S2FCCykcF7hSxBMxKf7Xn0bPRxD3KyFUZy25aQXzbbbUHhtdxj3r2h88SfEM3aA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.14.0.tgz", + "integrity": "sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.0.tgz", + "integrity": "sha512-4Wc0AWURII2cfXVVoZ6vDqK+s5n4K5IssdrlVrvGsx6OEOKdghKtJZqXAHWFiZv4nTDLH2/2fldjIHY8clMOjQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.1.0.tgz", + "integrity": "sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.1.0.tgz", + "integrity": "sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.1.0", + "@opentelemetry/resources": "2.1.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.37.0.tgz", + "integrity": "sha512-JD6DerIKdJGmRp4jQyX5FlrQjA4tjOw1cvfsPAZXfOOEErMUHjPcPSICS+6WnM0nB0efSFARh0KAZss+bvExOA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.0.tgz", + "integrity": "sha512-pmzXctVbEERbqSfiAgdes9Y63xjoOyXcD7B6IXBkVb+vbM7M9U98mn33nGXxPf4dfYR0M+vhcKRZmbSJ7HfqFA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@prisma/instrumentation": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.14.0.tgz", + "integrity": "sha512-Po/Hry5bAeunRDq0yAQueKookW3glpP+qjjvvyOfm6dI2KG5/Y6Bgg3ahyWd7B0u2E+Wf9xRk2rtdda7ySgK1A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.8" + } + }, + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", + "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", + "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.57.2", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "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", - "dependencies": { - "@prisma/debug": "6.7.0", - "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "@prisma/fetch-engine": "6.7.0", - "@prisma/get-platform": "6.7.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed.tgz", - "integrity": "sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA==", - "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@prisma/fetch-engine": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.7.0.tgz", - "integrity": "sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.7.0", - "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", - "@prisma/get-platform": "6.7.0" + "node_modules/@sentry/core": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.10.0.tgz", + "integrity": "sha512-4O1O6my/vYE98ZgfEuLEwOOuHzqqzfBT6IdRo1yiQM7/AXcmSl0H/k4HJtXCiCTiHm+veEuTDBHp0GQZmpIbtA==", + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/@prisma/get-platform": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.7.0.tgz", - "integrity": "sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw==", - "devOptional": true, - "license": "Apache-2.0", + "node_modules/@sentry/nestjs": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/@sentry/nestjs/-/nestjs-10.10.0.tgz", + "integrity": "sha512-qMO2XbEdUKsuB0DypGlcMzPepI28RkVnnfVS2rTTFtieeQErHl5hX/NHj2oeQQRSW7lpEXbEp2e7wESgRU7MZg==", + "license": "MIT", "dependencies": { - "@prisma/debug": "6.7.0" + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/instrumentation-nestjs-core": "0.49.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@sentry/core": "10.10.0", + "@sentry/node": "10.10.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" } }, + "node_modules/@sentry/node": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.10.0.tgz", + "integrity": "sha512-GdI/ELIipKhdL8gdvnRLtz1ItPzAXRCZrvTwGMd5C+kDRALakQIR7pONC9nf5TKCG2UaslHEX+2XDImorhM7OA==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.0.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/instrumentation-amqplib": "0.50.0", + "@opentelemetry/instrumentation-connect": "0.47.0", + "@opentelemetry/instrumentation-dataloader": "0.21.1", + "@opentelemetry/instrumentation-express": "0.52.0", + "@opentelemetry/instrumentation-fs": "0.23.0", + "@opentelemetry/instrumentation-generic-pool": "0.47.0", + "@opentelemetry/instrumentation-graphql": "0.51.0", + "@opentelemetry/instrumentation-hapi": "0.50.0", + "@opentelemetry/instrumentation-http": "0.203.0", + "@opentelemetry/instrumentation-ioredis": "0.51.0", + "@opentelemetry/instrumentation-kafkajs": "0.13.0", + "@opentelemetry/instrumentation-knex": "0.48.0", + "@opentelemetry/instrumentation-koa": "0.51.0", + "@opentelemetry/instrumentation-lru-memoizer": "0.48.0", + "@opentelemetry/instrumentation-mongodb": "0.56.0", + "@opentelemetry/instrumentation-mongoose": "0.50.0", + "@opentelemetry/instrumentation-mysql": "0.49.0", + "@opentelemetry/instrumentation-mysql2": "0.50.0", + "@opentelemetry/instrumentation-pg": "0.55.0", + "@opentelemetry/instrumentation-redis": "0.51.0", + "@opentelemetry/instrumentation-tedious": "0.22.0", + "@opentelemetry/instrumentation-undici": "0.14.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@prisma/instrumentation": "6.14.0", + "@sentry/core": "10.10.0", + "@sentry/node-core": "10.10.0", + "@sentry/opentelemetry": "10.10.0", + "import-in-the-middle": "^1.14.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.10.0.tgz", + "integrity": "sha512-7jHM1Is0Si737SVA0sHPg7lj7OmKoNM+f7+E3ySvtHIUeSINZBLM6jg1q57R1kIg8eavpHXudYljRMpuv/8bYA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.10.0", + "@sentry/opentelemetry": "10.10.0", + "import-in-the-middle": "^1.14.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/resources": "^1.30.1 || ^2.0.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@sentry/node/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@sentry/node/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.10.0.tgz", + "integrity": "sha512-EQ5/1Ps4n1JosmaDiFCyb5iByjjKja2pnmeMiLzTDZ5Zikjs/3GKzmh+SgTRFLOm6yKgQps0GdiCH2gxdrbONg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.10.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@tokenizer/inflate": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", @@ -674,29 +3432,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@tokenizer/inflate/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@tokenizer/inflate/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/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -707,34 +3442,90 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/bcrypt": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", - "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -742,9 +3533,9 @@ } }, "node_modules/@types/body-parser": { - "version": "1.19.5", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dev": true, "license": "MIT", "dependencies": { @@ -756,39 +3547,63 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@types/estree": "*", + "@types/json-schema": "*" } }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", + "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -798,34 +3613,74 @@ "@types/send": "*" } }, - "node_modules/@types/helmet": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/helmet/-/helmet-4.0.0.tgz", - "integrity": "sha512-ONIn/nSNQA57yRge3oaMQESef/6QhoeX7llWeDli0UZIfz8TQMkfNPTXA8VnnyeA1WUjG2pGqdjEIueYonMdfQ==", - "deprecated": "This is a stub types definition. helmet provides its own type definitions, so you do not need this installed.", + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "license": "MIT", "dependencies": { - "helmet": "*" + "@types/istanbul-lib-coverage": "*" } }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "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==", "dev": true, "license": "MIT" }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", - "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", "license": "MIT", "dependencies": { - "@types/ms": "*", "@types/node": "*" } }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -833,35 +3688,80 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/morgan": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", - "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", - "dev": true, + "node_modules/@types/mysql": { + "version": "2.15.27", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz", + "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==", "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, "node_modules/@types/node": { - "version": "20.17.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.32.tgz", - "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", + "version": "22.17.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz", + "integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.15.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", + "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" } }, "node_modules/@types/qs": { - "version": "6.9.18", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", - "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "dev": true, "license": "MIT" }, @@ -873,9 +3773,9 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "dev": true, "license": "MIT", "dependencies": { @@ -884,9 +3784,9 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.7", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "dev": true, "license": "MIT", "dependencies": { @@ -895,44 +3795,801 @@ "@types/send": "*" } }, - "node_modules/@types/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true, "license": "MIT" }, - "node_modules/@types/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/validator": { + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "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/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -941,11 +4598,43 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -954,48 +4643,136 @@ "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "4" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">= 6.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "ajv": "^8.0.0" }, - "engines": { - "node": ">=6.0" + "peerDependencies": { + "ajv": "^8.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "ajv": { "optional": true } } }, - "node_modules/agent-base/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==", + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "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-formats/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==", + "dev": true, "license": "MIT" }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.1.0.tgz", + "integrity": "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" } }, "node_modules/anymatch": { @@ -1012,118 +4789,267 @@ "node": ">= 8" } }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "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/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", "dev": true, "license": "MIT" }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", + "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.0.5", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "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": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" }, "engines": { - "node": ">= 10.0.0" + "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==", + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1143,6 +5069,87 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1153,9 +5160,19 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1165,6 +5182,24 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1194,65 +5229,382 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "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" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">=10" }, "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, "license": "ISC", - "bin": { - "color-support": "bin.js" + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "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": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -1270,19 +5622,43 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "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.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "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/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, "license": "MIT" }, "node_modules/cors": { @@ -1298,28 +5674,148 @@ "node": ">= 0.10" } }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "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/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", "license": "MIT" }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1329,35 +5825,64 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=8" } }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" } }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "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", @@ -1372,15 +5897,11 @@ "node": ">= 0.4" } }, - "node_modules/dynamic-dedupe": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", - "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - } + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", @@ -1397,6 +5918,26 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/electron-to-chromium": { + "version": "1.5.194", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", + "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1412,6 +5953,30 @@ "node": ">= 0.8" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1430,6 +5995,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -1442,84 +6014,30 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", - "devOptional": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.3", - "@esbuild/android-arm": "0.25.3", - "@esbuild/android-arm64": "0.25.3", - "@esbuild/android-x64": "0.25.3", - "@esbuild/darwin-arm64": "0.25.3", - "@esbuild/darwin-x64": "0.25.3", - "@esbuild/freebsd-arm64": "0.25.3", - "@esbuild/freebsd-x64": "0.25.3", - "@esbuild/linux-arm": "0.25.3", - "@esbuild/linux-arm64": "0.25.3", - "@esbuild/linux-ia32": "0.25.3", - "@esbuild/linux-loong64": "0.25.3", - "@esbuild/linux-mips64el": "0.25.3", - "@esbuild/linux-ppc64": "0.25.3", - "@esbuild/linux-riscv64": "0.25.3", - "@esbuild/linux-s390x": "0.25.3", - "@esbuild/linux-x64": "0.25.3", - "@esbuild/netbsd-arm64": "0.25.3", - "@esbuild/netbsd-x64": "0.25.3", - "@esbuild/openbsd-arm64": "0.25.3", - "@esbuild/openbsd-x64": "0.25.3", - "@esbuild/sunos-x64": "0.25.3", - "@esbuild/win32-arm64": "0.25.3", - "@esbuild/win32-ia32": "0.25.3", - "@esbuild/win32-x64": "0.25.3" - } - }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", - "devOptional": true, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" - } - }, - "node_modules/esbuild-register/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">= 0.4" } }, - "node_modules/esbuild-register/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==", - "devOptional": true, - "license": "MIT" + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } }, "node_modules/escape-html": { "version": "1.0.3", @@ -1527,6 +6045,235 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", + "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "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", @@ -1536,71 +6283,278 @@ "node": ">= 0.6" } }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.0.5", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.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/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "license": "MIT" }, - "node_modules/file-type": { - "version": "20.4.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", - "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "license": "MIT", "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-type": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -1620,23 +6574,177 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "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": ">= 0.8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1646,43 +6754,48 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "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.6" + "node": ">= 0.8" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", "dependencies": { - "minipass": "^3.0.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">= 8" + "node": ">=12" } }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -1709,25 +6822,23 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { @@ -1754,6 +6865,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -1767,38 +6888,90 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "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==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/gopd": { @@ -1813,6 +6986,84 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1825,11 +7076,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/hasown": { "version": "2.0.2", @@ -1843,14 +7103,12 @@ "node": ">= 0.4" } }, - "node_modules/helmet": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", - "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" }, "node_modules/http-errors": { "version": "2.0.0", @@ -1868,49 +7126,32 @@ "node": ">= 0.8" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, "engines": { - "node": ">= 6" + "node": ">= 0.8" } }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=10.17.0" } }, - "node_modules/https-proxy-agent/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/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -1936,11 +7177,87 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz", + "integrity": "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/import-in-the-middle/node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -1962,24 +7279,29 @@ "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==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, + "license": "MIT" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -2010,6 +7332,16 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2023,6 +7355,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2033,6 +7375,136 @@ "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/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterare": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", @@ -2042,6 +7514,908 @@ "node": ">=6" } }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", + "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.0.5", + "@jest/types": "30.0.5", + "import-local": "^3.2.0", + "jest-cli": "30.0.5" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.0.5", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", + "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "p-limit": "^3.1.0", + "pretty-format": "30.0.5", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", + "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", + "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.0.1", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.0.5", + "@jest/types": "30.0.5", + "babel-jest": "30.0.5", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.0.5", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-runner": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-config/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-diff": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", + "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", + "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "jest-util": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", + "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", + "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", + "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", + "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", + "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", + "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", + "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.5", + "@jest/environment": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-leak-detector": "30.0.5", + "jest-message-util": "30.0.5", + "jest-resolve": "30.0.5", + "jest-runtime": "30.0.5", + "jest-util": "30.0.5", + "jest-watcher": "30.0.5", + "jest-worker": "30.0.5", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", + "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/globals": "30.0.5", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-runtime/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-snapshot": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", + "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.5", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.5", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", + "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.5", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", + "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.5", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", + "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.0.tgz", + "integrity": "sha512-fpbpXN/TD04Xz1/cCXzUR3ghDkhyiHjbzTILx3wNyKXIzQJ55uYAkUGWwhX72uHge/6MdFA/kp1ZUh35DlYmaA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -2064,19 +8438,13 @@ "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -2091,6 +8459,53 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.12", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.12.tgz", + "integrity": "sha512-aWVR6xXYYRvnK0v/uIwkf5Lthq9Jpn0N8TISW/oDTWlYB2sOimuiLn9Q26aUw4KxkJoiT8ACdiw44Y8VwKFIfQ==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/load-esm": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", @@ -2110,6 +8525,38 @@ "node": ">=13.2.0" } }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2146,43 +8593,105 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "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/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, "license": "MIT", "dependencies": { - "semver": "^6.0.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, + "devOptional": true, "license": "ISC" }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/mapped-types": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/mapped-types/-/mapped-types-0.0.1.tgz", + "integrity": "sha512-9WtZd+/WCFNmkLJh5LdEFQq4QJJ60E+H86Xus35D5DjyuD6O15F9OFOaGLkBJscJWh91WIzYv1OWZhrHslm3fg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2193,69 +8702,142 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "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.6" + "node": ">= 0.8" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "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/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, "license": "MIT", "bin": { "mime": "cli.js" }, "engines": { - "node": ">=4" + "node": ">=4.0.0" } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "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": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2268,142 +8850,205 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, "bin": { "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" } }, - "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "license": "MIT", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "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/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "whatwg-url": "^5.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">= 0.6" } }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, "engines": { - "node": ">=6" + "node": ">= 0.6" } }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", + "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "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/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "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/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2414,17 +9059,17 @@ "node": ">=0.10.0" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/object-assign": { @@ -2460,15 +9105,6 @@ "node": ">= 0.8" } }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -2478,6 +9114,177 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2487,81 +9294,416 @@ "node": ">= 0.8" } }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "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-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/peek-readable": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", - "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, "engines": { - "node": ">=18" + "node": "20 || >=22" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/prisma": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.7.0.tgz", - "integrity": "sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@prisma/config": "6.7.0", - "@prisma/engines": "6.7.0" - }, - "bin": { - "prisma": "build/index.js" + "find-up": "^4.0.0" }, "engines": { - "node": ">=18.18" + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, - "optionalDependencies": { - "fsevents": "2.3.3" + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" }, - "peerDependencies": { - "typescript": ">=5.1.0" + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/proxy-addr": { @@ -2577,13 +9719,40 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -2592,6 +9761,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2602,20 +9802,27 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.6.3", "unpipe": "1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -2631,30 +9838,72 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -2671,18 +9920,109 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "glob": "^7.1.3" + "resolve-from": "^5.0.0" }, - "bin": { - "rimraf": "bin.js" + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "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/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" } }, "node_modules/rxjs": { @@ -2690,7 +10030,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -2721,10 +10060,29 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2734,64 +10092,68 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -2799,6 +10161,53 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2872,19 +10281,35 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" + "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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, "node_modules/source-map-support": { @@ -2898,15 +10323,88 @@ "source-map": "^0.6.0" } }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2916,6 +10414,43 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2930,7 +10465,31 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -2942,34 +10501,104 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strtok3": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", - "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", "license": "MIT", "dependencies": { - "@tokenizer/token": "^0.3.0", - "peek-readable": "^7.0.0" + "@tokenizer/token": "^0.3.0" }, "engines": { "node": ">=18" @@ -2979,11 +10608,58 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2992,23 +10668,304 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", + "node_modules/swagger-ui-dist": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "license": "Apache-2.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@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/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" }, "engines": { "node": ">=10" } }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "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/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3032,9 +10989,9 @@ } }, "node_modules/token-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", - "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.4.tgz", + "integrity": "sha512-MD9MjpVNhVyH4fyd5rKphjvt/1qj+PtQUz65aFqAZA6XniWAuSFRjLk3e2VALEFlh9OwBpXUN7rfeqSnT/Fmkw==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", @@ -3048,12 +11005,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -3064,11 +11015,111 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -3108,52 +11159,45 @@ } } }, - "node_modules/ts-node-dev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", - "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^3.5.1", - "dynamic-dedupe": "^0.3.0", + "json5": "^2.2.2", "minimist": "^1.2.6", - "mkdirp": "^1.0.4", - "resolve": "^1.0.0", - "rimraf": "^2.6.1", - "source-map-support": "^0.5.12", - "tree-kill": "^1.2.2", - "ts-node": "^10.4.0", - "tsconfig": "^7.0.0" - }, - "bin": { - "ts-node-dev": "lib/bin.js", - "tsnd": "lib/bin.js" + "strip-bom": "^3.0.0" }, "engines": { - "node": ">=0.8.0" - }, - "peerDependencies": { - "node-notifier": "*", - "typescript": "*" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": ">=6" } }, - "node_modules/tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", "dev": true, "license": "MIT", "dependencies": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, "node_modules/tslib": { @@ -3162,19 +11206,292 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", + "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==", + "license": "MIT", + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", + "sha.js": "^2.4.11", + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "reflect-metadata": "^0.1.14 || ^0.2.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/typeorm/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/typeorm/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -3189,6 +11506,44 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", + "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -3214,11 +11569,21 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3228,28 +11593,132 @@ "node": ">= 0.8" } }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", - "engines": { - "node": ">= 0.4.0" + "bin": { + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3259,29 +11728,369 @@ "node": ">= 0.8" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "makeerror": "1.0.12" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.0.tgz", + "integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.2", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "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/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/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==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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": { - "string-width": "^1.0.2 || 2 || 3 || 4" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/wrappy": { @@ -3290,31 +12099,107 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/backend/package.json b/backend/package.json index 3730147..0b68ed1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,36 +1,95 @@ { - "name": "petitspas-backend", - "version": "1.0.0", - "description": "Backend pour l'application P'titsPas", - "main": "dist/index.js", + "name": "ptitspas-ynov-back", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", "scripts": { - "start": "node dist/index.js", - "dev": "ts-node-dev --respawn --transpile-only src/index.ts", - "build": "tsc", + "typeorm": "typeorm-ts-node-commonjs", + "migration:run": "npm run typeorm migration:run", + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", - "init-admin": "ts-node src/scripts/initAdmin.ts" + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@nestjs/common": "^11.1.0", - "@prisma/client": "^6.7.0", - "@types/jsonwebtoken": "^9.0.9", - "bcrypt": "^5.1.1", - "cors": "^2.8.5", - "express": "^4.18.2", - "helmet": "^7.1.0", - "jsonwebtoken": "^9.0.2", - "morgan": "^1.10.0" + "@nestjs/common": "^11.1.6", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/mapped-types": "^2.1.0", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.0", + "@nestjs/typeorm": "^11.0.0", + "@sentry/nestjs": "^10.10.0", + "bcrypt": "^6.0.0", + "bcryptjs": "^3.0.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "joi": "^18.0.0", + "mapped-types": "^0.0.1", + "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.16", + "passport-jwt": "^4.0.1", + "pg": "^8.16.3", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1", + "typeorm": "^0.3.26" }, "devDependencies": { - "@types/bcrypt": "^5.0.2", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/helmet": "^4.0.0", - "@types/morgan": "^1.9.9", - "@types/node": "^20.11.19", - "prisma": "^6.7.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.10", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/bcrypt": "^6.0.0", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/multer": "^1.4.12", + "@types/node": "^22.10.7", + "@types/nodemailer": "^6.4.16", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^6.0.2", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^16.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", "ts-node": "^10.9.2", - "ts-node-dev": "^2.0.0", - "typescript": "^5.3.3" + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" } } diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts deleted file mode 100644 index 52a81db..0000000 --- a/backend/src/admin/admin.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common'; -import { AdminService } from './admin.service'; -import { JwtAuthGuard } from '../auth/jwt-auth.guard'; - -@Controller('admin') -export class AdminController { - constructor(private readonly adminService: AdminService) {} - - @Post('change-password') - @UseGuards(JwtAuthGuard) - async changePassword( - @Req() req, - @Body('oldPassword') oldPassword: string, - @Body('newPassword') newPassword: string, - ) { - return this.adminService.changePassword(req.user.id, oldPassword, newPassword); - } -} \ No newline at end of file diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts deleted file mode 100644 index 9d47b62..0000000 --- a/backend/src/admin/admin.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AdminController } from './admin.controller'; -import { AdminService } from './admin.service'; -import { PrismaModule } from '../prisma/prisma.module'; -import { JwtModule } from '@nestjs/jwt'; - -@Module({ - imports: [ - PrismaModule, - JwtModule.register({ - secret: process.env.JWT_SECRET, - signOptions: { expiresIn: '1d' }, - }), - ], - controllers: [AdminController], - providers: [AdminService], -}) -export class AdminModule {} \ No newline at end of file diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts deleted file mode 100644 index c671645..0000000 --- a/backend/src/admin/admin.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import { PrismaService } from '../prisma/prisma.service'; -import * as bcrypt from 'bcrypt'; - -@Injectable() -export class AdminService { - constructor( - private prisma: PrismaService, - private jwtService: JwtService, - ) {} - - async changePassword(adminId: string, oldPassword: string, newPassword: string) { - // Récupérer l'administrateur - const admin = await this.prisma.admin.findUnique({ - where: { id: adminId }, - }); - - if (!admin) { - throw new UnauthorizedException('Administrateur non trouvé'); - } - - // Vérifier l'ancien mot de passe - const isPasswordValid = await bcrypt.compare(oldPassword, admin.password); - if (!isPasswordValid) { - throw new UnauthorizedException('Ancien mot de passe incorrect'); - } - - // Hasher le nouveau mot de passe - const hashedPassword = await bcrypt.hash(newPassword, 10); - - // Mettre à jour le mot de passe - await this.prisma.admin.update({ - where: { id: adminId }, - data: { password: hashedPassword }, - }); - - return { message: 'Mot de passe modifié avec succès' }; - } -} \ No newline at end of file diff --git a/backend/src/app.controller.spec.ts b/backend/src/app.controller.spec.ts new file mode 100644 index 0000000..d22f389 --- /dev/null +++ b/backend/src/app.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +describe('AppController', () => { + let appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService], + }).compile(); + + appController = app.get(AppController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts new file mode 100644 index 0000000..1393510 --- /dev/null +++ b/backend/src/app.controller.ts @@ -0,0 +1,17 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getOverView() { + return this.appService.getOverView(); + } + + @Get('hello') + getHello(): string { + return this.appService.getHello(); + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 81069a9..0124bfe 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,17 +1,71 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { PrismaModule } from './prisma/prisma.module'; -import { AuthModule } from './auth/auth.module'; -import { AdminModule } from './admin/admin.module'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import appConfig from './config/app.config'; +import databaseConfig from './config/database.config'; +import jwtConfig from './config/jwt.config'; +import { configValidationSchema } from './config/validation.schema'; +import { UserModule } from './routes/user/user.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { APP_FILTER } from '@nestjs/core'; +import { ParentsModule } from './routes/parents/parents.module'; +import { AuthModule } from './routes/auth/auth.module'; +import { SentryGlobalFilter } from '@sentry/nestjs/setup'; +import { AllExceptionsFilter } from './common/filters/all_exceptions.filters'; +import { EnfantsModule } from './routes/enfants/enfants.module'; +import { AppConfigModule } from './modules/config/config.module'; +import { DocumentsLegauxModule } from './modules/documents-legaux'; @Module({ imports: [ ConfigModule.forRoot({ + // Gestion dynamique des fichiers .env + envFilePath: [`.env.${process.env.NODE_ENV || 'development'}`, '.env'], + // envFilePath: '.env', + + // Chargement de configurations typées + load: [appConfig, databaseConfig, jwtConfig], + isGlobal: true, + validationSchema: configValidationSchema, }), - PrismaModule, + TypeOrmModule.forRootAsync({ + imports: [ConfigModule, + ], + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + type: 'postgres', + host: config.get('database.host'), + port: config.get('database.port'), + username: config.get('database.username'), + password: config.get('database.password'), + database: config.get('database.database'), + entities: [__dirname + '/**/*.entity{.ts,.js}'], + synchronize: false, + migrations: [__dirname + '/migrations/**/*{.ts,.js}'], + logging: true, + }), + }), + UserModule, + ParentsModule, + EnfantsModule, AuthModule, - AdminModule, + AppConfigModule, + DocumentsLegauxModule, + ], + controllers: [AppController], + providers: [ + AppService, + { + provide: APP_FILTER, + useClass: SentryGlobalFilter + }, + { + provide: APP_FILTER, + useClass: AllExceptionsFilter, + } + ], }) -export class AppModule {} \ No newline at end of file +export class AppModule { } diff --git a/backend/src/app.service.ts b/backend/src/app.service.ts new file mode 100644 index 0000000..5ab509e --- /dev/null +++ b/backend/src/app.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello Test!!!'; + } + + getOverView() { + return { + name: "P'titsPas API", + version: "1.0", + description: "Documentation rapide des endpoints disponibles", + authentication: "JWT Bearer Token requis", + endpoints: [ + { + method: "GET", + path: "/parents", + description: "Liste tous les parents", + roles: ["SUPER_ADMIN", "GESTIONNAIRE"] + }, + { + method: "GET", + path: "/parents/:id", + description: "Récupère un parent par ID utilisateur", + roles: ["SUPER_ADMIN", "GESTIONNAIRE"], + params: ["id (UUID)"] + }, + { + method: "POST", + path: "/parents", + description: "Crée un parent", + roles: ["SUPER_ADMIN", "GESTIONNAIRE"], + body: { + user_id: "UUID", + co_parent_id: "UUID (optionnel)" + } + }, + { + method: "PATCH", + path: "/parents/:id", + description: "Met à jour un parent", + roles: ["SUPER_ADMIN", "GESTIONNAIRE"], + params: ["id (UUID)"], + body: { + user_id: "UUID (optionnel)", + co_parent_id: "UUID (optionnel)" + } + }, + { + method: "DELETE", + path: "/parents/:id", + description: "Supprime un parent", + roles: ["SUPER_ADMIN"], + params: ["id (UUID)"] + } + ], + docs: "/api/docs" + }; + } +} + diff --git a/backend/src/common/decorators/public.decorator.ts b/backend/src/common/decorators/public.decorator.ts new file mode 100644 index 0000000..431422b --- /dev/null +++ b/backend/src/common/decorators/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from "@nestjs/common"; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); \ No newline at end of file diff --git a/backend/src/common/decorators/roles.decorator.ts b/backend/src/common/decorators/roles.decorator.ts new file mode 100644 index 0000000..b1efe4b --- /dev/null +++ b/backend/src/common/decorators/roles.decorator.ts @@ -0,0 +1,3 @@ +import { SetMetadata } from "@nestjs/common"; + +export const Roles = (...roles: string[]) => SetMetadata("roles", roles); diff --git a/backend/src/common/decorators/user.decorator.ts b/backend/src/common/decorators/user.decorator.ts new file mode 100644 index 0000000..efbca73 --- /dev/null +++ b/backend/src/common/decorators/user.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator, ExecutionContext } from "@nestjs/common"; + +export const User = createParamDecorator((data: string | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user; + return data ? user?.[data] : user; +}); diff --git a/backend/src/common/dto/date_range_query.dto.ts b/backend/src/common/dto/date_range_query.dto.ts new file mode 100644 index 0000000..973e4aa --- /dev/null +++ b/backend/src/common/dto/date_range_query.dto.ts @@ -0,0 +1,11 @@ +import { IsDateString, IsOptional } from "class-validator"; + +export class DateRangeQueryDto { + @IsOptional() + @IsDateString() + start?: string; + + @IsOptional() + @IsDateString() + end?: string; +} diff --git a/backend/src/common/dto/id_param.dto.ts b/backend/src/common/dto/id_param.dto.ts new file mode 100644 index 0000000..f632953 --- /dev/null +++ b/backend/src/common/dto/id_param.dto.ts @@ -0,0 +1,6 @@ +import { IsUUID } from "class-validator"; + +export class IdParamDto { + @IsUUID() + id: string; +} diff --git a/backend/src/common/dto/pagination.query.ts b/backend/src/common/dto/pagination.query.ts new file mode 100644 index 0000000..371e358 --- /dev/null +++ b/backend/src/common/dto/pagination.query.ts @@ -0,0 +1,11 @@ +import { IsOptional, IsPositive } from "class-validator"; + +export class PaginationQueryDto { + @IsOptional() + @IsPositive() + offset?: number; + + @IsOptional() + @IsPositive() + limit?: number; +} \ No newline at end of file diff --git a/backend/src/common/dto/search_query.dto.ts b/backend/src/common/dto/search_query.dto.ts new file mode 100644 index 0000000..b256af1 --- /dev/null +++ b/backend/src/common/dto/search_query.dto.ts @@ -0,0 +1,8 @@ +import { IsOptional, IsString, MinLength } from "class-validator"; + +export class SearchQueryDto { + @IsOptional() + @IsString() + @MinLength(2) + q?: string; +} diff --git a/backend/src/common/filters/all_exceptions.filters.ts b/backend/src/common/filters/all_exceptions.filters.ts new file mode 100644 index 0000000..4d46609 --- /dev/null +++ b/backend/src/common/filters/all_exceptions.filters.ts @@ -0,0 +1,27 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common"; + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.getResponse() + : { message: 'Internal server error' }; + + response.status(status).json({ + success: false, + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + message, + }); + } +} \ No newline at end of file diff --git a/backend/src/common/guards/auth.guard.ts b/backend/src/common/guards/auth.guard.ts new file mode 100644 index 0000000..ecf0361 --- /dev/null +++ b/backend/src/common/guards/auth.guard.ts @@ -0,0 +1,49 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { JwtService } from "@nestjs/jwt"; +import { Request } from 'express'; +import { IS_PUBLIC_KEY } from "../decorators/public.decorator"; +import { ConfigService } from "@nestjs/config"; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + private readonly reflector: Reflector, + private readonly configService: ConfigService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + + const request = context.switchToHttp().getRequest(); + if (request.path.startsWith('/api-docs')) { + return true; + } + + const authHeader = request.headers['authorization'] as string | undefined; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new UnauthorizedException('Token manquant ou invalide'); + } + + const token = authHeader.split(' ')[1]; + try { + const payload = await this.jwtService.verifyAsync(token, { + secret: this.configService.get('jwt.accessSecret'), + }); + + request.user = { + ...payload, + id: payload.sub, + }; + + return true; + } catch (error) { + throw new UnauthorizedException('Token invalide ou expiré'); + } + } +} diff --git a/backend/src/common/guards/roles.guard.ts b/backend/src/common/guards/roles.guard.ts new file mode 100644 index 0000000..f9152f0 --- /dev/null +++ b/backend/src/common/guards/roles.guard.ts @@ -0,0 +1,26 @@ +import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { Observable } from "rxjs"; + +@Injectable() +export class RolesGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean | Promise | Observable { + const requiredRoles = this.reflector.getAllAndOverride('roles', [ + context.getHandler(), + context.getClass(), + ]); + + if (!requiredRoles || requiredRoles.length === 0) { + return true; + } + + const request = context.switchToHttp().getRequest(); + const user = request.user; + if (!user || !user.role) { + return false; + } + return requiredRoles.includes(user.role); + } +} diff --git a/backend/src/common/interceptors/transform.interceptor.ts b/backend/src/common/interceptors/transform.interceptor.ts new file mode 100644 index 0000000..300de4a --- /dev/null +++ b/backend/src/common/interceptors/transform.interceptor.ts @@ -0,0 +1,15 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; +import { map, Observable, timestamp } from "rxjs"; + +@Injectable() +export class TransformInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((data) => ({ + success: true, + timestamp: new Date().toISOString(), + data + })), + ); + } +} \ No newline at end of file diff --git a/backend/src/config/app.config.ts b/backend/src/config/app.config.ts new file mode 100644 index 0000000..ddd91e4 --- /dev/null +++ b/backend/src/config/app.config.ts @@ -0,0 +1,6 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('', () => ({ + port: process.env.PORT, + env: process.env.NODE_ENV, +})); diff --git a/backend/src/config/database.config.ts b/backend/src/config/database.config.ts new file mode 100644 index 0000000..163bf39 --- /dev/null +++ b/backend/src/config/database.config.ts @@ -0,0 +1,9 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('database', () => ({ + host: process.env.POSTGRES_HOST, + port: process.env.POSTGRES_PORT, + username: process.env.POSTGRES_USER, + password: process.env.POSTGRES_PASSWORD, + database: process.env.POSTGRES_DB, +})); diff --git a/backend/src/config/jwt.config.ts b/backend/src/config/jwt.config.ts new file mode 100644 index 0000000..8f12d90 --- /dev/null +++ b/backend/src/config/jwt.config.ts @@ -0,0 +1,8 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('jwt', () => ({ + accessSecret: process.env.JWT_ACCESS_SECRET, + accessExpiresIn: process.env.JWT_ACCESS_EXPIRES, + refreshSecret: process.env.JWT_REFRESH_SECRET, + refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES, +})); diff --git a/backend/src/config/validation.schema.ts b/backend/src/config/validation.schema.ts new file mode 100644 index 0000000..53e3109 --- /dev/null +++ b/backend/src/config/validation.schema.ts @@ -0,0 +1,21 @@ +import * as Joi from 'joi'; + +export const configValidationSchema = Joi.object({ + NODE_ENV: Joi.string() + .valid('development', 'production', 'test') + .default('development'), + PORT: Joi.number().optional(), + + // Base de données + POSTGRES_HOST: Joi.string().required(), + POSTGRES_PORT: Joi.number().required(), + POSTGRES_USER: Joi.string().required(), + POSTGRES_PASSWORD: Joi.string().required(), + POSTGRES_DB: Joi.string().required(), + + // JWT + JWT_ACCESS_SECRET: Joi.string().required(), + JWT_ACCESS_EXPIRES: Joi.string().required(), + JWT_REFRESH_SECRET: Joi.string().required(), + JWT_REFRESH_EXPIRES: Joi.string().required(), +}); diff --git a/backend/src/controllers/theme.controller.ts b/backend/src/controllers/theme.controller.ts deleted file mode 100644 index 30c1108..0000000 --- a/backend/src/controllers/theme.controller.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Request, Response } from 'express'; -import { ThemeService, ThemeData } from '../services/theme.service'; - -export class ThemeController { - // Créer un nouveau thème - static async createTheme(req: Request, res: Response) { - try { - const themeData: ThemeData = req.body; - const theme = await ThemeService.createTheme(themeData); - res.status(201).json(theme); - } catch (error) { - res.status(500).json({ error: 'Erreur lors de la création du thème' }); - } - } - - // Récupérer tous les thèmes - static async getAllThemes(req: Request, res: Response) { - try { - const themes = await ThemeService.getAllThemes(); - res.json(themes); - } catch (error) { - res.status(500).json({ error: 'Erreur lors de la récupération des thèmes' }); - } - } - - // Récupérer le thème actif - static async getActiveTheme(req: Request, res: Response) { - try { - const theme = await ThemeService.getActiveTheme(); - if (!theme) { - return res.status(404).json({ error: 'Aucun thème actif trouvé' }); - } - res.json(theme); - } catch (error) { - res.status(500).json({ error: 'Erreur lors de la récupération du thème actif' }); - } - } - - // Activer un thème - static async activateTheme(req: Request, res: Response) { - try { - const { themeId } = req.params; - const theme = await ThemeService.activateTheme(themeId); - res.json(theme); - } catch (error) { - res.status(500).json({ error: 'Erreur lors de l\'activation du thème' }); - } - } - - // Mettre à jour un thème - static async updateTheme(req: Request, res: Response) { - try { - const { themeId } = req.params; - const themeData: Partial = req.body; - const theme = await ThemeService.updateTheme(themeId, themeData); - res.json(theme); - } catch (error) { - res.status(500).json({ error: 'Erreur lors de la mise à jour du thème' }); - } - } - - // Supprimer un thème - static async deleteTheme(req: Request, res: Response) { - try { - const { themeId } = req.params; - await ThemeService.deleteTheme(themeId); - res.status(204).send(); - } catch (error) { - res.status(500).json({ error: 'Erreur lors de la suppression du thème' }); - } - } -} \ No newline at end of file diff --git a/backend/src/entities/acceptation-document.entity.ts b/backend/src/entities/acceptation-document.entity.ts new file mode 100644 index 0000000..011aa7d --- /dev/null +++ b/backend/src/entities/acceptation-document.entity.ts @@ -0,0 +1,40 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, +} from 'typeorm'; +import { Users } from './users.entity'; +import { DocumentLegal } from './document-legal.entity'; + +@Entity('acceptations_documents') +export class AcceptationDocument { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, { nullable: false, onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_utilisateur' }) + utilisateur: Users; + + @ManyToOne(() => DocumentLegal, { nullable: true }) + @JoinColumn({ name: 'id_document' }) + document: DocumentLegal | null; + + @Column({ type: 'varchar', length: 50, nullable: false }) + type_document: 'cgu' | 'privacy'; + + @Column({ type: 'integer', nullable: false }) + version_document: number; + + @CreateDateColumn({ name: 'accepte_le', type: 'timestamptz' }) + accepteLe: Date; + + @Column({ type: 'inet', nullable: true }) + ip_address: string | null; + + @Column({ type: 'text', nullable: true }) + user_agent: string | null; +} + diff --git a/backend/src/entities/assistantes_maternelles.entity.ts b/backend/src/entities/assistantes_maternelles.entity.ts new file mode 100644 index 0000000..f8793e6 --- /dev/null +++ b/backend/src/entities/assistantes_maternelles.entity.ts @@ -0,0 +1,51 @@ +import { Entity, PrimaryColumn, Column, OneToOne, JoinColumn } from 'typeorm'; +import { Users } from './users.entity'; + +@Entity('assistantes_maternelles') +export class AssistanteMaternelle { + // PK = FK vers utilisateurs.id + @PrimaryColumn('uuid', { name: 'id_utilisateur' }) + user_id: string; + + @OneToOne(() => Users, (user) => user.assistanteMaternelle, { + onDelete: 'CASCADE', + }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user: Users; + + @Column({ name: 'numero_agrement', length: 50, nullable: true }) + approval_number?: string; + + @Column({ name: 'nir_chiffre', length: 15, nullable: true }) + nir?: string; + + @Column({ name: 'nb_max_enfants', type: 'int', nullable: true }) + max_children?: number; + + @Column({ name: 'biographie', type: 'text', nullable: true }) + biography?: string; + + @Column({ + name: 'disponible', + type: 'boolean', + default: true, + nullable: true, + }) + available?: boolean; + + @Column({ name: 'ville_residence', length: 100, nullable: true }) + residence_city?: string; + + @Column( { name: 'date_agrement', type: 'date', nullable: true }) + agreement_date?: Date; + + @Column( { name: 'annee_experience', type: 'smallint', nullable: true }) + years_experience?: number; + + @Column( { name: 'specialite', length: 100, nullable: true }) + specialty?: string; + + @Column( { name: 'place_disponible', type: 'integer', nullable: true }) + places_available?: number; + +} diff --git a/backend/src/entities/avenants_contrats.entity.ts b/backend/src/entities/avenants_contrats.entity.ts new file mode 100644 index 0000000..bb3725f --- /dev/null +++ b/backend/src/entities/avenants_contrats.entity.ts @@ -0,0 +1,42 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Contrat } from "./contrats.entity"; +import { Users } from "./users.entity"; + +export enum StatutAvenantType { + PROPOSE = 'propose', + ACCEPTE = 'accepte', + REFUSE = 'refuse', +} + +@Entity('avenants_contrats') +export class AvenantContrat { + // Define your columns and relationships here + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Contrat, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_contrat' }) + contrat: Contrat; + + @Column({ type: 'jsonb', nullable: true, name: 'modifications' }) + modifications?: any; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'initie_par', referencedColumnName: 'id' }) + initiator?: Users; + + @Column({ + type: 'enum', + enum: StatutAvenantType, + enumName: 'statut_avenant_type', + default: StatutAvenantType.PROPOSE, + name: 'statut' + }) + statut: StatutAvenantType; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updatedAt: Date; +} diff --git a/backend/src/entities/children.entity.ts b/backend/src/entities/children.entity.ts new file mode 100644 index 0000000..3dd691f --- /dev/null +++ b/backend/src/entities/children.entity.ts @@ -0,0 +1,74 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + OneToMany, ManyToMany, CreateDateColumn, JoinTable +} from 'typeorm'; +import { Parents } from './parents.entity'; +import { ParentsChildren } from './parents_children.entity'; +import { Dossier } from './dossiers.entity'; + +export enum StatutEnfantType { + A_NAITRE = 'a_naitre', + ACTIF = 'actif', + SCOLARISE = 'scolarise', +} + +export enum GenreType { + H = 'H', + F = 'F', + AUTRE = 'Autre', +} + +@Entity('enfants') +export class Children { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: 'enum', + enum: StatutEnfantType, + enumName: 'statut_enfant_type', + name: 'statut' + }) + status: StatutEnfantType; + + @Column({ name: 'prenom', length: 100, nullable: true }) + first_name?: string; + + @Column({ name: 'nom', length: 100, nullable: true }) + last_name?: string; + + @Column({ + type: 'enum', + enum: GenreType, + enumName: 'genre_type', + nullable: true, + name: 'genre' + }) + gender?: GenreType; + + @Column({ type: 'date', nullable: true, name: 'date_naissance' }) + birth_date?: Date; + + @Column({ type: 'date', nullable: true, name: 'date_prevue_naissance' }) + due_date?: Date; + + @Column({ nullable: true, name: 'photo_url', type: 'text' }) + photo_url?: string; + + @Column({ default: false, name: 'consentement_photo', type: 'boolean' }) + consent_photo: boolean; + + @Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' }) + consent_photo_at?: Date; + + @Column({ default: false, name: 'est_multiple', type: 'boolean' }) + is_multiple: boolean; + + // Lien via table de jointure enfants_parents + @OneToMany(() => ParentsChildren, pc => pc.child) + parentLinks: ParentsChildren[]; + + // Relation avec Dossier + @OneToMany(() => Dossier, d => d.child) + dossiers: Dossier[]; +} diff --git a/backend/src/entities/configuration.entity.ts b/backend/src/entities/configuration.entity.ts new file mode 100644 index 0000000..d2d30bb --- /dev/null +++ b/backend/src/entities/configuration.entity.ts @@ -0,0 +1,39 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Users } from './users.entity'; + +@Entity('configuration') +export class Configuration { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 100, unique: true, nullable: false }) + cle: string; + + @Column({ type: 'text', nullable: true }) + valeur: string | null; + + @Column({ type: 'varchar', length: 50, nullable: false }) + type: 'string' | 'number' | 'boolean' | 'json' | 'encrypted'; + + @Column({ type: 'varchar', length: 50, nullable: true }) + categorie: 'email' | 'app' | 'security' | null; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @UpdateDateColumn({ name: 'modifie_le' }) + modifieLe: Date; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'modifie_par' }) + modifiePar: Users | null; +} + diff --git a/backend/src/entities/contrats.entity.ts b/backend/src/entities/contrats.entity.ts new file mode 100644 index 0000000..994014a --- /dev/null +++ b/backend/src/entities/contrats.entity.ts @@ -0,0 +1,57 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Dossier } from "./dossiers.entity"; + +export enum StatutContratType { + BROUILLON = 'brouillon', + EN_ATTENTE_SIGNATURE = 'en_attente_signature', + VALIDE = 'valide', + RESILIE = 'resilie', +} + +@Entity('contrats') +export class Contrat { + // Define your columns and relationships here + + @PrimaryGeneratedColumn('uuid') + id: string; + + @OneToOne(() => Dossier, {onDelete: 'CASCADE'} ) + @JoinColumn({ name: 'id_dossier'}) + dossier: Dossier; + + @Column({type: 'jsonb', nullable: true, name: 'planning'}) + planning?: any; + + @Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'tarif_horaire'}) + hourly_rate?: string; + + @Column({type: 'numeric', precision: 6, scale: 2, nullable: true, name: 'indemnites_repas'}) + meal_indemnity?: string; + + @Column( { name: 'date_debut', type: 'date', nullable: true }) + start_date?: Date; + + @Column({ + type: 'enum', + enum: StatutContratType, + enumName: 'statut_contrat_type', + default: StatutContratType.BROUILLON, + name: 'statut' + }) + statut: StatutContratType; + + @Column({type: 'boolean', default: false, name: 'signe_parent'}) + signed_by_parent: boolean; + + @Column({type: 'boolean', default: false, name: 'signe_am'}) + signed_by_am: boolean; + + @Column({type: 'timestamptz', nullable: true, name: 'finalise_le'}) + finalized_at?: Date; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; +} diff --git a/backend/src/entities/document-legal.entity.ts b/backend/src/entities/document-legal.entity.ts new file mode 100644 index 0000000..845bb62 --- /dev/null +++ b/backend/src/entities/document-legal.entity.ts @@ -0,0 +1,44 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + CreateDateColumn, +} from 'typeorm'; +import { Users } from './users.entity'; + +@Entity('documents_legaux') +export class DocumentLegal { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 50, nullable: false }) + type: 'cgu' | 'privacy'; + + @Column({ type: 'integer', nullable: false }) + version: number; + + @Column({ type: 'varchar', length: 255, nullable: false }) + fichier_nom: string; + + @Column({ type: 'varchar', length: 500, nullable: false }) + fichier_path: string; + + @Column({ type: 'varchar', length: 64, nullable: false }) + fichier_hash: string; + + @Column({ type: 'boolean', default: false }) + actif: boolean; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'televerse_par' }) + televersePar: Users | null; + + @CreateDateColumn({ name: 'televerse_le', type: 'timestamptz' }) + televerseLe: Date; + + @Column({ name: 'active_le', type: 'timestamptz', nullable: true }) + activeLe: Date | null; +} + diff --git a/backend/src/entities/dossiers.entity.ts b/backend/src/entities/dossiers.entity.ts new file mode 100644 index 0000000..d3787d8 --- /dev/null +++ b/backend/src/entities/dossiers.entity.ts @@ -0,0 +1,61 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn, JoinColumn +} from 'typeorm'; +import { Parents } from './parents.entity'; +import { Children } from './children.entity'; +import { Message } from './messages.entity'; + +export enum StatutDossierType { + ENVOYE = 'envoye', + ACCEPTE = 'accepte', + REFUSE = 'refuse', + CLOTURE = 'cloture', +} + +@Entity('dossiers') +export class Dossier { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Parents, p => p.dossiers, { onDelete: 'CASCADE', nullable: false }) + @JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' }) + parent: Parents; + + @ManyToOne(() => Children, c => c.dossiers, { onDelete: 'CASCADE', nullable: false }) + @JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' }) + child: Children; + + @Column({ type: 'text', nullable: true, name: 'presentation' }) + presentation?: string; + + @Column({ type: 'varchar', length: 50, nullable: true, name: 'type_contrat' }) + type_contrat?: string; + + @Column({ type: 'boolean', default: false, name: 'repas' }) + meals: boolean; + + @Column({ type: 'numeric', precision: 10, scale: 2, nullable: true, name: 'budget' }) + budget?: number; + + @Column({ type: 'jsonb', nullable: true, name: 'planning_souhaite' }) + desired_schedule?: any; + + @Column({ + type: 'enum', + enum: StatutDossierType, + enumName: 'statut_dossier_type', + default: StatutDossierType.ENVOYE, + name: 'statut' + }) + status: StatutDossierType; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; + + @OneToMany(() => Message, m => m.dossier) + messages: Message[]; +} \ No newline at end of file diff --git a/backend/src/entities/evenements.entity.ts b/backend/src/entities/evenements.entity.ts new file mode 100644 index 0000000..9d7a73c --- /dev/null +++ b/backend/src/entities/evenements.entity.ts @@ -0,0 +1,79 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Children } from "./children.entity"; +import { Users } from "./users.entity"; +import { Parents } from "./parents.entity"; + +export enum TypeEvenementType { + ABSENCE_ENFANT = 'absence_enfant', + CONGE_AM = 'conge_am', + CONGE_PARENT = 'conge_parent', + ARRET_MALADIE_AM = 'arret_maladie_am', + EVENEMENT_RPE = 'evenement_rpe', +} + +export enum StatutEvenementType { + PROPOSE = 'propose', + VALIDE = 'valide', + REFUSE = 'refuse', +} + +@Entity('evenements') +export class Evenement { + // Define your columns and relationships here + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ + type: 'enum', + enum: TypeEvenementType, + enumName: 'type_evenement_type', + name: 'type' + }) + type: TypeEvenementType; + + @ManyToOne(() => Children, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' }) + child?: Children; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'id_am', referencedColumnName: 'id' }) + assistanteMaternelle?: Users; + + @ManyToOne(() => Parents, { nullable: true }) + @JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' }) + parent?: Parents; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'cree_par', referencedColumnName: 'id' }) + created_by?: Users; + + @Column({ type: 'timestamptz', nullable: true, name: 'date_debut' }) + start_date?: Date; + + @Column({ type: 'timestamptz', nullable: true, name: 'date_fin' }) + end_date?: Date; + + @Column({ type: 'text', nullable: true, name: 'commentaires' }) + comments?: string; + + @Column({ + type: 'enum', + enum: StatutEvenementType, + enumName: 'statut_evenement_type', + name: 'statut', + default: StatutEvenementType.PROPOSE + }) + status: StatutEvenementType; + + @Column({type: 'timestamptz', nullable: true, name: 'delai_grace'}) + grace_deadline?: Date; + + @Column({type: 'boolean', default: false, name: 'urgent'}) + urgent: boolean; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; +} diff --git a/backend/src/entities/messages.entity.ts b/backend/src/entities/messages.entity.ts new file mode 100644 index 0000000..0083bf5 --- /dev/null +++ b/backend/src/entities/messages.entity.ts @@ -0,0 +1,29 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + ManyToOne, JoinColumn, CreateDateColumn +} from 'typeorm'; +import { Dossier } from './dossiers.entity'; +import { Users } from './users.entity'; + +@Entity('messages') +export class Message { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Dossier, d => d.messages, { onDelete: 'CASCADE', nullable: false }) + @JoinColumn({ name: 'id_dossier' }) + dossier: Dossier; + + @ManyToOne(() => Users, u => u.messages, { onDelete: 'CASCADE', nullable: false }) + @JoinColumn({ name: 'id_expediteur' }) + sender: Users; + + @Column({ type: 'text', name: 'contenu' }) + content: string; + + @Column({ type: 'boolean', name: 're_redige_par_ia', default: false }) + reRedigeParIA: boolean; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; +} diff --git a/backend/src/entities/notifications.entity.ts b/backend/src/entities/notifications.entity.ts new file mode 100644 index 0000000..5f1dbe8 --- /dev/null +++ b/backend/src/entities/notifications.entity.ts @@ -0,0 +1,23 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Users } from "./users.entity"; + +@Entity('notifications') +export class Notification { + // Define your columns and relationships here + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user: Users; + + @Column({ type: 'text', name: 'contenu' }) + content: string; + + @Column({type: 'boolean', name: 'lu', default: false}) + read: boolean; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + +} diff --git a/backend/src/entities/parents.entity.ts b/backend/src/entities/parents.entity.ts new file mode 100644 index 0000000..b66843c --- /dev/null +++ b/backend/src/entities/parents.entity.ts @@ -0,0 +1,31 @@ +import { + Entity, PrimaryColumn, OneToOne, JoinColumn, + ManyToOne, OneToMany +} from 'typeorm'; +import { Users } from './users.entity'; +import { ParentsChildren } from './parents_children.entity'; +import { Dossier } from './dossiers.entity'; + +@Entity('parents', { schema: 'public' }) +export class Parents { + // PK = FK vers utilisateurs.id + @PrimaryColumn('uuid', { name: 'id_utilisateur' }) + user_id: string; + + @OneToOne(() => Users, user => user.parent, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user: Users; + + // Co-parent (nullable) → FK vers utilisateurs.id + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'id_co_parent', referencedColumnName: 'id' }) + co_parent?: Users; + + // Lien vers enfants via la table enfants_parents + @OneToMany(() => ParentsChildren, pc => pc.parent) + parentChildren: ParentsChildren[]; + + // Lien vers les dossiers de ce parent + @OneToMany(() => Dossier, d => d.parent) + dossiers: Dossier[]; +} diff --git a/backend/src/entities/parents_children.entity.ts b/backend/src/entities/parents_children.entity.ts new file mode 100644 index 0000000..49e59c4 --- /dev/null +++ b/backend/src/entities/parents_children.entity.ts @@ -0,0 +1,22 @@ +import { + Entity, ManyToOne, JoinColumn, PrimaryColumn +} from 'typeorm'; +import { Parents } from './parents.entity'; +import { Children } from './children.entity'; + +@Entity('enfants_parents', { schema: 'public' }) +export class ParentsChildren { + @PrimaryColumn('uuid', { name: 'id_parent' }) + parentId: string; + + @PrimaryColumn('uuid', { name: 'id_enfant' }) + enfantId: string; + + @ManyToOne(() => Parents, p => p.parentChildren, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' }) + parent: Parents; + + @ManyToOne(() => Children, c => c.parentLinks, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' }) + child: Children; +} diff --git a/backend/src/entities/signalements_bugs.entity.ts b/backend/src/entities/signalements_bugs.entity.ts new file mode 100644 index 0000000..3759c6f --- /dev/null +++ b/backend/src/entities/signalements_bugs.entity.ts @@ -0,0 +1,19 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Users } from "./users.entity"; + +@Entity('signalements_bugs') +export class SignalementBug { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, {nullable: true}) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user?: Users; + + @Column({ type: 'text', name: 'description'}) + description: string; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + +} diff --git a/backend/src/entities/uploads.entity.ts b/backend/src/entities/uploads.entity.ts new file mode 100644 index 0000000..77470f8 --- /dev/null +++ b/backend/src/entities/uploads.entity.ts @@ -0,0 +1,21 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Users } from "./users.entity"; + +@Entity('uploads') +export class Upload { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, { onDelete: 'SET NULL', nullable: true }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user?: Users; + + @Column({ type: 'text', name: 'fichier_url' }) + file_url: string; + + @Column({type: 'varchar', length: 50, nullable: true, name: 'type'}) + type?: string; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; +} diff --git a/backend/src/entities/users.entity.ts b/backend/src/entities/users.entity.ts new file mode 100644 index 0000000..25db178 --- /dev/null +++ b/backend/src/entities/users.entity.ts @@ -0,0 +1,150 @@ +import { + Entity, PrimaryGeneratedColumn, Column, + CreateDateColumn, UpdateDateColumn, + OneToOne, OneToMany +} from 'typeorm'; +import { AssistanteMaternelle } from './assistantes_maternelles.entity'; +import { Parents } from './parents.entity'; +import { Message } from './messages.entity'; + +// Enums alignés avec la BDD PostgreSQL +export enum RoleType { + PARENT = 'parent', + GESTIONNAIRE = 'gestionnaire', + SUPER_ADMIN = 'super_admin', + ASSISTANTE_MATERNELLE = 'assistante_maternelle', + ADMINISTRATEUR = 'administrateur', +} + +//Enum pour definir le genre +export enum GenreType { + H = 'H', + F = 'F', + AUTRE = 'Autre', +} + +//Enum pour definir le statut utilisateur +export enum StatutUtilisateurType { + EN_ATTENTE = 'en_attente', + ACTIF = 'actif', + SUSPENDU = 'suspendu', +} + +export enum SituationFamilialeType { + CELIBATAIRE = 'celibataire', + MARIE = 'marie', + DIVORCE = 'divorce', + VEUF = 'veuf', + PACSE = 'pacse', + SEPARE = 'separe', + PARENT_ISOLE = 'parent_isole', + CONCUBINAGE = 'concubinage', +} + +//Declaration de l'entite utilisateur +@Entity('utilisateurs', { schema: 'public' }) +export class Users { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true, name: 'email' }) + email: string; + + @Column({ name: 'password', nullable: true }) + password?: string; + + @Column({ name: 'prenom', nullable: true }) + prenom?: string; + + @Column({ name: 'nom', nullable: true }) + nom?: string; + + @Column({ + type: 'enum', + enum: GenreType, + enumName: 'genre_type', // correspond à l'enum de la db psql + nullable: true, + name: 'genre' + }) + genre?: GenreType; + + @Column({ + type: 'enum', + enum: RoleType, + enumName: 'role_type', // correspond à l'enum de la db psql + name: 'role' + }) + role: RoleType; + + @Column({ + type: 'enum', + enum: StatutUtilisateurType, + enumName: 'statut_utilisateur_type', // correspond à l'enum de la db psql + default: StatutUtilisateurType.EN_ATTENTE, + name: 'statut' + }) + statut: StatutUtilisateurType; + + @Column({ type: 'enum', + enum: SituationFamilialeType, + enumName: 'situation_familiale_type', + nullable: true, + name: 'situation_familiale' + }) + situation_familiale?: SituationFamilialeType; + + @Column({ nullable: true, name: 'telephone' }) + telephone?: string; + + @Column({ nullable: true, name: 'adresse' }) + adresse?: string; + + @Column({ nullable: true, name: 'photo_url' }) + photo_url?: string; + + @Column({ default: false, name: 'consentement_photo' }) + consentement_photo: boolean; + + @Column({ type: 'timestamptz', nullable: true, name: 'date_consentement_photo' }) + date_consentement_photo?: Date; + + @Column({ default: false, name: 'changement_mdp_obligatoire' }) + changement_mdp_obligatoire: boolean; + + @Column({ nullable: true, name: 'token_creation_mdp', length: 255 }) + token_creation_mdp?: string; + + @Column({ type: 'timestamptz', nullable: true, name: 'token_creation_mdp_expire_le' }) + token_creation_mdp_expire_le?: Date; + + @Column({ nullable: true, name: 'ville' }) + ville?: string; + + @Column({ nullable: true, name: 'code_postal' }) + code_postal?: string; + + @Column({ nullable: true, name: 'profession' }) + profession?: string; + + @Column({ name: 'date_naissance', type: 'date', nullable: true }) + date_naissance?: Date; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + cree_le: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + modifie_le: Date; + + // Relations + @OneToOne(() => AssistanteMaternelle, a => a.user) + assistanteMaternelle?: AssistanteMaternelle; + + @OneToOne(() => Parents, p => p.user) + parent?: Parents; + + @OneToMany(() => Message, m => m.sender) + messages?: Message[]; + + @OneToMany(() => Parents, parent => parent.co_parent) + co_parent_in?: Parents[]; +} diff --git a/backend/src/entities/validations.entity.ts b/backend/src/entities/validations.entity.ts new file mode 100644 index 0000000..d931c59 --- /dev/null +++ b/backend/src/entities/validations.entity.ts @@ -0,0 +1,44 @@ +import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; +import { Users } from "./users.entity"; + +export enum StatutValidationType { + EN_ATTENTE = 'en_attente', + VALIDE = 'valide', + REFUSE = 'refuse', +} + +@Entity('validations') +export class Validation { + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'id_utilisateur', referencedColumnName: 'id' }) + user?: Users; + + @Column({ type: 'varchar', length: 50, name: 'type', nullable: true }) + type: string; + + @Column({ + type: 'enum', + enum: StatutValidationType, + enumName: 'statut_validation_type', + name: 'statut', + default: StatutValidationType.EN_ATTENTE + }) + status: StatutValidationType; + + @ManyToOne(() => Users, { nullable: true }) + @JoinColumn({ name: 'valide_par', referencedColumnName: 'id' }) + validated_by?: Users; + + @Column( { name: 'commentaire', type: 'text', nullable: true }) + comment?: string; + + @CreateDateColumn({ name: 'cree_le', type: 'timestamptz' }) + created_at: Date; + + @UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' }) + updated_at: Date; +} + diff --git a/backend/src/index.ts b/backend/src/index.ts deleted file mode 100644 index 490853f..0000000 --- a/backend/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import morgan from 'morgan'; -import themeRoutes from './routes/theme.routes'; - -const app = express(); -const port = process.env.PORT || 3000; - -// Middleware -app.use(cors()); -app.use(helmet()); -app.use(morgan('dev')); -app.use(express.json()); - -// Routes -app.use('/api/themes', themeRoutes); - -// Gestion des erreurs -app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { - console.error(err.stack); - res.status(500).json({ error: 'Une erreur est survenue' }); -}); - -// Démarrage du serveur -app.listen(port, () => { - console.log(`Serveur démarré sur le port ${port}`); -}); \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts new file mode 100644 index 0000000..1602cec --- /dev/null +++ b/backend/src/main.ts @@ -0,0 +1,59 @@ +import { NestFactory, Reflector } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { ConfigService } from '@nestjs/config'; +import { SwaggerModule } from '@nestjs/swagger/dist/swagger-module'; +import { DocumentBuilder } from '@nestjs/swagger'; +import { AuthGuard } from './common/guards/auth.guard'; +import { JwtService } from '@nestjs/jwt'; +import { RolesGuard } from './common/guards/roles.guard'; +import { ValidationPipe } from '@nestjs/common'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule, + { logger: ['error', 'warn', 'log', 'debug', 'verbose'] }); + app.enableCors(); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }) + ); + + const configService = app.get(ConfigService); + + const port = configService.get('app.port', 3000); + app.setGlobalPrefix('api/v1'); + + const config = new DocumentBuilder() + .setTitle("P'titsPas API") + .setDescription("API pour l'application P'titsPas") + .setVersion('1.0.0') + .addBearerAuth( + { + type: 'http', + scheme: 'Bearer', + bearerFormat: 'JWT', + name: 'Authorization', + description: 'Enter JWT token', + in: 'header', + }, + 'access-token', + ) + //.addServer('/api/v1') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/v1/swagger', app, document); + + + + await app.listen(port); + console.log(`✅ P'titsPas API is running on: ${await app.getUrl()}`); +} + +bootstrap().catch((err) => { + console.error('❌ Error starting the application:', err); + process.exit(1); +}); diff --git a/backend/src/modules/config/config.controller.ts b/backend/src/modules/config/config.controller.ts new file mode 100644 index 0000000..ee1c9ba --- /dev/null +++ b/backend/src/modules/config/config.controller.ts @@ -0,0 +1,232 @@ +import { + Controller, + Get, + Patch, + Post, + Body, + Param, + UseGuards, + Request, + HttpStatus, + HttpException, +} from '@nestjs/common'; +import { AppConfigService } from './config.service'; +import { UpdateConfigDto } from './dto/update-config.dto'; +import { TestSmtpDto } from './dto/test-smtp.dto'; + +@Controller('configuration') +export class ConfigController { + constructor(private readonly configService: AppConfigService) {} + + /** + * Vérifier si la configuration initiale est terminée + * GET /api/v1/configuration/setup/status + */ + @Get('setup/status') + async getSetupStatus() { + try { + const isCompleted = this.configService.isSetupCompleted(); + return { + success: true, + data: { + setupCompleted: isCompleted, + }, + }; + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors de la vérification du statut de configuration', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Marquer la configuration initiale comme terminée + * POST /api/v1/configuration/setup/complete + */ + @Post('setup/complete') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async completeSetup(@Request() req: any) { + try { + // TODO: Récupérer l'ID utilisateur depuis le JWT + const userId = req.user?.id || 'system'; + + await this.configService.markSetupCompleted(userId); + + return { + success: true, + message: 'Configuration initiale terminée avec succès', + }; + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors de la finalisation de la configuration', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Test de la connexion SMTP + * POST /api/v1/configuration/test-smtp + */ + @Post('test-smtp') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async testSmtp(@Body() testSmtpDto: TestSmtpDto) { + try { + const result = await this.configService.testSmtpConnection(testSmtpDto.testEmail); + + if (result.success) { + return { + success: true, + message: 'Connexion SMTP réussie. Email de test envoyé.', + }; + } else { + return { + success: false, + message: 'Échec du test SMTP', + error: result.error, + }; + } + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors du test SMTP', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Mise à jour multiple des configurations + * PATCH /api/v1/configuration/bulk + */ + @Patch('bulk') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async updateBulk(@Body() updateConfigDto: UpdateConfigDto, @Request() req: any) { + try { + // TODO: Récupérer l'ID utilisateur depuis le JWT + const userId = req.user?.id || null; + + let updated = 0; + const errors: string[] = []; + + // Parcourir toutes les clés du DTO + for (const [key, value] of Object.entries(updateConfigDto)) { + if (value !== undefined) { + try { + await this.configService.set(key, value, userId); + updated++; + } catch (error) { + errors.push(`${key}: ${error.message}`); + } + } + } + + // Recharger le cache après les modifications + await this.configService.loadCache(); + + if (errors.length > 0) { + return { + success: false, + message: 'Certaines configurations n\'ont pas pu être mises à jour', + updated, + errors, + }; + } + + return { + success: true, + message: 'Configuration mise à jour avec succès', + updated, + }; + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors de la mise à jour des configurations', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Récupérer toutes les configurations (pour l'admin) + * GET /api/v1/configuration + */ + @Get() + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async getAll() { + try { + const configs = await this.configService.getAll(); + return { + success: true, + data: configs, + }; + } catch (error) { + throw new HttpException( + { + success: false, + message: 'Erreur lors de la récupération des configurations', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + /** + * Récupérer les configurations par catégorie + * GET /api/v1/configuration/:category + */ + @Get(':category') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin') + async getByCategory(@Param('category') category: string) { + try { + if (!['email', 'app', 'security'].includes(category)) { + throw new HttpException( + { + success: false, + message: 'Catégorie invalide. Valeurs acceptées: email, app, security', + }, + HttpStatus.BAD_REQUEST, + ); + } + + const configs = await this.configService.getByCategory(category); + return { + success: true, + data: configs, + }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + { + success: false, + message: 'Erreur lors de la récupération des configurations', + error: error.message, + }, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } +} diff --git a/backend/src/modules/config/config.module.ts b/backend/src/modules/config/config.module.ts new file mode 100644 index 0000000..aed9842 --- /dev/null +++ b/backend/src/modules/config/config.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Configuration } from '../../entities/configuration.entity'; +import { AppConfigService } from './config.service'; +import { ConfigController } from './config.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Configuration])], + controllers: [ConfigController], + providers: [AppConfigService], + exports: [AppConfigService], +}) +export class AppConfigModule {} + diff --git a/backend/src/modules/config/config.service.ts b/backend/src/modules/config/config.service.ts new file mode 100644 index 0000000..421ccae --- /dev/null +++ b/backend/src/modules/config/config.service.ts @@ -0,0 +1,338 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Configuration } from '../../entities/configuration.entity'; +import * as crypto from 'crypto'; + +@Injectable() +export class AppConfigService implements OnModuleInit { + private readonly logger = new Logger(AppConfigService.name); + private cache: Map = new Map(); + private readonly ENCRYPTION_KEY: string; + private readonly ENCRYPTION_ALGORITHM = 'aes-256-cbc'; + private readonly IV_LENGTH = 16; + + constructor( + @InjectRepository(Configuration) + private configRepo: Repository, + ) { + // Clé de chiffrement depuis les variables d'environnement + // En production, cette clé doit être générée de manière sécurisée + this.ENCRYPTION_KEY = + process.env.CONFIG_ENCRYPTION_KEY || + crypto.randomBytes(32).toString('hex'); + + if (!process.env.CONFIG_ENCRYPTION_KEY) { + this.logger.warn( + '⚠️ CONFIG_ENCRYPTION_KEY non définie. Utilisation d\'une clé temporaire (NON RECOMMANDÉ EN PRODUCTION)', + ); + } + } + + /** + * Chargement du cache au démarrage de l'application + */ + async onModuleInit() { + await this.loadCache(); + } + + /** + * Chargement de toutes les configurations en cache + */ + async loadCache(): Promise { + try { + const configs = await this.configRepo.find(); + this.logger.log(`📦 Chargement de ${configs.length} configurations en cache`); + + for (const config of configs) { + let value = config.valeur; + + // Déchiffrement si nécessaire + if (config.type === 'encrypted' && value) { + try { + value = this.decrypt(value); + } catch (error) { + this.logger.error( + `❌ Erreur de déchiffrement pour la clé '${config.cle}'`, + error, + ); + value = null; + } + } + + // Conversion de type + const convertedValue = this.convertType(value, config.type); + this.cache.set(config.cle, convertedValue); + } + + this.logger.log('✅ Cache de configuration chargé avec succès'); + } catch (error) { + this.logger.error('❌ Erreur lors du chargement du cache', error); + throw error; + } + } + + /** + * Récupération d'une valeur de configuration + * @param key Clé de configuration + * @param defaultValue Valeur par défaut si la clé n'existe pas + * @returns Valeur de configuration + */ + get(key: string, defaultValue?: T): T { + if (this.cache.has(key)) { + return this.cache.get(key) as T; + } + + if (defaultValue !== undefined) { + return defaultValue; + } + + this.logger.warn(`⚠️ Configuration '${key}' non trouvée et aucune valeur par défaut fournie`); + return undefined as T; + } + + /** + * Mise à jour d'une valeur de configuration + * @param key Clé de configuration + * @param value Nouvelle valeur + * @param userId ID de l'utilisateur qui modifie + */ + async set(key: string, value: any, userId?: string): Promise { + const config = await this.configRepo.findOne({ where: { cle: key } }); + + if (!config) { + throw new Error(`Configuration '${key}' non trouvée`); + } + + let valueToStore = value !== null && value !== undefined ? String(value) : null; + + // Chiffrement si nécessaire + if (config.type === 'encrypted' && valueToStore) { + valueToStore = this.encrypt(valueToStore); + } + + config.valeur = valueToStore; + config.modifieLe = new Date(); + + if (userId) { + config.modifiePar = { id: userId } as any; + } + + await this.configRepo.save(config); + + // Mise à jour du cache (avec la valeur déchiffrée) + this.cache.set(key, value); + + this.logger.log(`✅ Configuration '${key}' mise à jour`); + } + + /** + * Récupération de toutes les configurations par catégorie + * @param category Catégorie de configuration + * @returns Objet clé/valeur des configurations + */ + async getByCategory(category: string): Promise> { + const configs = await this.configRepo.find({ + where: { categorie: category as any }, + }); + + const result: Record = {}; + + for (const config of configs) { + let value = config.valeur; + + // Masquer les mots de passe + if (config.type === 'encrypted') { + value = value ? '***********' : null; + } else { + value = this.convertType(value, config.type); + } + + result[config.cle] = { + value, + type: config.type, + description: config.description, + }; + } + + return result; + } + + /** + * Récupération de toutes les configurations (pour l'admin) + * @returns Liste de toutes les configurations + */ + async getAll(): Promise { + const configs = await this.configRepo.find({ + order: { categorie: 'ASC', cle: 'ASC' }, + }); + + // Masquer les valeurs chiffrées + return configs.map((config) => ({ + ...config, + valeur: config.type === 'encrypted' && config.valeur ? '***********' : config.valeur, + })); + } + + /** + * Test de la configuration SMTP + * @param testEmail Email de destination pour le test + * @returns Objet avec success et error éventuel + */ + async testSmtpConnection(testEmail?: string): Promise<{ success: boolean; error?: string }> { + try { + this.logger.log('🧪 Test de connexion SMTP...'); + + // Récupération de la configuration SMTP + const smtpHost = this.get('smtp_host'); + const smtpPort = this.get('smtp_port'); + const smtpSecure = this.get('smtp_secure'); + const smtpAuthRequired = this.get('smtp_auth_required'); + const smtpUser = this.get('smtp_user'); + const smtpPassword = this.get('smtp_password'); + const emailFromName = this.get('email_from_name'); + const emailFromAddress = this.get('email_from_address'); + + // Import dynamique de nodemailer + const nodemailer = await import('nodemailer'); + + // Configuration du transporteur + const transportConfig: any = { + host: smtpHost, + port: smtpPort, + secure: smtpSecure, + }; + + if (smtpAuthRequired && smtpUser && smtpPassword) { + transportConfig.auth = { + user: smtpUser, + pass: smtpPassword, + }; + } + + const transporter = nodemailer.createTransport(transportConfig); + + // Vérification de la connexion + await transporter.verify(); + this.logger.log('✅ Connexion SMTP vérifiée'); + + // Si un email de test est fourni, on envoie un email + if (testEmail) { + await transporter.sendMail({ + from: `"${emailFromName}" <${emailFromAddress}>`, + to: testEmail, + subject: '🧪 Test de configuration SMTP - P\'titsPas', + text: 'Ceci est un email de test pour vérifier la configuration SMTP de votre application P\'titsPas.', + html: ` +
+

✅ Test de configuration SMTP réussi !

+

Ceci est un email de test pour vérifier la configuration SMTP de votre application P'titsPas.

+

Si vous recevez cet email, cela signifie que votre configuration SMTP fonctionne correctement.

+
+

+ Cet email a été envoyé automatiquement depuis votre application P'titsPas.
+ Configuration testée le ${new Date().toLocaleString('fr-FR')} +

+
+ `, + }); + this.logger.log(`📧 Email de test envoyé à ${testEmail}`); + } + + return { success: true }; + } catch (error) { + this.logger.error('❌ Échec du test SMTP', error); + return { + success: false, + error: error.message || 'Erreur inconnue lors du test SMTP', + }; + } + } + + /** + * Vérification si la configuration initiale est terminée + * @returns true si la configuration est terminée + */ + isSetupCompleted(): boolean { + return this.get('setup_completed', false); + } + + /** + * Marquer la configuration initiale comme terminée + * @param userId ID de l'utilisateur qui termine la configuration + */ + async markSetupCompleted(userId: string): Promise { + await this.set('setup_completed', 'true', userId); + this.logger.log('✅ Configuration initiale marquée comme terminée'); + } + + /** + * Conversion de type selon le type de configuration + * @param value Valeur à convertir + * @param type Type cible + * @returns Valeur convertie + */ + private convertType(value: string | null, type: string): any { + if (value === null || value === undefined) { + return null; + } + + switch (type) { + case 'number': + return parseFloat(value); + case 'boolean': + return value === 'true' || value === '1'; + case 'json': + try { + return JSON.parse(value); + } catch { + return null; + } + case 'string': + case 'encrypted': + default: + return value; + } + } + + /** + * Chiffrement AES-256-CBC + * @param text Texte à chiffrer + * @returns Texte chiffré (format: iv:encrypted) + */ + private encrypt(text: string): string { + const iv = crypto.randomBytes(this.IV_LENGTH); + const key = Buffer.from(this.ENCRYPTION_KEY, 'hex'); + const cipher = crypto.createCipheriv(this.ENCRYPTION_ALGORITHM, key, iv); + + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + // Format: iv:encrypted + return `${iv.toString('hex')}:${encrypted}`; + } + + /** + * Déchiffrement AES-256-CBC + * @param encryptedText Texte chiffré (format: iv:encrypted) + * @returns Texte déchiffré + */ + private decrypt(encryptedText: string): string { + const parts = encryptedText.split(':'); + if (parts.length !== 2) { + throw new Error('Format de chiffrement invalide'); + } + + const iv = Buffer.from(parts[0], 'hex'); + const encrypted = parts[1]; + const key = Buffer.from(this.ENCRYPTION_KEY, 'hex'); + + const decipher = crypto.createDecipheriv(this.ENCRYPTION_ALGORITHM, key, iv); + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; + } +} + diff --git a/backend/src/modules/config/dto/test-smtp.dto.ts b/backend/src/modules/config/dto/test-smtp.dto.ts new file mode 100644 index 0000000..42968b2 --- /dev/null +++ b/backend/src/modules/config/dto/test-smtp.dto.ts @@ -0,0 +1,7 @@ +import { IsEmail } from 'class-validator'; + +export class TestSmtpDto { + @IsEmail() + testEmail: string; +} + diff --git a/backend/src/modules/config/dto/update-config.dto.ts b/backend/src/modules/config/dto/update-config.dto.ts new file mode 100644 index 0000000..04e392d --- /dev/null +++ b/backend/src/modules/config/dto/update-config.dto.ts @@ -0,0 +1,67 @@ +import { IsString, IsOptional, IsNumber, IsBoolean, IsEmail, IsUrl } from 'class-validator'; + +export class UpdateConfigDto { + // Configuration Email (SMTP) + @IsOptional() + @IsString() + smtp_host?: string; + + @IsOptional() + @IsNumber() + smtp_port?: number; + + @IsOptional() + @IsBoolean() + smtp_secure?: boolean; + + @IsOptional() + @IsBoolean() + smtp_auth_required?: boolean; + + @IsOptional() + @IsString() + smtp_user?: string; + + @IsOptional() + @IsString() + smtp_password?: string; + + @IsOptional() + @IsString() + email_from_name?: string; + + @IsOptional() + @IsEmail() + email_from_address?: string; + + // Configuration Application + @IsOptional() + @IsString() + app_name?: string; + + @IsOptional() + @IsUrl() + app_url?: string; + + @IsOptional() + @IsString() + app_logo_url?: string; + + // Configuration Sécurité + @IsOptional() + @IsNumber() + password_reset_token_expiry_days?: number; + + @IsOptional() + @IsNumber() + jwt_expiry_hours?: number; + + @IsOptional() + @IsNumber() + max_upload_size_mb?: number; + + @IsOptional() + @IsNumber() + bcrypt_rounds?: number; +} + diff --git a/backend/src/modules/config/index.ts b/backend/src/modules/config/index.ts new file mode 100644 index 0000000..9f4ac4b --- /dev/null +++ b/backend/src/modules/config/index.ts @@ -0,0 +1,3 @@ +export * from './config.module'; +export * from './config.service'; + diff --git a/backend/src/modules/documents-legaux/documents-legaux.controller.ts b/backend/src/modules/documents-legaux/documents-legaux.controller.ts new file mode 100644 index 0000000..0bef1e8 --- /dev/null +++ b/backend/src/modules/documents-legaux/documents-legaux.controller.ts @@ -0,0 +1,202 @@ +import { + Controller, + Get, + Post, + Patch, + Param, + Body, + UseInterceptors, + UploadedFile, + Res, + HttpStatus, + BadRequestException, + ParseUUIDPipe, + StreamableFile, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import type { Response } from 'express'; +import { DocumentsLegauxService } from './documents-legaux.service'; +import { UploadDocumentDto } from './dto/upload-document.dto'; +import { DocumentsActifsResponseDto } from './dto/documents-actifs.dto'; +import { DocumentVersionDto } from './dto/document-version.dto'; + +@Controller('documents-legaux') +export class DocumentsLegauxController { + constructor(private readonly documentsService: DocumentsLegauxService) {} + + /** + * GET /api/v1/documents-legaux/actifs + * Récupérer les documents actifs (CGU + Privacy) + * PUBLIC + */ + @Get('actifs') + async getDocumentsActifs(): Promise { + const { cgu, privacy } = await this.documentsService.getDocumentsActifs(); + + return { + cgu: { + id: cgu.id, + type: cgu.type, + version: cgu.version, + url: `/api/v1/documents-legaux/${cgu.id}/download`, + activeLe: cgu.activeLe, + }, + privacy: { + id: privacy.id, + type: privacy.type, + version: privacy.version, + url: `/api/v1/documents-legaux/${privacy.id}/download`, + activeLe: privacy.activeLe, + }, + }; + } + + /** + * GET /api/v1/documents-legaux/:type/versions + * Lister toutes les versions d'un type de document + * ADMIN ONLY + */ + @Get(':type/versions') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin', 'administrateur') + async listerVersions(@Param('type') type: string): Promise { + if (type !== 'cgu' && type !== 'privacy') { + throw new BadRequestException('Le type doit être "cgu" ou "privacy"'); + } + + const documents = await this.documentsService.listerVersions(type as 'cgu' | 'privacy'); + + return documents.map((doc) => ({ + id: doc.id, + version: doc.version, + fichier_nom: doc.fichier_nom, + actif: doc.actif, + televersePar: doc.televersePar + ? { + id: doc.televersePar.id, + prenom: doc.televersePar.prenom, + nom: doc.televersePar.nom, + } + : null, + televerseLe: doc.televerseLe, + activeLe: doc.activeLe, + })); + } + + /** + * POST /api/v1/documents-legaux + * Upload une nouvelle version d'un document + * ADMIN ONLY + */ + @Post() + @UseInterceptors(FileInterceptor('file')) + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin', 'administrateur') + async uploadDocument( + @Body() uploadDto: UploadDocumentDto, + @UploadedFile() file: Express.Multer.File, + // @CurrentUser() user: any, // TODO: Décommenter quand le guard sera implémenté + ) { + if (!file) { + throw new BadRequestException('Aucun fichier fourni'); + } + + // TODO: Récupérer l'ID utilisateur depuis le guard + const userId = '00000000-0000-0000-0000-000000000000'; // Temporaire + + const document = await this.documentsService.uploadNouvelleVersion( + uploadDto.type, + file, + userId, + ); + + return { + id: document.id, + type: document.type, + version: document.version, + fichier_nom: document.fichier_nom, + actif: document.actif, + televerseLe: document.televerseLe, + }; + } + + /** + * PATCH /api/v1/documents-legaux/:id/activer + * Activer une version d'un document + * ADMIN ONLY + */ + @Patch(':id/activer') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin', 'administrateur') + async activerVersion(@Param('id', ParseUUIDPipe) documentId: string) { + await this.documentsService.activerVersion(documentId); + + // Récupérer le document pour retourner les infos + const documents = await this.documentsService.listerVersions('cgu'); + const document = documents.find((d) => d.id === documentId); + + if (!document) { + const documentsPrivacy = await this.documentsService.listerVersions('privacy'); + const docPrivacy = documentsPrivacy.find((d) => d.id === documentId); + + if (!docPrivacy) { + throw new BadRequestException('Document non trouvé'); + } + + return { + message: 'Document activé avec succès', + documentId: docPrivacy.id, + type: docPrivacy.type, + version: docPrivacy.version, + }; + } + + return { + message: 'Document activé avec succès', + documentId: document.id, + type: document.type, + version: document.version, + }; + } + + /** + * GET /api/v1/documents-legaux/:id/download + * Télécharger un document + * PUBLIC + */ + @Get(':id/download') + async telechargerDocument( + @Param('id', ParseUUIDPipe) documentId: string, + @Res() res: Response, + ) { + const { stream, filename } = await this.documentsService.telechargerDocument(documentId); + + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${filename}"`, + }); + + res.status(HttpStatus.OK).send(stream); + } + + /** + * GET /api/v1/documents-legaux/:id/verifier-integrite + * Vérifier l'intégrité d'un document (hash SHA-256) + * ADMIN ONLY + */ + @Get(':id/verifier-integrite') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('super_admin', 'administrateur') + async verifierIntegrite(@Param('id', ParseUUIDPipe) documentId: string) { + const integre = await this.documentsService.verifierIntegrite(documentId); + + return { + documentId, + integre, + message: integre + ? 'Le document est intègre (hash valide)' + : 'ALERTE : Le document a été modifié (hash invalide)', + }; + } +} + diff --git a/backend/src/modules/documents-legaux/documents-legaux.module.ts b/backend/src/modules/documents-legaux/documents-legaux.module.ts new file mode 100644 index 0000000..13828af --- /dev/null +++ b/backend/src/modules/documents-legaux/documents-legaux.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DocumentLegal } from '../../entities/document-legal.entity'; +import { AcceptationDocument } from '../../entities/acceptation-document.entity'; +import { DocumentsLegauxService } from './documents-legaux.service'; +import { DocumentsLegauxController } from './documents-legaux.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([DocumentLegal, AcceptationDocument])], + providers: [DocumentsLegauxService], + controllers: [DocumentsLegauxController], + exports: [DocumentsLegauxService], +}) +export class DocumentsLegauxModule {} + diff --git a/backend/src/modules/documents-legaux/documents-legaux.service.ts b/backend/src/modules/documents-legaux/documents-legaux.service.ts new file mode 100644 index 0000000..02ce74b --- /dev/null +++ b/backend/src/modules/documents-legaux/documents-legaux.service.ts @@ -0,0 +1,209 @@ +import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DocumentLegal } from '../../entities/document-legal.entity'; +import { AcceptationDocument } from '../../entities/acceptation-document.entity'; +import * as crypto from 'crypto'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +@Injectable() +export class DocumentsLegauxService { + private readonly UPLOAD_DIR = '/app/documents/legaux'; + + constructor( + @InjectRepository(DocumentLegal) + private docRepo: Repository, + @InjectRepository(AcceptationDocument) + private acceptationRepo: Repository, + ) {} + + /** + * Récupérer les documents actifs (CGU + Privacy) + */ + async getDocumentsActifs(): Promise<{ cgu: DocumentLegal; privacy: DocumentLegal }> { + const cgu = await this.docRepo.findOne({ + where: { type: 'cgu', actif: true }, + }); + + const privacy = await this.docRepo.findOne({ + where: { type: 'privacy', actif: true }, + }); + + if (!cgu || !privacy) { + throw new NotFoundException('Documents légaux manquants'); + } + + return { cgu, privacy }; + } + + /** + * Uploader une nouvelle version d'un document + */ + async uploadNouvelleVersion( + type: 'cgu' | 'privacy', + file: Express.Multer.File, + userId: string, + ): Promise { + // Validation du type de fichier + if (file.mimetype !== 'application/pdf') { + throw new BadRequestException('Seuls les fichiers PDF sont acceptés'); + } + + // Validation de la taille (max 10MB) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + throw new BadRequestException('Le fichier ne doit pas dépasser 10MB'); + } + + // 1. Calculer la prochaine version + const lastDoc = await this.docRepo.findOne({ + where: { type }, + order: { version: 'DESC' }, + }); + const nouvelleVersion = (lastDoc?.version || 0) + 1; + + // 2. Calculer le hash SHA-256 du fichier + const fileBuffer = file.buffer; + const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + + // 3. Générer le nom de fichier unique + const timestamp = Date.now(); + const fileName = `${type}_v${nouvelleVersion}_${timestamp}.pdf`; + const filePath = path.join(this.UPLOAD_DIR, fileName); + + // 4. Créer le répertoire si nécessaire et sauvegarder le fichier + await fs.mkdir(this.UPLOAD_DIR, { recursive: true }); + await fs.writeFile(filePath, fileBuffer); + + // 5. Créer l'entrée en BDD + const document = this.docRepo.create({ + type, + version: nouvelleVersion, + fichier_nom: file.originalname, + fichier_path: filePath, + fichier_hash: hash, + actif: false, // Pas actif par défaut + televersePar: { id: userId } as any, + televerseLe: new Date(), + }); + + return await this.docRepo.save(document); + } + + /** + * Activer une version (désactive automatiquement l'ancienne) + */ + async activerVersion(documentId: string): Promise { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new NotFoundException('Document non trouvé'); + } + + // Transaction : désactiver l'ancienne version, activer la nouvelle + await this.docRepo.manager.transaction(async (manager) => { + // Désactiver toutes les versions de ce type + await manager.update( + DocumentLegal, + { type: document.type, actif: true }, + { actif: false }, + ); + + // Activer la nouvelle version + await manager.update( + DocumentLegal, + { id: documentId }, + { actif: true, activeLe: new Date() }, + ); + }); + } + + /** + * Lister toutes les versions d'un type de document + */ + async listerVersions(type: 'cgu' | 'privacy'): Promise { + return await this.docRepo.find({ + where: { type }, + order: { version: 'DESC' }, + relations: ['televersePar'], + }); + } + + /** + * Télécharger un document (retourne le buffer et le nom) + */ + async telechargerDocument(documentId: string): Promise<{ stream: Buffer; filename: string }> { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new NotFoundException('Document non trouvé'); + } + + try { + const fileBuffer = await fs.readFile(document.fichier_path); + + return { + stream: fileBuffer, + filename: document.fichier_nom, + }; + } catch (error) { + throw new NotFoundException('Fichier introuvable sur le système de fichiers'); + } + } + + /** + * Vérifier l'intégrité d'un document (hash SHA-256) + */ + async verifierIntegrite(documentId: string): Promise { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new NotFoundException('Document non trouvé'); + } + + try { + const fileBuffer = await fs.readFile(document.fichier_path); + const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + + return hash === document.fichier_hash; + } catch (error) { + return false; + } + } + + /** + * Enregistrer une acceptation de document (lors de l'inscription) + */ + async enregistrerAcceptation( + userId: string, + documentId: string, + typeDocument: 'cgu' | 'privacy', + versionDocument: number, + ipAddress: string | null, + userAgent: string | null, + ): Promise { + const acceptation = this.acceptationRepo.create({ + utilisateur: { id: userId } as any, + document: { id: documentId } as any, + type_document: typeDocument, + version_document: versionDocument, + ip_address: ipAddress, + user_agent: userAgent, + }); + + return await this.acceptationRepo.save(acceptation); + } + + /** + * Récupérer l'historique des acceptations d'un utilisateur + */ + async getAcceptationsUtilisateur(userId: string): Promise { + return await this.acceptationRepo.find({ + where: { utilisateur: { id: userId } }, + order: { accepteLe: 'DESC' }, + relations: ['document'], + }); + } +} + diff --git a/backend/src/modules/documents-legaux/dto/document-version.dto.ts b/backend/src/modules/documents-legaux/dto/document-version.dto.ts new file mode 100644 index 0000000..014b7ad --- /dev/null +++ b/backend/src/modules/documents-legaux/dto/document-version.dto.ts @@ -0,0 +1,14 @@ +export class DocumentVersionDto { + id: string; + version: number; + fichier_nom: string; + actif: boolean; + televersePar: { + id: string; + prenom?: string; + nom?: string; + } | null; + televerseLe: Date; + activeLe: Date | null; +} + diff --git a/backend/src/modules/documents-legaux/dto/documents-actifs.dto.ts b/backend/src/modules/documents-legaux/dto/documents-actifs.dto.ts new file mode 100644 index 0000000..cafa8f2 --- /dev/null +++ b/backend/src/modules/documents-legaux/dto/documents-actifs.dto.ts @@ -0,0 +1,13 @@ +export class DocumentActifDto { + id: string; + type: 'cgu' | 'privacy'; + version: number; + url: string; + activeLe: Date | null; +} + +export class DocumentsActifsResponseDto { + cgu: DocumentActifDto; + privacy: DocumentActifDto; +} + diff --git a/backend/src/modules/documents-legaux/dto/upload-document.dto.ts b/backend/src/modules/documents-legaux/dto/upload-document.dto.ts new file mode 100644 index 0000000..52a3546 --- /dev/null +++ b/backend/src/modules/documents-legaux/dto/upload-document.dto.ts @@ -0,0 +1,8 @@ +import { IsEnum, IsNotEmpty } from 'class-validator'; + +export class UploadDocumentDto { + @IsEnum(['cgu', 'privacy'], { message: 'Le type doit être "cgu" ou "privacy"' }) + @IsNotEmpty({ message: 'Le type est requis' }) + type: 'cgu' | 'privacy'; +} + diff --git a/backend/src/modules/documents-legaux/index.ts b/backend/src/modules/documents-legaux/index.ts new file mode 100644 index 0000000..28593ce --- /dev/null +++ b/backend/src/modules/documents-legaux/index.ts @@ -0,0 +1,3 @@ +export * from './documents-legaux.module'; +export * from './documents-legaux.service'; + diff --git a/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.spec.ts b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.spec.ts new file mode 100644 index 0000000..b0932ef --- /dev/null +++ b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AssistantesMaternellesController } from './assistantes_maternelles.controller'; +import { AssistantesMaternellesService } from './assistantes_maternelles.service'; + +describe('AssistantesMaternellesController', () => { + let controller: AssistantesMaternellesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AssistantesMaternellesController], + providers: [AssistantesMaternellesService], + }).compile(); + + controller = module.get(AssistantesMaternellesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.ts b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.ts new file mode 100644 index 0000000..d803c20 --- /dev/null +++ b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.controller.ts @@ -0,0 +1,81 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, +} from '@nestjs/common'; +import { AssistantesMaternellesService } from './assistantes_maternelles.service'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; +import { Roles } from 'src/common/decorators/roles.decorator'; +import { RoleType } from 'src/entities/users.entity'; +import { CreateAssistanteDto } from '../user/dto/create_assistante.dto'; +import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto'; +import { RolesGuard } from 'src/common/guards/roles.guard'; +import { AuthGuard } from 'src/common/guards/auth.guard'; + +@ApiTags("Assistantes Maternelles") +@ApiBearerAuth('access-token') +@UseGuards(AuthGuard, RolesGuard) +@Controller('assistantes-maternelles') +export class AssistantesMaternellesController { + constructor(private readonly assistantesMaternellesService: AssistantesMaternellesService) { } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @ApiOperation({ summary: 'Créer nounou' }) + @ApiResponse({ status: 201, description: 'Nounou créée avec succès' }) + @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' }) + @ApiBody({ type: CreateAssistanteDto }) + @Post() + create(@Body() dto: CreateAssistanteDto): Promise { + return this.assistantesMaternellesService.create(dto); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Get() + @ApiOperation({ summary: 'Récupérer la liste des nounous' }) + @ApiResponse({ status: 200, description: 'Liste des nounous' }) + @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' }) + getAll(): Promise { + return this.assistantesMaternellesService.findAll(); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Get(':id') + @ApiParam({ name: 'id', description: "UUID de la nounou" }) + @ApiOperation({ summary: 'Récupérer une nounou par id' }) + @ApiResponse({ status: 200, description: 'Détails de la nounou' }) + @ApiResponse({ status: 404, description: 'Nounou non trouvée' }) + @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' }) + getOne(@Param('id') user_id: string): Promise { + return this.assistantesMaternellesService.findOne(user_id); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @ApiBody({ type: UpdateAssistanteDto }) + @ApiOperation({ summary: 'Mettre à jour une nounou' }) + @ApiResponse({ status: 200, description: 'Nounou mise à jour avec succès' }) + @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins et gestionnaires' }) + @ApiResponse({ status: 404, description: 'Nounou non trouvée' }) + @ApiParam({ name: 'id', description: "UUID de la nounou" }) + @Patch(':id') + update(@Param('id') id: string, @Body() dto: UpdateAssistanteDto): Promise { + return this.assistantesMaternellesService.update(id, dto); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) + @ApiOperation({ summary: 'Supprimer une nounou' }) + @ApiResponse({ status: 200, description: 'Nounou supprimée avec succès' }) + @ApiResponse({ status: 403, description: 'Accès refusé : Réservé aux super_admins, gestionnaires et administrateurs' }) + @ApiResponse({ status: 404, description: 'Nounou non trouvée' }) + @ApiParam({ name: 'id', description: "UUID de la nounou" }) + @Delete(':id') + remove(@Param('id') id: string): Promise<{ message: string }> + { + return this.assistantesMaternellesService.remove(id); + } +} diff --git a/backend/src/routes/assistantes_maternelles/assistantes_maternelles.module.ts b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.module.ts new file mode 100644 index 0000000..9e2e231 --- /dev/null +++ b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { AssistantesMaternellesService } from './assistantes_maternelles.service'; +import { AssistantesMaternellesController } from './assistantes_maternelles.controller'; +import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Users } from 'src/entities/users.entity'; +import { AuthModule } from '../auth/auth.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([AssistanteMaternelle, Users]), + AuthModule + ], + controllers: [AssistantesMaternellesController], + providers: [AssistantesMaternellesService], + exports: [ + AssistantesMaternellesService, + TypeOrmModule, + ], +}) +export class AssistantesMaternellesModule { } diff --git a/backend/src/routes/assistantes_maternelles/assistantes_maternelles.service.spec.ts b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.service.spec.ts new file mode 100644 index 0000000..d4d7d5e --- /dev/null +++ b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AssistantesMaternellesService } from './assistantes_maternelles.service'; + +describe('AssistantesMaternellesService', () => { + let service: AssistantesMaternellesService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AssistantesMaternellesService], + }).compile(); + + service = module.get(AssistantesMaternellesService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/routes/assistantes_maternelles/assistantes_maternelles.service.ts b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.service.ts new file mode 100644 index 0000000..c9ba68f --- /dev/null +++ b/backend/src/routes/assistantes_maternelles/assistantes_maternelles.service.ts @@ -0,0 +1,80 @@ +import { + BadRequestException, + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { RoleType, Users } from 'src/entities/users.entity'; +import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; +import { CreateAssistanteDto } from '../user/dto/create_assistante.dto'; +import { UpdateAssistanteDto } from '../user/dto/update_assistante.dto'; + +@Injectable() +export class AssistantesMaternellesService { + constructor( + @InjectRepository(AssistanteMaternelle) + private readonly assistantesMaternelleRepository: Repository, + @InjectRepository(Users) + private readonly usersRepository: Repository + ) {} + + // Création d’une assistante maternelle + async create(dto: CreateAssistanteDto): Promise { + const user = await this.usersRepository.findOneBy({ id: dto.user_id }); + if (!user) throw new NotFoundException('Utilisateur introuvable'); + if (user.role !== RoleType.ASSISTANTE_MATERNELLE) { + throw new BadRequestException('Accès réservé aux assistantes maternelles'); + } + + const exist = await this.assistantesMaternelleRepository.findOneBy({ user_id: dto.user_id }); + if (exist) throw new ConflictException('Assistante maternelle déjà existante'); + + const entity = this.assistantesMaternelleRepository.create({ + user_id: dto.user_id, + user: { ...user, role: RoleType.ASSISTANTE_MATERNELLE }, + approval_number: dto.approval_number, + nir: dto.nir, + max_children: dto.max_children, + biography: dto.biography, + available: dto.available ?? true, + residence_city: dto.residence_city, + agreement_date: dto.agreement_date ? new Date(dto.agreement_date) : undefined, + years_experience: dto.years_experience, + specialty: dto.specialty, + places_available: dto.places_available, + }); + + return this.assistantesMaternelleRepository.save(entity); + } + + // Liste des assistantes maternelles + async findAll(): Promise { + return this.assistantesMaternelleRepository.find({ + relations: ['user'], + }); + } + + // Récupérer une assistante maternelle par user_id + async findOne(user_id: string): Promise { + const assistante = await this.assistantesMaternelleRepository.findOne({ + where: { user_id }, + relations: ['user'], + }); + if (!assistante) throw new NotFoundException('Assistante maternelle introuvable'); + return assistante; + } + + // Mise à jour + async update(id: string, dto: UpdateAssistanteDto): Promise { + await this.assistantesMaternelleRepository.update(id, dto); + return this.findOne(id); + } + + // Suppression d’une assistante maternelle + async remove(id: string): Promise<{ message: string }> { + await this.assistantesMaternelleRepository.delete(id); + return { message: 'Assistante maternelle supprimée' }; + } +} diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts deleted file mode 100644 index 31cef32..0000000 --- a/backend/src/routes/auth.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Router } from 'express'; -import { PrismaClient } from '@prisma/client'; -import * as bcrypt from 'bcrypt'; -import jwt from 'jsonwebtoken'; - -const router = Router(); -const prisma = new PrismaClient(); -const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; - -// Route de connexion -router.post('/login', async (req, res) => { - try { - const { email, password } = req.body; - - // Vérifier les identifiants - const admin = await prisma.admin.findUnique({ - where: { email } - }); - - if (!admin) { - return res.status(401).json({ error: 'Identifiants invalides' }); - } - - // Vérifier le mot de passe - const validPassword = await bcrypt.compare(password, admin.password); - if (!validPassword) { - return res.status(401).json({ error: 'Identifiants invalides' }); - } - - // Vérifier si le mot de passe doit être changé - if (!admin.passwordChanged) { - return res.status(403).json({ - error: 'Changement de mot de passe requis', - requiresPasswordChange: true - }); - } - - // Générer le token JWT - const token = jwt.sign( - { - id: admin.id, - email: admin.email, - role: 'admin' - }, - JWT_SECRET, - { expiresIn: '24h' } - ); - - res.json({ token }); - } catch (error) { - console.error('Erreur lors de la connexion:', error); - res.status(500).json({ error: 'Erreur serveur' }); - } -}); - -// Route de changement de mot de passe -router.post('/change-password', async (req, res) => { - try { - const { email, currentPassword, newPassword } = req.body; - - // Vérifier l'administrateur - const admin = await prisma.admin.findUnique({ - where: { email } - }); - - if (!admin) { - return res.status(404).json({ error: 'Administrateur non trouvé' }); - } - - // Vérifier l'ancien mot de passe - const validPassword = await bcrypt.compare(currentPassword, admin.password); - if (!validPassword) { - return res.status(401).json({ error: 'Mot de passe actuel incorrect' }); - } - - // Hasher le nouveau mot de passe - const hashedPassword = await bcrypt.hash(newPassword, 10); - - // Mettre à jour le mot de passe - await prisma.admin.update({ - where: { id: admin.id }, - data: { - password: hashedPassword, - passwordChanged: true - } - }); - - res.json({ message: 'Mot de passe changé avec succès' }); - } catch (error) { - console.error('Erreur lors du changement de mot de passe:', error); - res.status(500).json({ error: 'Erreur serveur' }); - } -}); - -export default router; \ No newline at end of file diff --git a/backend/src/routes/auth/auth.controller.spec.ts b/backend/src/routes/auth/auth.controller.spec.ts new file mode 100644 index 0000000..27a31e6 --- /dev/null +++ b/backend/src/routes/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/routes/auth/auth.controller.ts b/backend/src/routes/auth/auth.controller.ts new file mode 100644 index 0000000..3de3fc1 --- /dev/null +++ b/backend/src/routes/auth/auth.controller.ts @@ -0,0 +1,128 @@ +import { Body, Controller, Get, Post, Req, UnauthorizedException, BadRequestException, UseGuards } from '@nestjs/common'; +import { LoginDto } from './dto/login.dto'; +import { AuthService } from './auth.service'; +import { Public } from 'src/common/decorators/public.decorator'; +import { RegisterDto } from './dto/register.dto'; +import { RegisterParentDto } from './dto/register-parent.dto'; +import { RegisterParentCompletDto } from './dto/register-parent-complet.dto'; +import { ChangePasswordRequiredDto } from './dto/change-password.dto'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthGuard } from 'src/common/guards/auth.guard'; +import type { Request } from 'express'; +import { UserService } from '../user/user.service'; +import { ProfileResponseDto } from './dto/profile_response.dto'; +import { RefreshTokenDto } from './dto/refresh_token.dto'; +import { User } from 'src/common/decorators/user.decorator'; +import { Users } from 'src/entities/users.entity'; + +@ApiTags('Authentification') +@Controller('auth') +export class AuthController { + constructor( + private readonly authService: AuthService, + private readonly userService: UserService, + ) { } + + + @Public() + @ApiOperation({ summary: 'Connexion' }) + @Post('login') + async login(@Body() dto: LoginDto) { + return this.authService.login(dto); + } + + @Public() + @Post('register') + @ApiOperation({ summary: 'Inscription (OBSOLÈTE - utiliser /register/parent)' }) + @ApiResponse({ status: 409, description: 'Email déjà utilisé' }) + async register(@Body() dto: RegisterDto) { + return this.authService.register(dto); + } + + @Public() + @Post('register/parent') + @ApiOperation({ + summary: 'Inscription Parent COMPLÈTE - Workflow 6 étapes', + description: 'Crée Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU en une transaction' + }) + @ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' }) + @ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' }) + @ApiResponse({ status: 409, description: 'Email déjà utilisé' }) + async inscrireParentComplet(@Body() dto: RegisterParentCompletDto) { + return this.authService.inscrireParentComplet(dto); + } + + @Public() + @Post('register/parent/legacy') + @ApiOperation({ summary: '[OBSOLÈTE] Inscription Parent (étape 1/6 uniquement)' }) + @ApiResponse({ status: 201, description: 'Inscription réussie' }) + @ApiResponse({ status: 409, description: 'Email déjà utilisé' }) + async registerParentLegacy(@Body() dto: RegisterParentDto) { + return this.authService.registerParent(dto); + } + + @Public() + @Post('refresh') + @ApiBearerAuth('refresh_token') + @ApiResponse({ status: 200, description: 'Nouveaux tokens générés avec succès.' }) + @ApiResponse({ status: 401, description: 'Token de rafraîchissement invalide ou expiré.' }) + @ApiOperation({ summary: 'Rafraichir les tokens' }) + async refresh(@Body() dto: RefreshTokenDto) { + return this.authService.refreshTokens(dto.refresh_token); + } + + @Get('me') + @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') + @ApiOperation({ summary: "Récupérer le profil complet de l'utilisateur connecté" }) + @ApiResponse({ status: 200, type: ProfileResponseDto }) + async getProfile(@Req() req: Request): Promise { + if (!req.user || !req.user.sub) { + throw new UnauthorizedException('Utilisateur non authentifié'); + } + const user = await this.userService.findOne(req.user.sub); + return { + id: user.id, + email: user.email, + role: user.role, + prenom: user.prenom ?? '', + nom: user.nom ?? '', + statut: user.statut, + changement_mdp_obligatoire: user.changement_mdp_obligatoire, + }; + } + + @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') + @Post('logout') + logout(@User() currentUser: Users) { + return this.authService.logout(currentUser.id); + } + + @Post('change-password-required') + @UseGuards(AuthGuard) + @ApiBearerAuth('access-token') + @ApiOperation({ + summary: 'Changement de mot de passe obligatoire', + description: 'Permet de changer le mot de passe lors de la première connexion (flag changement_mdp_obligatoire)' + }) + @ApiResponse({ status: 200, description: 'Mot de passe changé avec succès' }) + @ApiResponse({ status: 400, description: 'Mot de passe actuel incorrect ou confirmation non correspondante' }) + @ApiResponse({ status: 403, description: 'Changement de mot de passe non requis pour cet utilisateur' }) + async changePasswordRequired( + @User() currentUser: Users, + @Body() dto: ChangePasswordRequiredDto, + ) { + // Vérifier que les mots de passe correspondent + if (dto.nouveau_mot_de_passe !== dto.confirmation_mot_de_passe) { + throw new BadRequestException('Les mots de passe ne correspondent pas'); + } + + return this.authService.changePasswordRequired( + currentUser.id, + dto.mot_de_passe_actuel, + dto.nouveau_mot_de_passe, + ); + } +} + diff --git a/backend/src/routes/auth/auth.module.ts b/backend/src/routes/auth/auth.module.ts new file mode 100644 index 0000000..6554be7 --- /dev/null +++ b/backend/src/routes/auth/auth.module.ts @@ -0,0 +1,31 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { UserModule } from '../user/user.module'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { Users } from 'src/entities/users.entity'; +import { Parents } from 'src/entities/parents.entity'; +import { Children } from 'src/entities/children.entity'; +import { AppConfigModule } from 'src/modules/config'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Users, Parents, Children]), + forwardRef(() => UserModule), + AppConfigModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: (config: ConfigService) => ({ + secret: config.get('jwt.secret'), + signOptions: { expiresIn: config.get('jwt.expirationTime') }, + }), + inject: [ConfigService], + }) + ], + controllers: [AuthController], + providers: [AuthService], + exports: [AuthService, JwtModule], +}) +export class AuthModule {} diff --git a/backend/src/routes/auth/auth.service.spec.ts b/backend/src/routes/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/backend/src/routes/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/routes/auth/auth.service.ts b/backend/src/routes/auth/auth.service.ts new file mode 100644 index 0000000..1c9985e --- /dev/null +++ b/backend/src/routes/auth/auth.service.ts @@ -0,0 +1,515 @@ +import { + ConflictException, + Injectable, + UnauthorizedException, + BadRequestException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { UserService } from '../user/user.service'; +import { JwtService } from '@nestjs/jwt'; +import * as bcrypt from 'bcrypt'; +import * as crypto from 'crypto'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { RegisterDto } from './dto/register.dto'; +import { RegisterParentDto } from './dto/register-parent.dto'; +import { RegisterParentCompletDto } from './dto/register-parent-complet.dto'; +import { ConfigService } from '@nestjs/config'; +import { RoleType, StatutUtilisateurType, Users } from 'src/entities/users.entity'; +import { Parents } from 'src/entities/parents.entity'; +import { Children, StatutEnfantType } from 'src/entities/children.entity'; +import { ParentsChildren } from 'src/entities/parents_children.entity'; +import { LoginDto } from './dto/login.dto'; +import { AppConfigService } from 'src/modules/config/config.service'; + +@Injectable() +export class AuthService { + constructor( + private readonly usersService: UserService, + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + private readonly appConfigService: AppConfigService, + @InjectRepository(Parents) + private readonly parentsRepo: Repository, + @InjectRepository(Users) + private readonly usersRepo: Repository, + @InjectRepository(Children) + private readonly childrenRepo: Repository, + ) { } + + /** + * Génère un access_token et un refresh_token + */ + async generateTokens(userId: string, email: string, role: RoleType) { + const accessSecret = this.configService.get('jwt.accessSecret'); + const accessExpiresIn = this.configService.get('jwt.accessExpiresIn'); + const refreshSecret = this.configService.get('jwt.refreshSecret'); + const refreshExpiresIn = this.configService.get('jwt.refreshExpiresIn'); + + const [accessToken, refreshToken] = await Promise.all([ + this.jwtService.signAsync({ sub: userId, email, role }, { secret: accessSecret, expiresIn: accessExpiresIn }), + this.jwtService.signAsync({ sub: userId }, { secret: refreshSecret, expiresIn: refreshExpiresIn }), + ]); + + return { + access_token: accessToken, + refresh_token: refreshToken, + }; + } + + /** + * Connexion utilisateur + */ + async login(dto: LoginDto) { + const user = await this.usersService.findByEmailOrNull(dto.email); + + if (!user) { + throw new UnauthorizedException('Identifiants invalides'); + } + + // Vérifier que le mot de passe existe (compte activé) + if (!user.password) { + throw new UnauthorizedException( + 'Compte non activé. Veuillez créer votre mot de passe via le lien reçu par email.', + ); + } + + // Vérifier le mot de passe + const isMatch = await bcrypt.compare(dto.password, user.password); + if (!isMatch) { + throw new UnauthorizedException('Identifiants invalides'); + } + + // Vérifier le statut du compte + if (user.statut === StatutUtilisateurType.EN_ATTENTE) { + throw new UnauthorizedException( + 'Votre compte est en attente de validation par un gestionnaire.', + ); + } + + if (user.statut === StatutUtilisateurType.SUSPENDU) { + throw new UnauthorizedException('Votre compte a été suspendu. Contactez un administrateur.'); + } + + return this.generateTokens(user.id, user.email, user.role); + } + + /** + * Rafraîchir les tokens + */ + async refreshTokens(refreshToken: string) { + try { + const payload = await this.jwtService.verifyAsync(refreshToken, { + secret: this.configService.get('jwt.refreshSecret'), + }); + + const user = await this.usersService.findOne(payload.sub); + if (!user) { + throw new UnauthorizedException('Utilisateur introuvable'); + } + + return this.generateTokens(user.id, user.email, user.role); + } catch { + throw new UnauthorizedException('Refresh token invalide'); + } + } + + /** + * Inscription utilisateur OBSOLÈTE - Utiliser registerParent() ou registerAM() + * @deprecated + */ + async register(registerDto: RegisterDto) { + const exists = await this.usersService.findByEmailOrNull(registerDto.email); + if (exists) { + throw new ConflictException('Email déjà utilisé'); + } + + const allowedRoles = new Set([RoleType.PARENT, RoleType.ASSISTANTE_MATERNELLE]); + if (!allowedRoles.has(registerDto.role)) { + registerDto.role = RoleType.PARENT; + } + + registerDto.statut = StatutUtilisateurType.EN_ATTENTE; + + if (!registerDto.consentement_photo) { + registerDto.date_consentement_photo = null; + } else if (registerDto.date_consentement_photo) { + const date = new Date(registerDto.date_consentement_photo); + if (isNaN(date.getTime())) { + registerDto.date_consentement_photo = null; + } + } + + const user = await this.usersService.createUser(registerDto); + const tokens = await this.generateTokens(user.id, user.email, user.role); + + return { + ...tokens, + user: { + id: user.id, + email: user.email, + role: user.role, + prenom: user.prenom, + nom: user.nom, + statut: user.statut, + }, + }; + } + + /** + * Inscription Parent (étape 1/6 du workflow CDC) + * SANS mot de passe - Token de création MDP généré + */ + async registerParent(dto: RegisterParentDto) { + // 1. Vérifier que l'email n'existe pas + const exists = await this.usersService.findByEmailOrNull(dto.email); + if (exists) { + throw new ConflictException('Un compte avec cet email existe déjà'); + } + + // 2. Vérifier l'email du co-parent s'il existe + if (dto.co_parent_email) { + const coParentExists = await this.usersService.findByEmailOrNull(dto.co_parent_email); + if (coParentExists) { + throw new ConflictException('L\'email du co-parent est déjà utilisé'); + } + } + + // 3. Récupérer la durée d'expiration du token depuis la config + const tokenExpiryDays = await this.appConfigService.get( + 'password_reset_token_expiry_days', + 7, + ); + + // 4. Générer les tokens de création de mot de passe + const tokenCreationMdp = crypto.randomUUID(); + const tokenExpiration = new Date(); + tokenExpiration.setDate(tokenExpiration.getDate() + tokenExpiryDays); + + // 5. Transaction : Créer Parent 1 + Parent 2 (si existe) + entités parents + const result = await this.usersRepo.manager.transaction(async (manager) => { + // Créer Parent 1 + const parent1 = manager.create(Users, { + email: dto.email, + prenom: dto.prenom, + nom: dto.nom, + role: RoleType.PARENT, + statut: StatutUtilisateurType.EN_ATTENTE, + telephone: dto.telephone, + adresse: dto.adresse, + code_postal: dto.code_postal, + ville: dto.ville, + token_creation_mdp: tokenCreationMdp, + token_creation_mdp_expire_le: tokenExpiration, + }); + + const savedParent1 = await manager.save(Users, parent1); + + // Créer Parent 2 si renseigné + let savedParent2: Users | null = null; + let tokenCoParent: string | null = null; + + if (dto.co_parent_email && dto.co_parent_prenom && dto.co_parent_nom) { + tokenCoParent = crypto.randomUUID(); + const tokenExpirationCoParent = new Date(); + tokenExpirationCoParent.setDate(tokenExpirationCoParent.getDate() + tokenExpiryDays); + + const parent2 = manager.create(Users, { + email: dto.co_parent_email, + prenom: dto.co_parent_prenom, + nom: dto.co_parent_nom, + role: RoleType.PARENT, + statut: StatutUtilisateurType.EN_ATTENTE, + telephone: dto.co_parent_telephone, + adresse: dto.co_parent_meme_adresse ? dto.adresse : dto.co_parent_adresse, + code_postal: dto.co_parent_meme_adresse ? dto.code_postal : dto.co_parent_code_postal, + ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville, + token_creation_mdp: tokenCoParent, + token_creation_mdp_expire_le: tokenExpirationCoParent, + }); + + savedParent2 = await manager.save(Users, parent2); + } + + // Créer l'entité métier Parents pour Parent 1 + const parentEntity = manager.create(Parents, { + user_id: savedParent1.id, + }); + parentEntity.user = savedParent1; + if (savedParent2) { + parentEntity.co_parent = savedParent2; + } + + await manager.save(Parents, parentEntity); + + // Créer l'entité métier Parents pour Parent 2 (si existe) + if (savedParent2) { + const coParentEntity = manager.create(Parents, { + user_id: savedParent2.id, + }); + coParentEntity.user = savedParent2; + coParentEntity.co_parent = savedParent1; + + await manager.save(Parents, coParentEntity); + } + + return { + parent1: savedParent1, + parent2: savedParent2, + tokenCreationMdp, + tokenCoParent, + }; + }); + + // 6. TODO: Envoyer email avec lien de création de MDP + // await this.mailService.sendPasswordCreationEmail(result.parent1, result.tokenCreationMdp); + // if (result.parent2 && result.tokenCoParent) { + // await this.mailService.sendPasswordCreationEmail(result.parent2, result.tokenCoParent); + // } + + return { + message: 'Inscription réussie. Un email de validation vous a été envoyé.', + parent_id: result.parent1.id, + co_parent_id: result.parent2?.id, + statut: StatutUtilisateurType.EN_ATTENTE, + }; + } + + /** + * Inscription Parent COMPLÈTE - Workflow CDC 6 étapes en 1 transaction + * Gère : Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU + */ + async inscrireParentComplet(dto: RegisterParentCompletDto) { + if (!dto.acceptation_cgu || !dto.acceptation_privacy) { + throw new BadRequestException('L\'acceptation des CGU et de la politique de confidentialité est obligatoire'); + } + + if (!dto.enfants || dto.enfants.length === 0) { + throw new BadRequestException('Au moins un enfant est requis'); + } + + const existe = await this.usersService.findByEmailOrNull(dto.email); + if (existe) { + throw new ConflictException('Un compte avec cet email existe déjà'); + } + + if (dto.co_parent_email) { + const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email); + if (coParentExiste) { + throw new ConflictException('L\'email du co-parent est déjà utilisé'); + } + } + + const joursExpirationToken = await this.appConfigService.get( + 'password_reset_token_expiry_days', + 7, + ); + + const tokenCreationMdp = crypto.randomUUID(); + const dateExpiration = new Date(); + dateExpiration.setDate(dateExpiration.getDate() + joursExpirationToken); + + const resultat = await this.usersRepo.manager.transaction(async (manager) => { + const parent1 = manager.create(Users, { + email: dto.email, + prenom: dto.prenom, + nom: dto.nom, + role: RoleType.PARENT, + statut: StatutUtilisateurType.EN_ATTENTE, + telephone: dto.telephone, + adresse: dto.adresse, + code_postal: dto.code_postal, + ville: dto.ville, + token_creation_mdp: tokenCreationMdp, + token_creation_mdp_expire_le: dateExpiration, + }); + + const parent1Enregistre = await manager.save(Users, parent1); + + let parent2Enregistre: Users | null = null; + let tokenCoParent: string | null = null; + + if (dto.co_parent_email && dto.co_parent_prenom && dto.co_parent_nom) { + tokenCoParent = crypto.randomUUID(); + const dateExpirationCoParent = new Date(); + dateExpirationCoParent.setDate(dateExpirationCoParent.getDate() + joursExpirationToken); + + const parent2 = manager.create(Users, { + email: dto.co_parent_email, + prenom: dto.co_parent_prenom, + nom: dto.co_parent_nom, + role: RoleType.PARENT, + statut: StatutUtilisateurType.EN_ATTENTE, + telephone: dto.co_parent_telephone, + adresse: dto.co_parent_meme_adresse ? dto.adresse : dto.co_parent_adresse, + code_postal: dto.co_parent_meme_adresse ? dto.code_postal : dto.co_parent_code_postal, + ville: dto.co_parent_meme_adresse ? dto.ville : dto.co_parent_ville, + token_creation_mdp: tokenCoParent, + token_creation_mdp_expire_le: dateExpirationCoParent, + }); + + parent2Enregistre = await manager.save(Users, parent2); + } + + const entiteParent = manager.create(Parents, { + user_id: parent1Enregistre.id, + }); + entiteParent.user = parent1Enregistre; + if (parent2Enregistre) { + entiteParent.co_parent = parent2Enregistre; + } + + await manager.save(Parents, entiteParent); + + if (parent2Enregistre) { + const entiteCoParent = manager.create(Parents, { + user_id: parent2Enregistre.id, + }); + entiteCoParent.user = parent2Enregistre; + entiteCoParent.co_parent = parent1Enregistre; + + await manager.save(Parents, entiteCoParent); + } + + const enfantsEnregistres: Children[] = []; + for (const enfantDto of dto.enfants) { + let urlPhoto: string | null = null; + + if (enfantDto.photo_base64 && enfantDto.photo_filename) { + urlPhoto = await this.sauvegarderPhotoDepuisBase64( + enfantDto.photo_base64, + enfantDto.photo_filename, + ); + } + + const enfant = new Children(); + enfant.first_name = enfantDto.prenom; + enfant.last_name = enfantDto.nom || dto.nom; + enfant.gender = enfantDto.genre; + enfant.birth_date = enfantDto.date_naissance ? new Date(enfantDto.date_naissance) : undefined; + enfant.due_date = enfantDto.date_previsionnelle_naissance + ? new Date(enfantDto.date_previsionnelle_naissance) + : undefined; + enfant.photo_url = urlPhoto || undefined; + enfant.status = enfantDto.date_naissance ? StatutEnfantType.ACTIF : StatutEnfantType.A_NAITRE; + enfant.consent_photo = false; + enfant.is_multiple = enfantDto.grossesse_multiple || false; + + const enfantEnregistre = await manager.save(Children, enfant); + enfantsEnregistres.push(enfantEnregistre); + + const lienParentEnfant1 = manager.create(ParentsChildren, { + parentId: parent1Enregistre.id, + enfantId: enfantEnregistre.id, + }); + await manager.save(ParentsChildren, lienParentEnfant1); + + if (parent2Enregistre) { + const lienParentEnfant2 = manager.create(ParentsChildren, { + parentId: parent2Enregistre.id, + enfantId: enfantEnregistre.id, + }); + await manager.save(ParentsChildren, lienParentEnfant2); + } + } + + return { + parent1: parent1Enregistre, + parent2: parent2Enregistre, + enfants: enfantsEnregistres, + tokenCreationMdp, + tokenCoParent, + }; + }); + + return { + message: 'Inscription réussie. Votre dossier est en attente de validation par un gestionnaire.', + parent_id: resultat.parent1.id, + co_parent_id: resultat.parent2?.id, + enfants_ids: resultat.enfants.map(e => e.id), + statut: StatutUtilisateurType.EN_ATTENTE, + }; + } + + /** + * Sauvegarde une photo depuis base64 vers le système de fichiers + */ + private async sauvegarderPhotoDepuisBase64(donneesBase64: string, nomFichier: string): Promise { + const correspondances = donneesBase64.match(/^data:image\/(\w+);base64,(.+)$/); + if (!correspondances) { + throw new BadRequestException('Format de photo invalide (doit être base64)'); + } + + const extension = correspondances[1]; + const tamponImage = Buffer.from(correspondances[2], 'base64'); + + const dossierUpload = '/app/uploads/photos'; + await fs.mkdir(dossierUpload, { recursive: true }); + + const nomFichierUnique = `${Date.now()}-${crypto.randomUUID()}.${extension}`; + const cheminFichier = path.join(dossierUpload, nomFichierUnique); + + await fs.writeFile(cheminFichier, tamponImage); + + return `/uploads/photos/${nomFichierUnique}`; + } + + /** + * Changement de mot de passe obligatoire (première connexion) + */ + async changePasswordRequired( + userId: string, + motDePasseActuel: string, + nouveauMotDePasse: string, + ) { + const user = await this.usersRepo.findOne({ where: { id: userId } }); + + if (!user) { + throw new UnauthorizedException('Utilisateur introuvable'); + } + + // Vérifier que le changement est bien obligatoire + if (!user.changement_mdp_obligatoire) { + throw new BadRequestException( + 'Le changement de mot de passe n\'est pas requis pour cet utilisateur', + ); + } + + // Vérifier que l'utilisateur a un mot de passe + if (!user.password) { + throw new BadRequestException('Compte non activé'); + } + + // Vérifier le mot de passe actuel + const motDePasseValide = await bcrypt.compare(motDePasseActuel, user.password); + if (!motDePasseValide) { + throw new BadRequestException('Mot de passe actuel incorrect'); + } + + // Vérifier que le nouveau mot de passe est différent de l'ancien + const memeMotDePasse = await bcrypt.compare(nouveauMotDePasse, user.password); + if (memeMotDePasse) { + throw new BadRequestException( + 'Le nouveau mot de passe doit être différent de l\'ancien', + ); + } + + // Hasher et sauvegarder le nouveau mot de passe + const sel = await bcrypt.genSalt(12); + user.password = await bcrypt.hash(nouveauMotDePasse, sel); + user.changement_mdp_obligatoire = false; + user.modifie_le = new Date(); + + await this.usersRepo.save(user); + + return { + success: true, + message: 'Mot de passe changé avec succès', + }; + } + + async logout(userId: string) { + return { success: true, message: 'Deconnexion'} + } +} diff --git a/backend/src/routes/auth/dto/change-password.dto.ts b/backend/src/routes/auth/dto/change-password.dto.ts new file mode 100644 index 0000000..a3e692c --- /dev/null +++ b/backend/src/routes/auth/dto/change-password.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, MinLength, Matches } from 'class-validator'; + +export class ChangePasswordRequiredDto { + @ApiProperty({ description: 'Mot de passe actuel' }) + @IsString() + mot_de_passe_actuel: string; + + @ApiProperty({ + description: 'Nouveau mot de passe (min 8 caractères, 1 majuscule, 1 chiffre)', + minLength: 8 + }) + @IsString() + @MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caractères' }) + @Matches(/^(?=.*[A-Z])(?=.*\d)/, { + message: 'Le mot de passe doit contenir au moins une majuscule et un chiffre' + }) + nouveau_mot_de_passe: string; + + @ApiProperty({ description: 'Confirmation du nouveau mot de passe' }) + @IsString() + confirmation_mot_de_passe: string; +} diff --git a/backend/src/routes/auth/dto/enfant-inscription.dto.ts b/backend/src/routes/auth/dto/enfant-inscription.dto.ts new file mode 100644 index 0000000..9ee120c --- /dev/null +++ b/backend/src/routes/auth/dto/enfant-inscription.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsString, + IsNotEmpty, + IsOptional, + IsEnum, + IsDateString, + IsBoolean, + MinLength, + MaxLength, +} from 'class-validator'; +import { GenreType } from 'src/entities/children.entity'; + +export class EnfantInscriptionDto { + @ApiProperty({ example: 'Emma', required: false, description: 'Prénom de l\'enfant (obligatoire si déjà né)' }) + @IsOptional() + @IsString() + @MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' }) + @MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' }) + prenom?: string; + + @ApiProperty({ example: 'MARTIN', required: false, description: 'Nom de l\'enfant (hérité des parents si non fourni)' }) + @IsOptional() + @IsString() + @MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' }) + @MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' }) + nom?: string; + + @ApiProperty({ example: '2023-02-15', required: false, description: 'Date de naissance (si enfant déjà né)' }) + @IsOptional() + @IsDateString() + date_naissance?: string; + + @ApiProperty({ example: '2025-06-15', required: false, description: 'Date prévisionnelle de naissance (si enfant à naître)' }) + @IsOptional() + @IsDateString() + date_previsionnelle_naissance?: string; + + @ApiProperty({ enum: GenreType, example: GenreType.F }) + @IsEnum(GenreType, { message: 'Le genre doit être H, F ou Autre' }) + @IsNotEmpty({ message: 'Le genre est requis' }) + genre: GenreType; + + @ApiProperty({ + example: 'data:image/jpeg;base64,/9j/4AAQSkZJRg...', + required: false, + description: 'Photo de l\'enfant en base64 (obligatoire si déjà né)' + }) + @IsOptional() + @IsString() + photo_base64?: string; + + @ApiProperty({ example: 'emma_martin.jpg', required: false, description: 'Nom du fichier photo' }) + @IsOptional() + @IsString() + photo_filename?: string; + + @ApiProperty({ example: false, required: false, description: 'Grossesse multiple (jumeaux, triplés, etc.)' }) + @IsOptional() + @IsBoolean() + grossesse_multiple?: boolean; +} + diff --git a/backend/src/routes/auth/dto/login.dto.ts b/backend/src/routes/auth/dto/login.dto.ts new file mode 100644 index 0000000..f292798 --- /dev/null +++ b/backend/src/routes/auth/dto/login.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsString, MaxLength, MinLength } from "class-validator"; + +export class LoginDto { + @ApiProperty({ example: 'mon.utilisateur@exemple.com', description: "Adresse email de l'utililisateur" }) + @IsEmail() + email: string; + + @ApiProperty({ + example: "Mon_motdepasse_fort_1234?", + description: "Mot de passe de l'utilisateur" + }) + @IsString({ message: 'Le mot de passe doit etre une chaine de caracteres' }) + //@MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caracteres' }) + @MaxLength(50) + password: string; +} \ No newline at end of file diff --git a/backend/src/routes/auth/dto/profile_response.dto.ts b/backend/src/routes/auth/dto/profile_response.dto.ts new file mode 100644 index 0000000..1e2b32d --- /dev/null +++ b/backend/src/routes/auth/dto/profile_response.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity'; + +export class ProfileResponseDto { + @ApiProperty() + id: string; + + @ApiProperty() + email: string; + + @ApiProperty({ enum: RoleType }) + role: RoleType; + + @ApiProperty() + prenom?: string; + + @ApiProperty() + nom?: string; + + @ApiProperty({ enum: StatutUtilisateurType }) + statut: StatutUtilisateurType; + + @ApiProperty({ description: 'Indique si le changement de mot de passe est obligatoire à la première connexion' }) + changement_mdp_obligatoire: boolean; +} diff --git a/backend/src/routes/auth/dto/refresh_token.dto.ts b/backend/src/routes/auth/dto/refresh_token.dto.ts new file mode 100644 index 0000000..c328692 --- /dev/null +++ b/backend/src/routes/auth/dto/refresh_token.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +export class RefreshTokenDto { + + @ApiProperty({ + description: 'Token de rafraîchissement', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + }) + @IsString() + refresh_token: string; +} \ No newline at end of file diff --git a/backend/src/routes/auth/dto/register-parent-complet.dto.ts b/backend/src/routes/auth/dto/register-parent-complet.dto.ts new file mode 100644 index 0000000..7d1c36a --- /dev/null +++ b/backend/src/routes/auth/dto/register-parent-complet.dto.ts @@ -0,0 +1,166 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEmail, + IsNotEmpty, + IsOptional, + IsString, + IsDateString, + IsEnum, + IsBoolean, + IsArray, + ValidateNested, + MinLength, + MaxLength, + Matches, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { SituationFamilialeType } from 'src/entities/users.entity'; +import { EnfantInscriptionDto } from './enfant-inscription.dto'; + +export class RegisterParentCompletDto { + // ============================================ + // ÉTAPE 1 : PARENT 1 (Obligatoire) + // ============================================ + + @ApiProperty({ example: 'claire.martin@ptits-pas.fr' }) + @IsEmail({}, { message: 'Email invalide' }) + @IsNotEmpty({ message: 'L\'email est requis' }) + email: string; + + @ApiProperty({ example: 'Claire' }) + @IsString() + @IsNotEmpty({ message: 'Le prénom est requis' }) + @MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' }) + @MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' }) + prenom: string; + + @ApiProperty({ example: 'MARTIN' }) + @IsString() + @IsNotEmpty({ message: 'Le nom est requis' }) + @MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' }) + @MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' }) + nom: string; + + @ApiProperty({ example: '0689567890' }) + @IsString() + @IsNotEmpty({ message: 'Le téléphone est requis' }) + @Matches(/^(\+33|0)[1-9](\d{2}){4}$/, { + message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)', + }) + telephone: string; + + @ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false }) + @IsOptional() + @IsString() + adresse?: string; + + @ApiProperty({ example: '95870', required: false }) + @IsOptional() + @IsString() + @MaxLength(10) + code_postal?: string; + + @ApiProperty({ example: 'Bezons', required: false }) + @IsOptional() + @IsString() + @MaxLength(150) + ville?: string; + + // ============================================ + // ÉTAPE 2 : PARENT 2 / CO-PARENT (Optionnel) + // ============================================ + + @ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false }) + @IsOptional() + @IsEmail({}, { message: 'Email du co-parent invalide' }) + co_parent_email?: string; + + @ApiProperty({ example: 'Thomas', required: false }) + @IsOptional() + @IsString() + co_parent_prenom?: string; + + @ApiProperty({ example: 'MARTIN', required: false }) + @IsOptional() + @IsString() + co_parent_nom?: string; + + @ApiProperty({ example: '0678456789', required: false }) + @IsOptional() + @IsString() + @Matches(/^(\+33|0)[1-9](\d{2}){4}$/, { + message: 'Le numéro de téléphone du co-parent doit être valide', + }) + co_parent_telephone?: string; + + @ApiProperty({ example: true, description: 'Le co-parent habite à la même adresse', required: false }) + @IsOptional() + @IsBoolean() + co_parent_meme_adresse?: boolean; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + co_parent_adresse?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + co_parent_code_postal?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + co_parent_ville?: string; + + // ============================================ + // ÉTAPE 3 : ENFANT(S) (Au moins 1 requis) + // ============================================ + + @ApiProperty({ + type: [EnfantInscriptionDto], + description: 'Liste des enfants (au moins 1 requis)', + example: [{ + prenom: 'Emma', + nom: 'MARTIN', + date_naissance: '2023-02-15', + genre: 'F', + photo_base64: 'data:image/jpeg;base64,...', + photo_filename: 'emma_martin.jpg' + }] + }) + @IsArray({ message: 'La liste des enfants doit être un tableau' }) + @IsNotEmpty({ message: 'Au moins un enfant est requis' }) + @ValidateNested({ each: true }) + @Type(() => EnfantInscriptionDto) + enfants: EnfantInscriptionDto[]; + + // ============================================ + // ÉTAPE 4 : PRÉSENTATION DU DOSSIER (Optionnel) + // ============================================ + + @ApiProperty({ + example: 'Nous recherchons une assistante maternelle bienveillante pour nos triplés...', + required: false, + description: 'Présentation du dossier (max 2000 caractères)' + }) + @IsOptional() + @IsString() + @MaxLength(2000, { message: 'La présentation ne peut pas dépasser 2000 caractères' }) + presentation_dossier?: string; + + // ============================================ + // ÉTAPE 5 : ACCEPTATION CGU (Obligatoire) + // ============================================ + + @ApiProperty({ example: true, description: 'Acceptation des Conditions Générales d\'Utilisation' }) + @IsBoolean() + @IsNotEmpty({ message: 'L\'acceptation des CGU est requise' }) + acceptation_cgu: boolean; + + @ApiProperty({ example: true, description: 'Acceptation de la Politique de confidentialité' }) + @IsBoolean() + @IsNotEmpty({ message: 'L\'acceptation de la politique de confidentialité est requise' }) + acceptation_privacy: boolean; +} + diff --git a/backend/src/routes/auth/dto/register-parent.dto.ts b/backend/src/routes/auth/dto/register-parent.dto.ts new file mode 100644 index 0000000..a022724 --- /dev/null +++ b/backend/src/routes/auth/dto/register-parent.dto.ts @@ -0,0 +1,105 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEmail, + IsNotEmpty, + IsOptional, + IsString, + IsDateString, + IsEnum, + MinLength, + MaxLength, + Matches, +} from 'class-validator'; +import { SituationFamilialeType } from 'src/entities/users.entity'; + +export class RegisterParentDto { + // === Informations obligatoires === + @ApiProperty({ example: 'claire.martin@ptits-pas.fr' }) + @IsEmail({}, { message: 'Email invalide' }) + @IsNotEmpty({ message: 'L\'email est requis' }) + email: string; + + @ApiProperty({ example: 'Claire' }) + @IsString() + @IsNotEmpty({ message: 'Le prénom est requis' }) + @MinLength(2, { message: 'Le prénom doit contenir au moins 2 caractères' }) + @MaxLength(100, { message: 'Le prénom ne peut pas dépasser 100 caractères' }) + prenom: string; + + @ApiProperty({ example: 'MARTIN' }) + @IsString() + @IsNotEmpty({ message: 'Le nom est requis' }) + @MinLength(2, { message: 'Le nom doit contenir au moins 2 caractères' }) + @MaxLength(100, { message: 'Le nom ne peut pas dépasser 100 caractères' }) + nom: string; + + @ApiProperty({ example: '0689567890' }) + @IsString() + @IsNotEmpty({ message: 'Le téléphone est requis' }) + @Matches(/^(\+33|0)[1-9](\d{2}){4}$/, { + message: 'Le numéro de téléphone doit être valide (ex: 0689567890 ou +33689567890)', + }) + telephone: string; + + // === Informations optionnelles === + @ApiProperty({ example: '5 Avenue du Général de Gaulle', required: false }) + @IsOptional() + @IsString() + adresse?: string; + + @ApiProperty({ example: '95870', required: false }) + @IsOptional() + @IsString() + @MaxLength(10) + code_postal?: string; + + @ApiProperty({ example: 'Bezons', required: false }) + @IsOptional() + @IsString() + @MaxLength(150) + ville?: string; + + // === Informations co-parent (optionnel) === + @ApiProperty({ example: 'thomas.martin@ptits-pas.fr', required: false }) + @IsOptional() + @IsEmail({}, { message: 'Email du co-parent invalide' }) + co_parent_email?: string; + + @ApiProperty({ example: 'Thomas', required: false }) + @IsOptional() + @IsString() + co_parent_prenom?: string; + + @ApiProperty({ example: 'MARTIN', required: false }) + @IsOptional() + @IsString() + co_parent_nom?: string; + + @ApiProperty({ example: '0612345678', required: false }) + @IsOptional() + @IsString() + @Matches(/^(\+33|0)[1-9](\d{2}){4}$/, { + message: 'Le numéro de téléphone du co-parent doit être valide', + }) + co_parent_telephone?: string; + + @ApiProperty({ example: 'true', description: 'Le co-parent habite à la même adresse', required: false }) + @IsOptional() + co_parent_meme_adresse?: boolean; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + co_parent_adresse?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + co_parent_code_postal?: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + co_parent_ville?: string; +} + diff --git a/backend/src/routes/auth/dto/register.dto.ts b/backend/src/routes/auth/dto/register.dto.ts new file mode 100644 index 0000000..0d38d91 --- /dev/null +++ b/backend/src/routes/auth/dto/register.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty, OmitType } from '@nestjs/swagger'; +import { IsEnum, IsOptional } from 'class-validator'; +import { CreateUserDto } from '../../user/dto/create_user.dto'; +import { RoleType, StatutUtilisateurType } from 'src/entities/users.entity'; + +export class RegisterDto extends OmitType(CreateUserDto, ['changement_mdp_obligatoire'] as const) { + @ApiProperty({ enum: [RoleType.ASSISTANTE_MATERNELLE, RoleType.PARENT], default: RoleType.PARENT }) + @IsEnum(RoleType) + role: RoleType = RoleType.PARENT; + + @IsEnum(StatutUtilisateurType) + @IsOptional() + statut?: StatutUtilisateurType = StatutUtilisateurType.EN_ATTENTE; +} diff --git a/backend/src/routes/dossiers/dossiers.module.ts b/backend/src/routes/dossiers/dossiers.module.ts new file mode 100644 index 0000000..66026d2 --- /dev/null +++ b/backend/src/routes/dossiers/dossiers.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class DossiersModule {} diff --git a/backend/src/routes/enfants/dto/create_enfants.dto.ts b/backend/src/routes/enfants/dto/create_enfants.dto.ts new file mode 100644 index 0000000..3ede09d --- /dev/null +++ b/backend/src/routes/enfants/dto/create_enfants.dto.ts @@ -0,0 +1,66 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsBoolean, + IsDateString, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + MaxLength, + ValidateIf, +} from 'class-validator'; +import { GenreType, StatutEnfantType } from 'src/entities/children.entity'; + +export class CreateEnfantsDto { + @ApiProperty({ enum: StatutEnfantType, example: StatutEnfantType.ACTIF }) + @IsEnum(StatutEnfantType) + @IsNotEmpty() + status: StatutEnfantType; + + @ApiProperty({ example: 'Georges', required: false }) + @IsOptional() + @IsString() + @MaxLength(100) + first_name?: string; + + @ApiProperty({ example: 'Dupont', required: false }) + @IsOptional() + @IsString() + @MaxLength(100) + last_name?: string; + + @ApiProperty({ enum: GenreType }) + @IsEnum(GenreType) + @IsNotEmpty() + gender: GenreType; + + @ApiProperty({ example: '2018-06-24', required: false }) + @ValidateIf(o => o.status !== StatutEnfantType.A_NAITRE) + @IsOptional() + @IsDateString() + birth_date?: string; + + @ApiProperty({ example: '2025-12-15', required: false }) + @ValidateIf(o => o.status === StatutEnfantType.A_NAITRE) + @IsOptional() + @IsDateString() + due_date?: string; + + @ApiProperty({ example: 'https://monimage.com/photo.jpg', required: false }) + @IsOptional() + @IsString() + photo_url?: string; + + @ApiProperty({ default: false }) + @IsBoolean() + consent_photo: boolean; + + @ApiProperty({ required: false }) + @IsOptional() + @IsDateString() + consent_photo_at?: string; + + @ApiProperty({ default: false }) + @IsBoolean() + is_multiple: boolean; +} diff --git a/backend/src/routes/enfants/dto/enfants_response.dto.ts b/backend/src/routes/enfants/dto/enfants_response.dto.ts new file mode 100644 index 0000000..41f0b1e --- /dev/null +++ b/backend/src/routes/enfants/dto/enfants_response.dto.ts @@ -0,0 +1,37 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { GenreType, StatutEnfantType } from 'src/entities/children.entity'; + +export class EnfantResponseDto { + @ApiProperty({ example: 'UUID-enfant' }) + id: string; + + @ApiProperty({ enum: StatutEnfantType }) + status: StatutEnfantType; + + @ApiProperty({ example: 'Georges', required: false }) + first_name?: string; + + @ApiProperty({ example: 'Dupont', required: false }) + last_name?: string; + + @ApiProperty({ enum: GenreType, required: false }) + gender?: GenreType; + + @ApiProperty({ example: '2018-06-24', required: false }) + birth_date?: string; + + @ApiProperty({ example: '2025-12-15', required: false }) + due_date?: string; + + @ApiProperty({ example: 'https://monimage.com/photo.jpg', required: false }) + photo_url?: string; + + @ApiProperty({ example: false }) + consent_photo: boolean; + + @ApiProperty({ example: false }) + is_multiple: boolean; + + @ApiProperty({ example: 'UUID-parent' }) + parent_id: string; +} diff --git a/backend/src/routes/enfants/dto/update_enfants.dto.ts b/backend/src/routes/enfants/dto/update_enfants.dto.ts new file mode 100644 index 0000000..bcaea31 --- /dev/null +++ b/backend/src/routes/enfants/dto/update_enfants.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateEnfantsDto } from './create_enfants.dto'; + +export class UpdateEnfantsDto extends PartialType(CreateEnfantsDto) {} diff --git a/backend/src/routes/enfants/enfants.controller.spec.ts b/backend/src/routes/enfants/enfants.controller.spec.ts new file mode 100644 index 0000000..04e9e0e --- /dev/null +++ b/backend/src/routes/enfants/enfants.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EnfantsController } from './enfants.controller'; + +describe('EnfantsController', () => { + let controller: EnfantsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [EnfantsController], + }).compile(); + + controller = module.get(EnfantsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/routes/enfants/enfants.controller.ts b/backend/src/routes/enfants/enfants.controller.ts new file mode 100644 index 0000000..e112972 --- /dev/null +++ b/backend/src/routes/enfants/enfants.controller.ts @@ -0,0 +1,101 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseUUIDPipe, + Patch, + Post, + UseGuards, + UseInterceptors, + UploadedFile, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, ApiTags, ApiConsumes } from '@nestjs/swagger'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import { EnfantsService } from './enfants.service'; +import { CreateEnfantsDto } from './dto/create_enfants.dto'; +import { UpdateEnfantsDto } from './dto/update_enfants.dto'; +import { RoleType, Users } from 'src/entities/users.entity'; +import { User } from 'src/common/decorators/user.decorator'; +import { AuthGuard } from 'src/common/guards/auth.guard'; +import { Roles } from 'src/common/decorators/roles.decorator'; +import { RolesGuard } from 'src/common/guards/roles.guard'; + +@ApiBearerAuth('access-token') +@ApiTags('Enfants') +@UseGuards(AuthGuard, RolesGuard) +@Controller('enfants') +export class EnfantsController { + constructor(private readonly enfantsService: EnfantsService) { } + + @Roles(RoleType.PARENT) + @Post() + @ApiConsumes('multipart/form-data') + @UseInterceptors( + FileInterceptor('photo', { + storage: diskStorage({ + destination: './uploads/photos', + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); + const ext = extname(file.originalname); + cb(null, `enfant-${uniqueSuffix}${ext}`); + }, + }), + fileFilter: (req, file, cb) => { + if (!file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) { + return cb(new Error('Seules les images sont autorisées'), false); + } + cb(null, true); + }, + limits: { + fileSize: 5 * 1024 * 1024, + }, + }), + ) + create( + @Body() dto: CreateEnfantsDto, + @UploadedFile() photo: Express.Multer.File, + @User() currentUser: Users, + ) { + return this.enfantsService.create(dto, currentUser, photo); + } + + @Roles(RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN) + @Get() + findAll() { + return this.enfantsService.findAll(); + } + + @Roles( + RoleType.PARENT, + RoleType.ADMINISTRATEUR, + RoleType.SUPER_ADMIN, + RoleType.GESTIONNAIRE + ) + @Get(':id') + findOne( + @Param('id', new ParseUUIDPipe()) id: string, + @User() currentUser: Users + ) { + return this.enfantsService.findOne(id, currentUser); + } + + @Roles(RoleType.ADMINISTRATEUR, RoleType.SUPER_ADMIN, RoleType.PARENT) + @Patch(':id') + update( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() dto: UpdateEnfantsDto, + @User() currentUser: Users, + ) { + return this.enfantsService.update(id, dto, currentUser); + } + + @Roles(RoleType.SUPER_ADMIN) + @Delete(':id') + remove(@Param('id', new ParseUUIDPipe()) id: string) { + return this.enfantsService.remove(id); + } +} diff --git a/backend/src/routes/enfants/enfants.module.ts b/backend/src/routes/enfants/enfants.module.ts new file mode 100644 index 0000000..2b9385b --- /dev/null +++ b/backend/src/routes/enfants/enfants.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { EnfantsController } from './enfants.controller'; +import { EnfantsService } from './enfants.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Children } from 'src/entities/children.entity'; +import { Parents } from 'src/entities/parents.entity'; +import { ParentsChildren } from 'src/entities/parents_children.entity'; +import { AuthModule } from '../auth/auth.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([Children, Parents, ParentsChildren]), + AuthModule + + ], + controllers: [EnfantsController], + providers: [EnfantsService] +}) +export class EnfantsModule { } diff --git a/backend/src/routes/enfants/enfants.service.spec.ts b/backend/src/routes/enfants/enfants.service.spec.ts new file mode 100644 index 0000000..c1ded59 --- /dev/null +++ b/backend/src/routes/enfants/enfants.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EnfantsService } from './enfants.service'; + +describe('EnfantsService', () => { + let service: EnfantsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [EnfantsService], + }).compile(); + + service = module.get(EnfantsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/routes/enfants/enfants.service.ts b/backend/src/routes/enfants/enfants.service.ts new file mode 100644 index 0000000..bfefedb --- /dev/null +++ b/backend/src/routes/enfants/enfants.service.ts @@ -0,0 +1,131 @@ +import { + BadRequestException, + ConflictException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Children, StatutEnfantType } from 'src/entities/children.entity'; +import { Parents } from 'src/entities/parents.entity'; +import { ParentsChildren } from 'src/entities/parents_children.entity'; +import { RoleType, Users } from 'src/entities/users.entity'; +import { CreateEnfantsDto } from './dto/create_enfants.dto'; + +@Injectable() +export class EnfantsService { + constructor( + @InjectRepository(Children) + private readonly childrenRepository: Repository, + @InjectRepository(Parents) + private readonly parentsRepository: Repository, + @InjectRepository(ParentsChildren) + private readonly parentsChildrenRepository: Repository, + ) { } + + // Création d'un enfant + async create(dto: CreateEnfantsDto, currentUser: Users, photoFile?: Express.Multer.File): Promise { + const parent = await this.parentsRepository.findOne({ + where: { user_id: currentUser.id }, + relations: ['co_parent'], + }); + if (!parent) throw new NotFoundException('Parent introuvable'); + + // Vérif métier simple + if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) { + throw new BadRequestException('Un enfant actif doit avoir une date de naissance'); + } + + // Vérif doublon éventuel (ex: même prénom + date de naissance pour ce parent) + const exist = await this.childrenRepository.findOne({ + where: { + first_name: dto.first_name, + last_name: dto.last_name, + birth_date: dto.birth_date ? new Date(dto.birth_date) : undefined, + }, + }); + if (exist) throw new ConflictException('Cet enfant existe déjà'); + + // Gestion de la photo uploadée + if (photoFile) { + dto.photo_url = `/uploads/photos/${photoFile.filename}`; + if (dto.consent_photo) { + dto.consent_photo_at = new Date().toISOString(); + } + } + + // Création + const child = this.childrenRepository.create(dto); + await this.childrenRepository.save(child); + + // Lien parent-enfant (Parent 1) + const parentLink = this.parentsChildrenRepository.create({ + parentId: parent.user_id, + enfantId: child.id, + }); + await this.parentsChildrenRepository.save(parentLink); + + // Rattachement automatique au co-parent s'il existe + if (parent.co_parent) { + const coParentLink = this.parentsChildrenRepository.create({ + parentId: parent.co_parent.id, + enfantId: child.id, + }); + await this.parentsChildrenRepository.save(coParentLink); + } + + return this.findOne(child.id, currentUser); + } + + // Liste des enfants + async findAll(): Promise { + return this.childrenRepository.find({ + relations: ['parentLinks'], + order: { last_name: 'ASC', first_name: 'ASC' }, + }); + } + + // Récupérer un enfant par id + async findOne(id: string, currentUser: Users): Promise { + const child = await this.childrenRepository.findOne({ + where: { id }, + relations: ['parentLinks'], + }); + if (!child) throw new NotFoundException('Enfant introuvable'); + + switch (currentUser.role) { + case RoleType.PARENT: + if (!child.parentLinks.some(link => link.parentId === currentUser.id)) { + throw new ForbiddenException('Cet enfant ne vous appartient pas'); + } + break; + + case RoleType.ADMINISTRATEUR: + case RoleType.SUPER_ADMIN: + case RoleType.GESTIONNAIRE: + // accès complet + break; + + default: + throw new ForbiddenException('Accès interdit'); + } + + return child; + } + + + // Mise à jour + async update(id: string, dto: Partial, currentUser: Users): Promise { + const child = await this.childrenRepository.findOne({ where: { id } }); + if (!child) throw new NotFoundException('Enfant introuvable'); + + await this.childrenRepository.update(id, dto); + return this.findOne(id, currentUser); + } + + // Suppression + async remove(id: string): Promise { + await this.childrenRepository.delete(id); + } +} diff --git a/backend/src/routes/parents/parents.controller.spec.ts b/backend/src/routes/parents/parents.controller.spec.ts new file mode 100644 index 0000000..6735376 --- /dev/null +++ b/backend/src/routes/parents/parents.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ParentsController } from './parents.controller'; + +describe('ParentsController', () => { + let controller: ParentsController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ParentsController], + }).compile(); + + controller = module.get(ParentsController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/routes/parents/parents.controller.ts b/backend/src/routes/parents/parents.controller.ts new file mode 100644 index 0000000..aa02313 --- /dev/null +++ b/backend/src/routes/parents/parents.controller.ts @@ -0,0 +1,58 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, +} from '@nestjs/common'; +import { ParentsService } from './parents.service'; +import { Parents } from 'src/entities/parents.entity'; +import { Roles } from 'src/common/decorators/roles.decorator'; +import { RoleType } from 'src/entities/users.entity'; +import { ApiBody, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CreateParentDto } from '../user/dto/create_parent.dto'; +import { UpdateParentsDto } from '../user/dto/update_parent.dto'; + +@ApiTags('Parents') +@Controller('parents') +export class ParentsController { + constructor(private readonly parentsService: ParentsService) {} + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Get() + @ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' }) + @ApiResponse({ status: 403, description: 'Accès refusé !' }) + getAll(): Promise { + return this.parentsService.findAll(); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Get(':id') + @ApiResponse({ status: 200, type: Parents, description: 'Détails du parent par ID utilisateur' }) + @ApiResponse({ status: 404, description: 'Parent non trouvé' }) + @ApiResponse({ status: 403, description: 'Accès refusé !' }) + getOne(@Param('id') user_id: string): Promise { + return this.parentsService.findOne(user_id); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Post() + @ApiBody({ type: CreateParentDto }) + @ApiResponse({ status: 201, type: Parents, description: 'Parent créé avec succès' }) + @ApiResponse({ status: 403, description: 'Accès refusé !' }) + create(@Body() dto: CreateParentDto): Promise { + return this.parentsService.create(dto); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @Patch(':id') + @ApiBody({ type: UpdateParentsDto }) + @ApiResponse({ status: 200, type: Parents, description: 'Parent mis à jour avec succès' }) + @ApiResponse({ status: 404, description: 'Parent introuvable' }) + @ApiResponse({ status: 403, description: 'Accès refusé !' }) + update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise { + return this.parentsService.update(id, dto); + } +} diff --git a/backend/src/routes/parents/parents.module.ts b/backend/src/routes/parents/parents.module.ts new file mode 100644 index 0000000..dc57fe6 --- /dev/null +++ b/backend/src/routes/parents/parents.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Parents } from 'src/entities/parents.entity'; +import { ParentsController } from './parents.controller'; +import { ParentsService } from './parents.service'; +import { Users } from 'src/entities/users.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Parents, Users])], + controllers: [ParentsController], + providers: [ParentsService], + exports: [ParentsService, + TypeOrmModule, + ], +}) +export class ParentsModule { } \ No newline at end of file diff --git a/backend/src/routes/parents/parents.service.spec.ts b/backend/src/routes/parents/parents.service.spec.ts new file mode 100644 index 0000000..83b23f2 --- /dev/null +++ b/backend/src/routes/parents/parents.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ParentsService } from './parents.service'; + +describe('ParentsService', () => { + let service: ParentsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ParentsService], + }).compile(); + + service = module.get(ParentsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/routes/parents/parents.service.ts b/backend/src/routes/parents/parents.service.ts new file mode 100644 index 0000000..d2cafee --- /dev/null +++ b/backend/src/routes/parents/parents.service.ts @@ -0,0 +1,74 @@ +import { + BadRequestException, + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Parents } from 'src/entities/parents.entity'; +import { RoleType, Users } from 'src/entities/users.entity'; +import { CreateParentDto } from '../user/dto/create_parent.dto'; +import { UpdateParentsDto } from '../user/dto/update_parent.dto'; + +@Injectable() +export class ParentsService { + constructor( + @InjectRepository(Parents) + private readonly parentsRepository: Repository, + @InjectRepository(Users) + private readonly usersRepository: Repository, + ) {} + + // Création d’un parent + async create(dto: CreateParentDto): Promise { + const user = await this.usersRepository.findOneBy({ id: dto.user_id }); + if (!user) throw new NotFoundException('Utilisateur introuvable'); + if (user.role !== RoleType.PARENT) { + throw new BadRequestException('Accès réservé aux parents'); + } + + const exist = await this.parentsRepository.findOneBy({ user_id: dto.user_id }); + if (exist) throw new ConflictException('Ce parent existe déjà'); + + let co_parent: Users | null = null; + if (dto.co_parent_id) { + co_parent = await this.usersRepository.findOneBy({ id: dto.co_parent_id }); + if (!co_parent) throw new NotFoundException('Co-parent introuvable'); + if (co_parent.role !== RoleType.PARENT) { + throw new BadRequestException('Accès réservé aux parents'); + } + } + + const entity = this.parentsRepository.create({ + user_id: dto.user_id, + user, + co_parent: co_parent ?? undefined, + }); + + return this.parentsRepository.save(entity); + } + + // Liste des parents + async findAll(): Promise { + return this.parentsRepository.find({ + relations: ['user', 'co_parent', 'parentChildren', 'dossiers'], + }); + } + + // Récupérer un parent par user_id + async findOne(user_id: string): Promise { + const parent = await this.parentsRepository.findOne({ + where: { user_id }, + relations: ['user', 'co_parent', 'parentChildren', 'dossiers'], + }); + if (!parent) throw new NotFoundException('Parent introuvable'); + return parent; + } + + // Mise à jour + async update(id: string, dto: UpdateParentsDto): Promise { + await this.parentsRepository.update(id, dto); + return this.findOne(id); + } +} diff --git a/backend/src/routes/theme.routes.ts b/backend/src/routes/theme.routes.ts deleted file mode 100644 index e43e388..0000000 --- a/backend/src/routes/theme.routes.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Router } from 'express'; -import { ThemeController } from '../controllers/theme.controller'; - -const router = Router(); - -// Routes pour les thèmes -router.post('/', ThemeController.createTheme); -router.get('/', ThemeController.getAllThemes); -router.get('/active', ThemeController.getActiveTheme); -router.put('/:themeId/activate', ThemeController.activateTheme); -router.put('/:themeId', ThemeController.updateTheme); -router.delete('/:themeId', ThemeController.deleteTheme); - -export default router; \ No newline at end of file diff --git a/backend/src/routes/user/dto/create_admin.dto.ts b/backend/src/routes/user/dto/create_admin.dto.ts new file mode 100644 index 0000000..f35f781 --- /dev/null +++ b/backend/src/routes/user/dto/create_admin.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from "@nestjs/swagger"; +import { CreateUserDto } from "./create_user.dto"; + +export class CreateAdminDto extends OmitType(CreateUserDto, ['role'] as const) {} diff --git a/backend/src/routes/user/dto/create_assistante.dto.ts b/backend/src/routes/user/dto/create_assistante.dto.ts new file mode 100644 index 0000000..1e05809 --- /dev/null +++ b/backend/src/routes/user/dto/create_assistante.dto.ts @@ -0,0 +1,56 @@ +import { OmitType } from "@nestjs/swagger"; +import { IsBoolean, IsDateString, IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Length, Matches, Max, Min } from "class-validator"; +import { CreateUserDto } from "./create_user.dto"; + +export class CreateAssistanteDto extends OmitType(CreateUserDto, ['role', 'photo_url', 'consentement_photo'] as const) { + @IsUUID() + @IsNotEmpty() + user_id?: string; + + @IsString() + @IsNotEmpty() + @Length(1, 50) + approval_number: string; + + @Matches(/^\d{15}$/) + @IsNotEmpty() + nir: string; + + @IsInt() + @Min(1) + @Max(10) + @IsNotEmpty() + max_children: number; + + @IsString() + @IsNotEmpty() + photo_url: string; + + @IsBoolean() + @IsNotEmpty() + consentement_photo: boolean; + + @IsDateString() + @IsNotEmpty() + agreement_date: string; + + @IsString() + @IsNotEmpty() + @Length(1, 100) + residence_city: string; + + @IsOptional() + biography?: string; + + @IsOptional() + available?: boolean; + + @IsOptional() + years_experience?: number; + + @IsOptional() + specialty?: string; + + @IsOptional() + places_available?: number; +} diff --git a/backend/src/routes/user/dto/create_gestionnaire.dto.ts b/backend/src/routes/user/dto/create_gestionnaire.dto.ts new file mode 100644 index 0000000..fceea23 --- /dev/null +++ b/backend/src/routes/user/dto/create_gestionnaire.dto.ts @@ -0,0 +1,4 @@ +import { OmitType } from "@nestjs/swagger"; +import { CreateUserDto } from "./create_user.dto"; + +export class CreateGestionnaireDto extends OmitType(CreateUserDto, ['role'] as const) {} diff --git a/backend/src/routes/user/dto/create_parent.dto.ts b/backend/src/routes/user/dto/create_parent.dto.ts new file mode 100644 index 0000000..071f0a4 --- /dev/null +++ b/backend/src/routes/user/dto/create_parent.dto.ts @@ -0,0 +1,17 @@ +import { OmitType } from "@nestjs/swagger"; +import { CreateUserDto } from "./create_user.dto"; +import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator"; + +export class CreateParentDto extends OmitType(CreateUserDto, ['role', 'photo_url'] as const) { + @IsUUID() + @IsNotEmpty() + user_id: string; + + @IsOptional() + @IsUUID() + co_parent_id?: string; + + @IsString() + @IsNotEmpty() + photo_url: string; +} \ No newline at end of file diff --git a/backend/src/routes/user/dto/create_user.dto.ts b/backend/src/routes/user/dto/create_user.dto.ts new file mode 100644 index 0000000..cae620d --- /dev/null +++ b/backend/src/routes/user/dto/create_user.dto.ts @@ -0,0 +1,105 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsBoolean, + IsDateString, + IsEmail, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, + MinLength, + MaxLength, +} from 'class-validator'; +import { RoleType, GenreType, StatutUtilisateurType, SituationFamilialeType } from 'src/entities/users.entity'; + +export class CreateUserDto { + @ApiProperty({ example: 'sosso.test@example.com' }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ minLength: 6, example: 'Mon_motdepasse_fort_1234?' }) + @IsString() + @IsNotEmpty() + @MinLength(6) + password: string; + + @ApiProperty({ example: 'Julien' }) + @IsString() + @IsNotEmpty() + @MaxLength(100) + prenom: string; + + @ApiProperty({ example: 'Dupont' }) + @IsString() + @IsNotEmpty() + @MaxLength(100) + nom: string; + + @ApiProperty({ enum: GenreType, required: false, default: GenreType.AUTRE }) + @IsOptional() + @IsEnum(GenreType) + genre?: GenreType = GenreType.AUTRE; + + @ApiProperty({ enum: RoleType }) + @IsEnum(RoleType) + role: RoleType; + + @ApiProperty({ enum: StatutUtilisateurType, required: false, default: StatutUtilisateurType.EN_ATTENTE }) + @IsOptional() + @IsEnum(StatutUtilisateurType) + statut?: StatutUtilisateurType = StatutUtilisateurType.EN_ATTENTE; + + @ApiProperty({ example: SituationFamilialeType.MARIE, required: false, enum: SituationFamilialeType, default: SituationFamilialeType.MARIE}) + @IsOptional() + @IsEnum(SituationFamilialeType) + situation_familiale?: SituationFamilialeType; + + @ApiProperty({ example: '+33123456789' }) + @IsString() + @IsNotEmpty() + @MaxLength(20) + telephone: string; + + @ApiProperty({ example: 'Paris', required: false }) + @IsOptional() + @IsString() + @MaxLength(150) + ville?: string; + + @ApiProperty({ example: '75000', required: false }) + @IsOptional() + @IsString() + @MaxLength(10) + code_postal?: string; + + @ApiProperty({ example: '10 rue de la paix, 75000 Paris' }) + @IsString() + @IsNotEmpty() + adresse: string; + + @ApiProperty({ example: 'https://example.com/photo.jpg', required: false }) + @IsOptional() + @IsString() + photo_url?: string; + + @ApiProperty({ default: false }) + @IsOptional() + @IsBoolean() + consentement_photo?: boolean = false; + + @ApiProperty({ required: false }) + @IsOptional() + @IsDateString({}, { message: 'date_consentement_photo doit être une date ISO valide' }) + date_consentement_photo?: string | null; + + @ApiProperty({ default: false }) + @IsOptional() + @IsBoolean() + changement_mdp_obligatoire?: boolean = false; + + @ApiProperty({ example: true }) + @IsBoolean() + @IsNotEmpty() + cguAccepted: boolean; +} diff --git a/backend/src/routes/user/dto/update_admin.dto.ts b/backend/src/routes/user/dto/update_admin.dto.ts new file mode 100644 index 0000000..722dfb5 --- /dev/null +++ b/backend/src/routes/user/dto/update_admin.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateAdminDto } from "./create_admin.dto"; + +export class UpdateAdminDto extends PartialType(CreateAdminDto) {} \ No newline at end of file diff --git a/backend/src/routes/user/dto/update_assistante.dto.ts b/backend/src/routes/user/dto/update_assistante.dto.ts new file mode 100644 index 0000000..37c4f98 --- /dev/null +++ b/backend/src/routes/user/dto/update_assistante.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateAssistanteDto } from "./create_assistante.dto"; + +export class UpdateAssistanteDto extends PartialType(CreateAssistanteDto) {} diff --git a/backend/src/routes/user/dto/update_gestionnaire.dto.ts b/backend/src/routes/user/dto/update_gestionnaire.dto.ts new file mode 100644 index 0000000..ab035db --- /dev/null +++ b/backend/src/routes/user/dto/update_gestionnaire.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateGestionnaireDto } from "./create_gestionnaire.dto"; + +export class UpdateGestionnaireDto extends PartialType(CreateGestionnaireDto) {} \ No newline at end of file diff --git a/backend/src/routes/user/dto/update_parent.dto.ts b/backend/src/routes/user/dto/update_parent.dto.ts new file mode 100644 index 0000000..0f26d56 --- /dev/null +++ b/backend/src/routes/user/dto/update_parent.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateParentDto } from "./create_parent.dto"; + +export class UpdateParentsDto extends PartialType(CreateParentDto) {} \ No newline at end of file diff --git a/backend/src/routes/user/dto/update_user.dto.ts b/backend/src/routes/user/dto/update_user.dto.ts new file mode 100644 index 0000000..13eb819 --- /dev/null +++ b/backend/src/routes/user/dto/update_user.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateUserDto } from "./create_user.dto"; + +export class UpdateUserDto extends PartialType(CreateUserDto) {} \ No newline at end of file diff --git a/backend/src/routes/user/gestionnaires/gestionnaires.controller.spec.ts b/backend/src/routes/user/gestionnaires/gestionnaires.controller.spec.ts new file mode 100644 index 0000000..6dee426 --- /dev/null +++ b/backend/src/routes/user/gestionnaires/gestionnaires.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GestionnairesController } from './gestionnaires.controller'; +import { GestionnairesService } from './gestionnaires.service'; + +describe('GestionnairesController', () => { + let controller: GestionnairesController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [GestionnairesController], + providers: [GestionnairesService], + }).compile(); + + controller = module.get(GestionnairesController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/routes/user/gestionnaires/gestionnaires.controller.ts b/backend/src/routes/user/gestionnaires/gestionnaires.controller.ts new file mode 100644 index 0000000..7a1489f --- /dev/null +++ b/backend/src/routes/user/gestionnaires/gestionnaires.controller.ts @@ -0,0 +1,73 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, +} from '@nestjs/common'; +import { GestionnairesService } from './gestionnaires.service'; +import { RoleType, Users } from 'src/entities/users.entity'; +import { Roles } from 'src/common/decorators/roles.decorator'; +import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto'; +import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto'; +import { RolesGuard } from 'src/common/guards/roles.guard'; +import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthGuard } from 'src/common/guards/auth.guard'; + + +@ApiTags('Gestionnaires') +@ApiBearerAuth('access-token') +@UseGuards(AuthGuard, RolesGuard) +@Controller('gestionnaires') +export class GestionnairesController { + constructor(private readonly gestionnairesService: GestionnairesService) { } + + @Roles(RoleType.SUPER_ADMIN) + @ApiResponse({ status: 201, description: 'Le gestionnaire a été créé avec succès.', type: Users }) + @ApiResponse({ status: 409, description: 'Conflit. L\'email est déjà utilisé.' }) + @ApiOperation({ summary: 'Création d\'un gestionnaire' }) + @ApiBody({ type: CreateGestionnaireDto }) + @Post() + create(@Body() dto: CreateGestionnaireDto): Promise { + return this.gestionnairesService.create(dto); + } + + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @ApiOperation({ summary: 'Liste des gestionnaires' }) + @ApiResponse({ status: 200, description: 'Liste des gestionnaires : ', type: [Users] }) + @Get() + getAll(): Promise { + return this.gestionnairesService.findAll(); + } + + @Roles(RoleType.GESTIONNAIRE, RoleType.SUPER_ADMIN) + @ApiOperation({ summary: 'Récupérer un gestionnaire par ID' }) + @ApiResponse({ status: 400, description: 'ID invalide' }) + @ApiResponse({ status: 403, description: 'Accès refusé' }) + @ApiResponse({ status: 401, description: 'Non authentifié' }) + @ApiParam({ name: 'id', description: 'ID du gestionnaire' }) + @ApiResponse({ status: 200, description: 'Gestionnaire trouvé', type: Users }) + @ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' }) + @Get(':id') + findOne(@Param('id') id: string): Promise { + return this.gestionnairesService.findOne(id); + } + + @Roles(RoleType.SUPER_ADMIN) + @ApiOperation({ summary: 'Mettre à jour un gestionnaire' }) + @ApiResponse({ status: 200, description: 'Le gestionnaire a été mis à jour avec succès.', type: Users }) + @ApiResponse({ status: 404, description: 'Gestionnaire non trouvé' }) + @ApiResponse({ status: 403, description: 'Accès refusé' }) + @ApiResponse({ status: 401, description: 'Non authentifié' }) + @ApiParam({ name: 'id', description: 'ID du gestionnaire' }) + @Patch(':id') + update( + @Param('id') id: string, + @Body() dto: UpdateGestionnaireDto, + ): Promise { + return this.gestionnairesService.update(id, dto); + } +} diff --git a/backend/src/routes/user/gestionnaires/gestionnaires.module.ts b/backend/src/routes/user/gestionnaires/gestionnaires.module.ts new file mode 100644 index 0000000..bfd32f8 --- /dev/null +++ b/backend/src/routes/user/gestionnaires/gestionnaires.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { GestionnairesService } from './gestionnaires.service'; +import { GestionnairesController } from './gestionnaires.controller'; +import { Users } from 'src/entities/users.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [TypeOrmModule.forFeature([Users])], + controllers: [GestionnairesController], + providers: [GestionnairesService], +}) +export class GestionnairesModule { } diff --git a/backend/src/routes/user/gestionnaires/gestionnaires.service.spec.ts b/backend/src/routes/user/gestionnaires/gestionnaires.service.spec.ts new file mode 100644 index 0000000..5460385 --- /dev/null +++ b/backend/src/routes/user/gestionnaires/gestionnaires.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GestionnairesService } from './gestionnaires.service'; + +describe('GestionnairesService', () => { + let service: GestionnairesService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [GestionnairesService], + }).compile(); + + service = module.get(GestionnairesService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/routes/user/gestionnaires/gestionnaires.service.ts b/backend/src/routes/user/gestionnaires/gestionnaires.service.ts new file mode 100644 index 0000000..4e1406e --- /dev/null +++ b/backend/src/routes/user/gestionnaires/gestionnaires.service.ts @@ -0,0 +1,87 @@ +import { + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { RoleType, Users } from 'src/entities/users.entity'; +import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto'; +import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto'; +import * as bcrypt from 'bcrypt'; + +@Injectable() +export class GestionnairesService { + constructor( + @InjectRepository(Users) + private readonly gestionnaireRepository: Repository, + ) { } + + // Création d’un gestionnaire + async create(dto: CreateGestionnaireDto): Promise { + const exist = await this.gestionnaireRepository.findOneBy({ email: dto.email }); + if (exist) throw new ConflictException('Email déjà utilisé'); + + const salt = await bcrypt.genSalt(); + const hashedPassword = await bcrypt.hash(dto.password, salt); + + const entity = this.gestionnaireRepository.create({ + email: dto.email, + password: hashedPassword, + prenom: dto.prenom, + nom: dto.nom, + genre: dto.genre, + statut: dto.statut, + telephone: dto.telephone, + adresse: dto.adresse, + photo_url: dto.photo_url, + consentement_photo: dto.consentement_photo ?? false, + date_consentement_photo: dto.date_consentement_photo + ? new Date(dto.date_consentement_photo) + : undefined, + changement_mdp_obligatoire: dto.changement_mdp_obligatoire ?? false, + role: RoleType.GESTIONNAIRE, + }); + return this.gestionnaireRepository.save(entity); + } + + // Liste des gestionnaires + async findAll(): Promise { + return this.gestionnaireRepository.find({ where: { role: RoleType.GESTIONNAIRE } }); + } + + // Récupérer un gestionnaire par ID + async findOne(id: string): Promise { + const gestionnaire = await this.gestionnaireRepository.findOne({ + where: { id, role: RoleType.GESTIONNAIRE }, + }); + if (!gestionnaire) throw new NotFoundException('Gestionnaire introuvable'); + return gestionnaire; + } + + // Mise à jour d’un gestionnaire + async update(id: string, dto: UpdateGestionnaireDto): Promise { + const gestionnaire = await this.findOne(id); + + if (dto.password) { + const salt = await bcrypt.genSalt(); + gestionnaire.password = await bcrypt.hash(dto.password, salt); + } + + if (dto.date_consentement_photo !== undefined) { + gestionnaire.date_consentement_photo = dto.date_consentement_photo + ? new Date(dto.date_consentement_photo) + : undefined; + } + + const { password, date_consentement_photo, ...rest } = dto; + Object.entries(rest).forEach(([key, value]) => { + if (value !== undefined) { + (gestionnaire as any)[key] = value; + } + }); + + return this.gestionnaireRepository.save(gestionnaire); + } + +} diff --git a/backend/src/routes/user/user.controller.spec.ts b/backend/src/routes/user/user.controller.spec.ts new file mode 100644 index 0000000..1f38440 --- /dev/null +++ b/backend/src/routes/user/user.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; + +describe('UserController', () => { + let controller: UserController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + providers: [UserService], + }).compile(); + + controller = module.get(UserController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/routes/user/user.controller.ts b/backend/src/routes/user/user.controller.ts new file mode 100644 index 0000000..3fe3187 --- /dev/null +++ b/backend/src/routes/user/user.controller.ts @@ -0,0 +1,94 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthGuard } from 'src/common/guards/auth.guard'; +import { Roles } from 'src/common/decorators/roles.decorator'; +import { User } from 'src/common/decorators/user.decorator'; +import { RoleType, Users } from 'src/entities/users.entity'; +import { UserService } from './user.service'; +import { CreateUserDto } from './dto/create_user.dto'; +import { UpdateUserDto } from './dto/update_user.dto'; + +@ApiTags('Utilisateurs') +@ApiBearerAuth('access-token') +@UseGuards(AuthGuard) +@Controller('users') +export class UserController { + constructor(private readonly userService: UserService) { } + + // Création d'un utilisateur (réservée aux super admins) + @Post() + @Roles(RoleType.SUPER_ADMIN) + @ApiOperation({ summary: 'Créer un nouvel utilisateur (super admin seulement)' }) + createUser( + @Body() dto: CreateUserDto, + @User() currentUser: Users + ) { + return this.userService.createUser(dto, currentUser); + } + + // Lister tous les utilisateurs (super_admin uniquement) + @Get() + @Roles(RoleType.SUPER_ADMIN) + @ApiOperation({ summary: 'Lister tous les utilisateurs' }) + findAll() { + return this.userService.findAll(); + } + + // Récupérer un utilisateur par son ID + @Get(':id') + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE) + @ApiOperation({ summary: 'Trouver un utilisateur par son id' }) + @ApiParam({ name: 'id', description: "UUID de l'utilisateur" }) + findOne(@Param('id') id: string) { + return this.userService.findOne(id); + } + + // Modifier un utilisateur (réservé super_admin) + @Patch(':id') + @Roles(RoleType.SUPER_ADMIN) + @ApiOperation({ summary: 'Mettre à jour un utilisateur' }) + @ApiParam({ name: 'id', description: "UUID de l'utilisateur" }) + updateUser( + @Param('id') id: string, + @Body() dto: UpdateUserDto, + @User() currentUser: Users + ) { + return this.userService.updateUser(id, dto, currentUser); + } + + @Patch(':id/valider') + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) + @ApiOperation({ summary: 'Valider un compte utilisateur' }) + @ApiParam({ name: 'id', description: "UUID de l'utilisateur" }) + @ApiResponse({ status: 400, description: 'ID invalide' }) + @ApiResponse({ status: 403, description: 'Accès refusé' }) + @ApiResponse({ status: 200, description: 'Compte validé avec succès' }) + validate( + @Param('id') id: string, + @User() currentUser: Users, + @Body('comment') comment?: string, + ) { + return this.userService.validateUser(id, currentUser, comment); + } + + @Patch(':id/suspendre') + @Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR) + @ApiOperation({ summary: 'Suspendre un compte utilisateur' }) + @ApiParam({ name: 'id', description: "UUID de l'utilisateur" }) + suspend( + @Param('id') id: string, + @User() currentUser: Users, + @Body('comment') comment?: string, + ) { + return this.userService.suspendUser(id, currentUser, comment); + } + + // Supprimer un utilisateur (super_admin uniquement) + @Delete(':id') + @Roles(RoleType.SUPER_ADMIN) + @ApiOperation({ summary: 'Supprimer un utilisateur' }) + @ApiParam({ name: 'id', description: "UUID de l'utilisateur" }) + remove(@Param('id') id: string, @User() currentUser: Users) { + return this.userService.remove(id, currentUser); + } +} diff --git a/backend/src/routes/user/user.module.ts b/backend/src/routes/user/user.module.ts new file mode 100644 index 0000000..484f85d --- /dev/null +++ b/backend/src/routes/user/user.module.ts @@ -0,0 +1,28 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Users } from 'src/entities/users.entity'; +import { AuthModule } from '../auth/auth.module'; +import { Validation } from 'src/entities/validations.entity'; +import { ParentsModule } from '../parents/parents.module'; +import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity'; +import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module'; +import { Parents } from 'src/entities/parents.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature( + [ + Users, + Validation, + Parents, + AssistanteMaternelle, + ]), forwardRef(() => AuthModule), + ParentsModule, + AssistantesMaternellesModule, + ], + controllers: [UserController], + providers: [UserService], + exports: [UserService], +}) +export class UserModule { } diff --git a/backend/src/routes/user/user.service.spec.ts b/backend/src/routes/user/user.service.spec.ts new file mode 100644 index 0000000..873de8a --- /dev/null +++ b/backend/src/routes/user/user.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserService], + }).compile(); + + service = module.get(UserService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/routes/user/user.service.ts b/backend/src/routes/user/user.service.ts new file mode 100644 index 0000000..db775fb --- /dev/null +++ b/backend/src/routes/user/user.service.ts @@ -0,0 +1,233 @@ +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { RoleType, StatutUtilisateurType, Users } from "src/entities/users.entity"; +import { In, Repository } from "typeorm"; +import { CreateUserDto } from "./dto/create_user.dto"; +import { UpdateUserDto } from "./dto/update_user.dto"; +import * as bcrypt from 'bcrypt'; +import { StatutValidationType, Validation } from "src/entities/validations.entity"; +import { Parents } from "src/entities/parents.entity"; +import { AssistanteMaternelle } from "src/entities/assistantes_maternelles.entity"; + +@Injectable() +export class UserService { + constructor( + @InjectRepository(Users) + private readonly usersRepository: Repository, + + @InjectRepository(Validation) + private readonly validationRepository: Repository, + + @InjectRepository(Parents) + private readonly parentsRepository: Repository, + + @InjectRepository(AssistanteMaternelle) + private readonly assistantesRepository: Repository + ) { } + + async createUser(dto: CreateUserDto, currentUser?: Users): Promise { + if (!dto.cguAccepted) { + throw new BadRequestException( + 'Vous devez accepter les CGU et la Politique de confidentialité pour créer un compte.', + ); + } + + const exist = await this.usersRepository.findOneBy({ email: dto.email }); + if (exist) throw new BadRequestException('Email déjà utilisé'); + + const isSuperAdmin = currentUser?.role === RoleType.SUPER_ADMIN; + const isAdmin = currentUser?.role === RoleType.ADMINISTRATEUR; + + let role: RoleType; + + if (dto.role === RoleType.GESTIONNAIRE) { + if (!isAdmin && !isSuperAdmin) { + throw new ForbiddenException('Seuls les administrateurs peuvent créer un gestionnaire'); + } + role = RoleType.GESTIONNAIRE; + } else if (dto.role === RoleType.ADMINISTRATEUR) { + if (!isAdmin && !isSuperAdmin) { + throw new ForbiddenException('Seuls les administrateurs peuvent créer un administrateur'); + } + role = RoleType.ADMINISTRATEUR; + } else if (dto.role === RoleType.ASSISTANTE_MATERNELLE) { + role = RoleType.ASSISTANTE_MATERNELLE; + if (!dto.photo_url) { + throw new BadRequestException( + 'La photo de profil est obligatoire pour les assistantes maternelles.', + ); + } + } else { + role = RoleType.PARENT; + } + + const statut = isSuperAdmin + ? dto.statut ?? StatutUtilisateurType.EN_ATTENTE + : StatutUtilisateurType.EN_ATTENTE; + + if (!dto.nom?.trim()) throw new BadRequestException('Nom est obligatoire.'); + if (!dto.prenom?.trim()) throw new BadRequestException('Prénom est obligatoire.'); + if (!dto.adresse?.trim()) throw new BadRequestException('Adresse est obligatoire.'); + if (!dto.telephone?.trim()) throw new BadRequestException('Téléphone est obligatoire.'); + + let consentDate: Date | undefined; + if (dto.consentement_photo && dto.date_consentement_photo) { + const parsed = new Date(dto.date_consentement_photo); + if (!isNaN(parsed.getTime())) { + consentDate = parsed; + } + } + + const salt = await bcrypt.genSalt(); + const hashedPassword = await bcrypt.hash(dto.password, salt); + + const entity = this.usersRepository.create({ + email: dto.email, + password: hashedPassword, + prenom: dto.prenom, + nom: dto.nom, + role, + statut, + genre: dto.genre, + telephone: dto.telephone, + ville: dto.ville, + code_postal: dto.code_postal, + adresse: dto.adresse, + photo_url: dto.photo_url, + consentement_photo: dto.consentement_photo ?? false, + date_consentement_photo: consentDate, + changement_mdp_obligatoire: + role === RoleType.ADMINISTRATEUR || role === RoleType.GESTIONNAIRE + ? true + : dto.changement_mdp_obligatoire ?? false, + }); + + const saved = await this.usersRepository.save(entity); + return this.findOne(saved.id); + } + + async findAll(): Promise { + return this.usersRepository.find(); + } + + async findOneBy(where: Partial): Promise { + return this.usersRepository.findOne({ where }); + } + + async findOne(id: string): Promise { + const user = await this.usersRepository.findOne({ where: { id } }); + if (!user) { + throw new NotFoundException('Utilisateur introuvable'); + } + return user; + } + + async findByEmailOrNull(email: string): Promise { + return this.usersRepository.findOne({ where: { email } }); + } + + async updateUser(id: string, dto: UpdateUserDto, currentUser: Users): Promise { + const user = await this.findOne(id); + + // Interdire changement de rôle si pas super admin + if (dto.role && currentUser.role !== RoleType.SUPER_ADMIN) { + throw new ForbiddenException('Accès réservé aux super admins'); + } + + // Empêcher de modifier le flag changement_mdp_obligatoire pour admin/gestionnaire + if ( + (user.role === RoleType.ADMINISTRATEUR || user.role === RoleType.GESTIONNAIRE) && + dto.changement_mdp_obligatoire === false + ) { + throw new ForbiddenException( + 'Impossible de désactiver l’obligation de changement de mot de passe pour ce rôle', + ); + } + + // Gestion du mot de passe + if (dto.password) { + const salt = await bcrypt.genSalt(); + user.password = await bcrypt.hash(dto.password, salt); + delete (dto as any).password; + // Une fois le mot de passe changé, on peut lever l’obligation + user.changement_mdp_obligatoire = false; + } + + // Conversion de la date de consentement + if (dto.date_consentement_photo !== undefined) { + user.date_consentement_photo = dto.date_consentement_photo + ? new Date(dto.date_consentement_photo) + : undefined; + delete (dto as any).date_consentement_photo; + } + + Object.assign(user, dto); + return this.usersRepository.save(user); + } + + // Valider un compte utilisateur + async validateUser(user_id: string, currentUser: Users, comment?: string): Promise { + if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) { + throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires'); + } + + const user = await this.usersRepository.findOne({ where: { id: user_id } }); + if (!user) throw new NotFoundException('Utilisateur introuvable'); + + user.statut = StatutUtilisateurType.ACTIF; + const savedUser = await this.usersRepository.save(user); + if (user.role === RoleType.PARENT) { + const existParent = await this.parentsRepository.findOneBy({ user_id: user.id }); + if (!existParent) { + const parentEntity = this.parentsRepository.create({ user_id: user.id, user }); + await this.parentsRepository.save(parentEntity); + } + } else if (user.role === RoleType.ASSISTANTE_MATERNELLE) { + const existAssistante = await this.assistantesRepository.findOneBy({ user_id: user.id }); + if (!existAssistante) { + const assistanteEntity = this.assistantesRepository.create({ user_id: user.id, user }); + await this.assistantesRepository.save(assistanteEntity); + } + } + const validation = this.validationRepository.create({ + user: savedUser, + type: 'validation_compte', + status: StatutValidationType.VALIDE, + validated_by: currentUser, + comment, + }); + await this.validationRepository.save(validation); + return savedUser; + } + + + // Mettre un compte en statut suspendu + async suspendUser(user_id: string, currentUser: Users, comment?: string): Promise { + if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) { + throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires'); + } + const user = await this.usersRepository.findOne({ where: { id: user_id } }); + if (!user) throw new NotFoundException('Utilisateur introuvable'); + user.statut = StatutUtilisateurType.SUSPENDU; + const savedUser = await this.usersRepository.save(user); + + const suspend = this.validationRepository.create({ + user: savedUser, + type: 'suspension_compte', + status: StatutValidationType.VALIDE, + validated_by: currentUser, + comment, + }) + await this.validationRepository.save(suspend); + return savedUser; + } + async remove(id: string, currentUser: Users): Promise { + if (currentUser.role !== RoleType.SUPER_ADMIN) { + throw new ForbiddenException('Accès réservé aux super admins'); + } + const result = await this.usersRepository.delete(id); + if (result.affected === 0) { + throw new NotFoundException('Utilisateur introuvable'); + } + } +} \ No newline at end of file diff --git a/backend/src/scripts/initAdmin.ts b/backend/src/scripts/initAdmin.ts deleted file mode 100644 index dd59a96..0000000 --- a/backend/src/scripts/initAdmin.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import * as bcrypt from 'bcrypt'; - -const prisma = new PrismaClient(); - -async function main() { - try { - // Vérifier si l'administrateur existe déjà - const existingAdmin = await prisma.admin.findUnique({ - where: { email: 'administrateur@ptitspas.fr' } - }); - - if (!existingAdmin) { - // Hasher le mot de passe - const hashedPassword = await bcrypt.hash('password', 10); - - // Créer l'administrateur - await prisma.admin.create({ - data: { - email: 'administrateur@ptitspas.fr', - password: hashedPassword, - firstName: 'Administrateur', - lastName: 'P\'titsPas', - passwordChanged: false - } - }); - - console.log('✅ Administrateur créé avec succès'); - } else { - console.log('ℹ️ L\'administrateur existe déjà'); - } - } catch (error) { - console.error('❌ Erreur lors de la création de l\'administrateur:', error); - } finally { - await prisma.$disconnect(); - } -} - -main(); \ No newline at end of file diff --git a/backend/src/services/theme.service.ts b/backend/src/services/theme.service.ts deleted file mode 100644 index 47a0c79..0000000 --- a/backend/src/services/theme.service.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -export interface ThemeData { - name: string; - primaryColor: string; - secondaryColor: string; - backgroundColor: string; - textColor: string; -} - -export class ThemeService { - // Créer un nouveau thème - static async createTheme(data: ThemeData) { - return prisma.theme.create({ - data: { - ...data, - isActive: false, - }, - }); - } - - // Récupérer tous les thèmes - static async getAllThemes() { - return prisma.theme.findMany(); - } - - // Récupérer le thème actif - static async getActiveTheme() { - const settings = await prisma.appSettings.findFirst({ - include: { - currentTheme: true, - }, - }); - return settings?.currentTheme; - } - - // Activer un thème - static async activateTheme(themeId: string) { - // Désactiver tous les thèmes - await prisma.theme.updateMany({ - where: { isActive: true }, - data: { isActive: false }, - }); - - // Activer le thème sélectionné - const updatedTheme = await prisma.theme.update({ - where: { id: themeId }, - data: { isActive: true }, - }); - - // Mettre à jour les paramètres de l'application - await prisma.appSettings.upsert({ - where: { id: '1' }, - update: { currentThemeId: themeId }, - create: { id: '1', currentThemeId: themeId }, - }); - - return updatedTheme; - } - - // Mettre à jour un thème - static async updateTheme(themeId: string, data: Partial) { - return prisma.theme.update({ - where: { id: themeId }, - data, - }); - } - - // Supprimer un thème - static async deleteTheme(themeId: string) { - return prisma.theme.delete({ - where: { id: themeId }, - }); - } -} \ No newline at end of file diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts new file mode 100644 index 0000000..14c2c1c --- /dev/null +++ b/backend/src/types/express/index.d.ts @@ -0,0 +1,11 @@ +import { Users } from 'src/entities/users.entity'; + +declare module 'express-serve-static-core' { + interface Request { + user?: Users & { + sub?: string; + email?: string; + role?: string; + }; + } +} \ No newline at end of file diff --git a/backend/test/app.e2e-spec.ts b/backend/test/app.e2e-spec.ts new file mode 100644 index 0000000..4df6580 --- /dev/null +++ b/backend/test/app.e2e-spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/backend/test/jest-e2e.json b/backend/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/backend/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 97175de..8ced230 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,28 +1,26 @@ { "compilerOptions": { - "target": "es2018", - "module": "commonjs", - "lib": ["es2018", "esnext.asynciterable"], - "skipLibCheck": true, - "sourceMap": true, - "outDir": "./dist", - "moduleResolution": "node", - "removeComments": true, - "noImplicitAny": true, - "strictNullChecks": true, - "strictFunctionTypes": true, - "noImplicitThis": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "allowSyntheticDefaultImports": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "resolveJsonModule": true, - "baseUrl": "." - }, - "exclude": ["node_modules"], - "include": ["./src/**/*.ts"] -} \ No newline at end of file + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false, + "typeRoots": ["./node_modules/@types", "./src/types"] + } +} diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +.env diff --git a/database/BDD.sql b/database/BDD.sql new file mode 100644 index 0000000..1e7bc0b --- /dev/null +++ b/database/BDD.sql @@ -0,0 +1,369 @@ +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- ========================================================== +-- ENUMS +-- ========================================================== +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN + CREATE TYPE role_type AS ENUM ('parent', 'gestionnaire', 'super_admin', 'administrateur', 'assistante_maternelle'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'genre_type') THEN + CREATE TYPE genre_type AS ENUM ('H', 'F'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN + CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente','actif','suspendu'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN + CREATE TYPE statut_enfant_type AS ENUM ('a_naitre','actif','scolarise'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_dossier_type') THEN + CREATE TYPE statut_dossier_type AS ENUM ('envoye','accepte','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_contrat_type') THEN + CREATE TYPE statut_contrat_type AS ENUM ('brouillon','en_attente_signature','valide','resilie'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN + CREATE TYPE statut_avenant_type AS ENUM ('propose','accepte','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN + CREATE TYPE type_evenement_type AS ENUM ('absence_enfant','conge_am','conge_parent','arret_maladie_am','evenement_rpe'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN + CREATE TYPE statut_evenement_type AS ENUM ('propose','valide','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN + CREATE TYPE statut_validation_type AS ENUM ('en_attente','valide','refuse'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'situation_familiale_type') THEN + CREATE TYPE situation_familiale_type AS ENUM ('celibataire','marie','concubinage','pacse','separe','divorce','veuf','parent_isole'); + END IF; +END $$; + +-- ========================================================== +-- Table : utilisateurs +-- ========================================================== +CREATE TABLE utilisateurs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) NOT NULL UNIQUE, + CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + password TEXT, -- NULL avant création via token + prenom VARCHAR(100), + nom VARCHAR(100), + genre genre_type, + role role_type NOT NULL, + statut statut_utilisateur_type DEFAULT 'en_attente', + telephone VARCHAR(20), -- Unifié (mobile privilégié) + adresse TEXT, + date_naissance DATE, + photo_url TEXT, -- Obligatoire pour AM, non utilisé pour parents + consentement_photo BOOLEAN DEFAULT false, + date_consentement_photo TIMESTAMPTZ, + token_creation_mdp VARCHAR(255), -- Token pour créer MDP après validation + token_creation_mdp_expire_le TIMESTAMPTZ, -- Expiration 7 jours + changement_mdp_obligatoire BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now(), + ville VARCHAR(150), + code_postal VARCHAR(10), + profession VARCHAR(150), + situation_familiale situation_familiale_type +); + +-- Index pour recherche par token +CREATE INDEX idx_utilisateurs_token_creation_mdp + ON utilisateurs(token_creation_mdp) + WHERE token_creation_mdp IS NOT NULL; + +-- ========================================================== +-- Table : assistantes_maternelles +-- ========================================================== +CREATE TABLE assistantes_maternelles ( + id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE, + numero_agrement VARCHAR(50), + date_agrement DATE NOT NULL, -- Obligatoire selon CDC v1.3 + nir_chiffre CHAR(15), + nb_max_enfants INT, + place_disponible INT, + biographie TEXT, + disponible BOOLEAN DEFAULT true +); + +-- ========================================================== +-- Table : parents +-- ========================================================== +CREATE TABLE parents ( + id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE, + id_co_parent UUID REFERENCES utilisateurs(id) +); + +-- ========================================================== +-- Table : enfants +-- ========================================================== +CREATE TABLE enfants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + statut statut_enfant_type, + prenom VARCHAR(100), + nom VARCHAR(100), + genre genre_type NOT NULL, -- Obligatoire selon CDC + date_naissance DATE, + date_prevue_naissance DATE, + photo_url TEXT, + consentement_photo BOOLEAN DEFAULT false, + date_consentement_photo TIMESTAMPTZ, + est_multiple BOOLEAN DEFAULT false +); + +-- ========================================================== +-- Table : enfants_parents +-- ========================================================== +CREATE TABLE enfants_parents ( + id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + PRIMARY KEY (id_parent, id_enfant) +); + +-- ========================================================== +-- Table : dossiers +-- ========================================================== +CREATE TABLE dossiers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + presentation TEXT, + type_contrat VARCHAR(50), + repas BOOLEAN DEFAULT false, + budget NUMERIC(10,2), + planning_souhaite JSONB, + statut statut_dossier_type DEFAULT 'envoye', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : messages +-- ========================================================== +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_dossier UUID REFERENCES dossiers(id) ON DELETE CASCADE, + id_expediteur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + contenu TEXT, + re_redige_par_ia BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : contrats +-- ========================================================== +CREATE TABLE contrats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_dossier UUID UNIQUE REFERENCES dossiers(id) ON DELETE CASCADE, + planning JSONB, + tarif_horaire NUMERIC(6,2), + indemnites_repas NUMERIC(6,2), + date_debut DATE, + statut statut_contrat_type DEFAULT 'brouillon', + signe_parent BOOLEAN DEFAULT false, + signe_am BOOLEAN DEFAULT false, + finalise_le TIMESTAMPTZ, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : avenants_contrats +-- ========================================================== +CREATE TABLE avenants_contrats ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_contrat UUID REFERENCES contrats(id) ON DELETE CASCADE, + modifications JSONB, + initie_par UUID REFERENCES utilisateurs(id), + statut statut_avenant_type DEFAULT 'propose', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : evenements +-- ========================================================== +CREATE TABLE evenements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type type_evenement_type, + id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, + id_am UUID REFERENCES utilisateurs(id), + id_parent UUID REFERENCES parents(id_utilisateur), + cree_par UUID REFERENCES utilisateurs(id), + date_debut TIMESTAMPTZ, + date_fin TIMESTAMPTZ, + commentaires TEXT, + statut statut_evenement_type DEFAULT 'propose', + delai_grace TIMESTAMPTZ, + urgent BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : signalements_bugs +-- ========================================================== +CREATE TABLE signalements_bugs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id), + description TEXT, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : uploads +-- ========================================================== +CREATE TABLE uploads ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL, + fichier_url TEXT NOT NULL, + type VARCHAR(50), + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : notifications +-- ========================================================== +CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + contenu TEXT, + lu BOOLEAN DEFAULT false, + cree_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : validations +-- ========================================================== +CREATE TABLE validations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id), + type VARCHAR(50), + statut statut_validation_type DEFAULT 'en_attente', + cree_le TIMESTAMPTZ DEFAULT now(), + modifie_le TIMESTAMPTZ DEFAULT now() +); + +-- ========================================================== +-- Table : configuration +-- ========================================================== +CREATE TABLE configuration ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + cle VARCHAR(100) UNIQUE NOT NULL, + valeur TEXT, + type VARCHAR(50) NOT NULL, + categorie VARCHAR(50), + description TEXT, + modifie_le TIMESTAMPTZ DEFAULT now(), + modifie_par UUID REFERENCES utilisateurs(id) +); + +-- Index pour performance +CREATE INDEX idx_configuration_cle ON configuration(cle); +CREATE INDEX idx_configuration_categorie ON configuration(categorie); + +-- Seed initial de configuration +INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES +-- === Configuration Email (SMTP) === +('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'), +('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'), +('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'), +('smtp_auth_required', 'false', 'boolean', 'email', 'Authentification SMTP requise'), +('smtp_user', '', 'string', 'email', 'Utilisateur SMTP (si authentification requise)'), +('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'), +('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'), +('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'), + +-- === Configuration Application === +('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'), +('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'), +('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'), +('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'), + +-- === Configuration Sécurité === +('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'), +('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'), +('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'), +('bcrypt_rounds', '12', 'number', 'security', 'Nombre de rounds bcrypt pour le hachage des mots de passe'); + +-- ========================================================== +-- Table : documents_legaux +-- ========================================================== +CREATE TABLE documents_legaux ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy' + version INTEGER NOT NULL, -- Numéro de version (auto-incrémenté) + fichier_nom VARCHAR(255) NOT NULL, -- Nom original du fichier + fichier_path VARCHAR(500) NOT NULL, -- Chemin de stockage + fichier_hash VARCHAR(64) NOT NULL, -- Hash SHA-256 pour intégrité + actif BOOLEAN DEFAULT false, -- Version actuellement active + televerse_par UUID REFERENCES utilisateurs(id), -- Qui a uploadé + televerse_le TIMESTAMPTZ DEFAULT now(), -- Date d'upload + active_le TIMESTAMPTZ, -- Date d'activation + UNIQUE(type, version) -- Pas de doublon version +); + +-- Index pour performance +CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif); +CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC); + +-- ========================================================== +-- Table : acceptations_documents +-- ========================================================== +CREATE TABLE acceptations_documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + id_document UUID REFERENCES documents_legaux(id), + type_document VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy' + version_document INTEGER NOT NULL, -- Version acceptée + accepte_le TIMESTAMPTZ DEFAULT now(), -- Date d'acceptation + ip_address INET, -- IP de l'utilisateur (RGPD) + user_agent TEXT -- Navigateur (preuve) +); + +-- Index pour performance +CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur); +CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document); + +-- ========================================================== +-- Modification Table : utilisateurs (ajout colonnes documents) +-- ========================================================== +ALTER TABLE utilisateurs + ADD COLUMN IF NOT EXISTS cgu_version_acceptee INTEGER, + ADD COLUMN IF NOT EXISTS cgu_acceptee_le TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS privacy_version_acceptee INTEGER, + ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ; + +-- ========================================================== +-- Seed : Documents légaux génériques v1 +-- ========================================================== +INSERT INTO documents_legaux (type, version, fichier_nom, fichier_path, fichier_hash, actif, televerse_le, active_le) VALUES +('cgu', 1, 'cgu_v1_default.pdf', '/documents/legaux/cgu_v1_default.pdf', 'a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2', true, now(), now()), +('privacy', 1, 'privacy_v1_default.pdf', '/documents/legaux/privacy_v1_default.pdf', 'b4f9c3d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4', true, now(), now()); + +-- ========================================================== +-- Seed : Super Administrateur par défaut +-- ========================================================== +-- Email: admin@ptits-pas.fr +-- Mot de passe: 4dm1n1strateur (hashé bcrypt) +-- IMPORTANT: Changer ce mot de passe en production ! +-- ========================================================== +INSERT INTO utilisateurs ( + email, + password, + prenom, + nom, + role, + statut, + changement_mdp_obligatoire +) VALUES ( + 'admin@ptits-pas.fr', + '$2b$12$plOZCW7lzLFkWgDPcE6p6u10EA4yErQt6Xcp5nyH3Sp/2.6EpNW.6', + 'Super', + 'Administrateur', + 'super_admin', + 'actif', + true +); diff --git a/database/README.md b/database/README.md new file mode 100644 index 0000000..4ebcd90 --- /dev/null +++ b/database/README.md @@ -0,0 +1,80 @@ + + +# PtitsPas Ynov - Base de Données + +Ce projet contient la **base de données** pour l'application PtitsPas, avec scripts de migration, import de données, documentation et configuration Docker. + +--- + +## Prérequis + +- Docker Desktop (https://www.docker.com/products/docker-desktop/) +- Docker Compose + +--- + +## Structure du projet + +- `migrations/` : scripts SQL pour la création et l'import de la base +- `bdd/data_test/` : fichiers CSV pour l'import de données de test +- `docs/` : documentation métier et technique +- `seed/` : scripts de seed +- `tests/` : tests SQL +- `docker-compose.dev.yml` : configuration Docker pour le développement + +--- + +## Lancer la base de données en local + +Dans le terminal, depuis le dossier du projet : + +```bash +docker compose -f docker-compose.dev.yml up -d +``` + +Pour arrêter et supprimer les volumes : + +```bash +docker compose -f docker-compose.dev.yml down -v +``` + +--- + + +## Importation automatique des données de test + +Les données de test (CSV) sont automatiquement importées dans la base au démarrage du conteneur Docker grâce aux scripts présents dans le dossier `migrations/`. + +Il n'est pas nécessaire de lancer manuellement le script d'import. + +--- + +## Accéder à pgAdmin4 + +### Via Docker (local) + +Ouvre ton navigateur sur : + +``` +http://localhost:8081 +``` + +**Email** : `admin@ptits-pas.fr` +**Mot de passe** : `admin123` + +**Mot de passse pour se connecter au server local** : `admin123` + + +## Conseils et bonnes pratiques + +- Vérifie la cohérence des identifiants dans les CSV avant import +- Pour modifier la structure, utilise les scripts de migration dans `migrations/` +- Pour ajouter des scripts d'automatisation, crée un dossier `scripts/` +- Documente les étapes spécifiques dans le README ou dans `docs/` + +--- + +## Contact + +Pour toute question ou contribution, consulte la documentation ou contacte l'équipe PtitsPas. + diff --git a/database/bdd/data_test/assistantes_maternelles.csv b/database/bdd/data_test/assistantes_maternelles.csv new file mode 100644 index 0000000..6403aba --- /dev/null +++ b/database/bdd/data_test/assistantes_maternelles.csv @@ -0,0 +1,3 @@ +"id_utilisateur","numero_agrement","nir_chiffre","nb_max_enfants","biographie","disponible","ville_residence","date_agrement","annee_experience","specialite","place_disponible" +"a1f3d5c7-8b9a-4e2f-9c1d-3b2a4f6e7d8c","AGR5678",,3,"Agrément 3 enfants - Spécialité 1-3 ans - 1 place disponible",True,,"2010-09-01",14,"1-3 ans",1 +"d9c2e3f4-5b6a-4c3d-9f1a-2e7b3c5d8a1f","AGR1234",,4,"Agrément 4 enfants - Spécialité bébés 0-18 mois - 2 places disponibles",True,,"2005-06-15",18,"Bébés 0-18 mois",2 diff --git a/database/bdd/data_test/contrats.csv b/database/bdd/data_test/contrats.csv new file mode 100644 index 0000000..ad9ff5d --- /dev/null +++ b/database/bdd/data_test/contrats.csv @@ -0,0 +1,2 @@ +"id","id_dossier","planning","tarif_horaire","indemnites_repas","date_debut","statut","signe_parent","signe_am","finalise_le","cree_le","modifie_le" +"f09c6ffa-4627-4aa8-b20b-829c2c828f0d","bb9c30a0-60b4-4832-9947-8a7d2366673d","{""jours"": [""Lundi"", ""Mardi"", ""Mercredi""]}","10.50","4.50","2024-09-01","brouillon",True,False,,"2025-09-09 11:05:47.933418+00","2025-09-09 11:05:47.933418+00" diff --git a/database/bdd/data_test/dossiers.csv b/database/bdd/data_test/dossiers.csv new file mode 100644 index 0000000..8088f6d --- /dev/null +++ b/database/bdd/data_test/dossiers.csv @@ -0,0 +1,2 @@ +"id","id_parent","id_enfant","presentation","type_contrat","repas","budget","planning_souhaite","statut","cree_le","modifie_le" +"bb9c30a0-60b4-4832-9947-8a7d2366673d","f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","Contrat test pour garde à temps plein","temps_plein",False,"1200.00",,"envoye","2025-09-09 10:58:28.718654+00","2025-09-09 10:58:28.718654+00" diff --git a/database/bdd/data_test/enfants.csv b/database/bdd/data_test/enfants.csv new file mode 100644 index 0000000..1cf2ab6 --- /dev/null +++ b/database/bdd/data_test/enfants.csv @@ -0,0 +1,10 @@ +"id","statut","prenom","nom","genre","date_naissance","date_prevue_naissance","photo_url","consentement_photo","date_consentement_photo","est_multiple" +"5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","actif","Emma","Dupont","F","2020-06-01",,,False,,False +"a5c3268e-07eb-41a4-9f6c-2f9f16f37c3d","actif",,,,"2020-01-01","2025-01-01",,False,,False +"e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c","actif","Emma","Martin",,"2023-02-15",,,False,,False +"e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d","actif","Noah","Martin",,"2023-02-15",,,False,,False +"e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e","actif","Léa","Martin",,"2023-02-15",,,False,,False +"e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f","actif","Chloé","Rousseau",,"2022-04-20",,,False,,False +"e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a","actif","Hugo","Rousseau",,"2024-03-10",,,False,,False +"e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b","actif","Maxime","Lecomte",,"2023-04-15",,,False,,False +"edd19cd1-bb67-4f14-8a37-c66b75c94537","scolarise","Lucas","Durand","H","2018-09-15",,,False,,False diff --git a/database/bdd/data_test/enfants_parents.csv b/database/bdd/data_test/enfants_parents.csv new file mode 100644 index 0000000..910c800 --- /dev/null +++ b/database/bdd/data_test/enfants_parents.csv @@ -0,0 +1,9 @@ +"id_parent","id_enfant" +"b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e","e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f" +"b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e","e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a" +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f","e1a2b3c4-d5e6-4f7a-8b9c-1d2e3f4a5b6c" +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f","e2b3c4d5-e6f7-4a8b-9c1d-2e3f4a5b6c7d" +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f","e3c4d5e6-f7a8-4b9c-1d2e-3f4a5b6c7d8e" +"d3e5f7a9-1c2b-4d6e-8f3a-2b4c6d8e9f1a","e6f7a8b9-c1d2-4e3f-5a6b-7c8d9e0f1a2b" +"f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","e4d5e6f7-a8b9-4c1d-2e3f-4a5b6c7d8e9f" +"f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","e5e6f7a8-b9c1-4d2e-3f4a-5b6c7d8e9f1a" diff --git a/database/bdd/data_test/evenements.csv b/database/bdd/data_test/evenements.csv new file mode 100644 index 0000000..1a63298 --- /dev/null +++ b/database/bdd/data_test/evenements.csv @@ -0,0 +1,2 @@ +"id","type","id_enfant","id_am","id_parent","cree_par","date_debut","date_fin","commentaires","statut","delai_grace","urgent","cree_le","modifie_le" +"9f09425c-a374-4c9f-b3b1-be7258b60cd3","absence_enfant","5e8574b7-63e6-4d48-9af3-8d3bf7a6a6cf","62de8e71-8082-4383-a3a2-4277bdd07516",,"bbcae75c-0e60-4b84-b281-079dba23b44e","2024-11-10 00:00:00+00","2024-11-11 00:00:00+00","Enfant malade","propose",,True,"2025-09-02 12:55:43.781467+00","2025-09-05 14:39:38.390126+00" diff --git a/database/bdd/data_test/notifications.csv b/database/bdd/data_test/notifications.csv new file mode 100644 index 0000000..7a177c1 --- /dev/null +++ b/database/bdd/data_test/notifications.csv @@ -0,0 +1,2 @@ +"id","id_utilisateur","contenu","lu","cree_le" +"71e90c37-f2cb-4aff-ad34-1c728f620afb","bbcae75c-0e60-4b84-b281-079dba23b44e","Votre dossier a été accepté",False,"2025-09-02 12:57:42.845264+00" diff --git a/database/bdd/data_test/parents.csv b/database/bdd/data_test/parents.csv new file mode 100644 index 0000000..c3d98f7 --- /dev/null +++ b/database/bdd/data_test/parents.csv @@ -0,0 +1,5 @@ +"id_utilisateur","id_co_parent" +"b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e","f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b" +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f", +"d3e5f7a9-1c2b-4d6e-8f3a-2b4c6d8e9f1a", +"f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e" diff --git a/database/bdd/data_test/uploads.csv b/database/bdd/data_test/uploads.csv new file mode 100644 index 0000000..25709b6 --- /dev/null +++ b/database/bdd/data_test/uploads.csv @@ -0,0 +1,2 @@ +"id","id_utilisateur","fichier_url","type","cree_le" +"db1eb36d-5f30-4027-b529-1d972b79180a","bbcae75c-0e60-4b84-b281-079dba23b44e","https://placeholder.local/file","image","2025-09-02 12:57:35.140078+00" diff --git a/database/bdd/data_test/utilisateurs.csv b/database/bdd/data_test/utilisateurs.csv new file mode 100644 index 0000000..c4539ce --- /dev/null +++ b/database/bdd/data_test/utilisateurs.csv @@ -0,0 +1,12 @@ +"id","email","password","prenom","nom","genre","role","statut","telephone","adresse","photo_url","consentement_photo","date_consentement_photo","changement_mdp_obligatoire","cree_le","modifie_le","ville","code_postal","mobile","telephone_fixe","profession","situation_familiale","date_naissance" +"62de8e71-8082-4383-a3a2-4277bdd07516","am1@example.com","hash125","Claire","Martin","F","assistante_maternelle","actif","0609091011","5 place Bellecour",,False,,False,"2025-09-02 12:30:48.724463+00","2025-09-02 12:30:48.724463+00","Lyon","69002",,,,, +"76c40571-5da6-4d27-8e07-303185875b36","gest1@example.com","hash126","Paul","Lemoine","H","gestionnaire","actif","0612131415","10 rue Victor Hugo",,False,,False,"2025-09-02 12:30:48.724463+00","2025-09-02 12:30:48.724463+00","Paris","75002",,,,, +"9bc30373-91a4-45ed-9c05-ffe1651ad906","coparent@example.com","hash124","Marc","Durand","H","parent","actif","0605060708","45 avenue de Lyon",,False,,False,"2025-09-02 12:30:48.724463+00","2025-09-02 12:30:48.724463+00","Lyon","69000",,,,, +"a1f3d5c7-8b9a-4e2f-9c1d-3b2a4f6e7d8c","fatima.elmansouri@ptits-pas.fr","password","Fatima","El Mansouri",,"assistante_maternelle","actif",,"17 Boulevard Aristide Briand",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 75 45 67 89","01 39 98 78 90","Assistante maternelle","marie","1975-11-12" +"b6c4d2e3-5f7a-4b8c-9d1e-2a3c5f7b8d9e","julien.rousseau@ptits-pas.fr","password","Julien","Rousseau",,"parent","actif",,"14 Rue Pasteur",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 56 67 78 89","01 39 98 01 23","Commercial","divorce","1985-08-29" +"bbcae75c-0e60-4b84-b281-079dba23b44e","parent1@example.com","hash123","Alice","Dupont","F","parent","actif","0601020304","12 rue de Paris",,False,,False,"2025-09-02 12:30:48.724463+00","2025-09-02 12:30:48.724463+00","Paris","75001",,,,, +"c4e2d1f5-6b7a-4c3d-8f2a-1e9c3b5a7d6f","claire.martin@ptits-pas.fr","password","Claire","Martin",,"parent","actif",,"5 Avenue du Général de Gaulle",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 89 56 78 90","01 39 98 89 01","Infirmière","marie","1990-04-03" +"d3e5f7a9-1c2b-4d6e-8f3a-2b4c6d8e9f1a","david.lecomte@ptits-pas.fr","password","David","Lecomte",,"parent","actif",,"31 Rue Émile Zola",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 45 56 67 78","01 39 98 12 34","Développeur web","celibataire","1992-10-07" +"d9c2e3f4-5b6a-4c3d-9f1a-2e7b3c5d8a1f","marie.dubois@ptits-pas.fr","password","Marie","Dubois",,"assistante_maternelle","actif",,"25 Rue de la République",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 12:44:51.654512+00","Bezons","95870","06 96 34 56 78","01 39 98 67 89","Assistante maternelle","marie","1980-06-08" +"f1d3c5b7-8a9e-4f2d-9c1b-3e7a5d8c2f1b","amelie.durand@ptits-pas.fr","password","Amélie","Durand",,"parent","actif",,"23 Rue Victor Hugo",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 67 78 89 90","01 39 98 90 12","Comptable","divorce","1987-12-14" +"f3b1a2d4-1c7e-4c2f-8b1a-9d3a8e2f5b6c","sophie.bernard@ptits-pas.fr","password","Sophie","Bernard",,"administrateur","actif",,"12 Avenue Gabriel Péri",,False,,False,"2025-09-04 11:49:22.636003+00","2025-09-04 11:49:22.636003+00","Bezons","95870","06 78 12 34 56","01 39 98 45 67","Responsable administrative","marie","1978-03-15" diff --git a/database/bdd/data_test/validations.csv b/database/bdd/data_test/validations.csv new file mode 100644 index 0000000..a93c5da --- /dev/null +++ b/database/bdd/data_test/validations.csv @@ -0,0 +1,4 @@ +"id","id_utilisateur","type","statut","cree_le","modifie_le","valide_par","commentaire" +"8ec99565-e8c2-469f-9641-01b99b8281eb","62de8e71-8082-4383-a3a2-4277bdd07516","identité","valide","2025-09-02 12:57:49.846424+00","2025-09-02 12:57:49.846424+00",, +"be1c4779-341b-436d-b17e-8bc486d22ef8","62de8e71-8082-4383-a3a2-4277bdd07516",,"valide","2025-09-09 14:47:01.963573+00","2025-09-09 14:47:01.963573+00",,"Contrôle OK" +"fcc45701-5708-4368-b467-b95ddb7e1580","d9c2e3f4-5b6a-4c3d-9f1a-2e7b3c5d8a1f",,"valide","2025-09-09 14:52:47.339858+00","2025-09-09 14:52:47.339858+00","76c40571-5da6-4d27-8e07-303185875b36","Contrôle OK" diff --git a/database/bdd/schemas/bdd_simple.png b/database/bdd/schemas/bdd_simple.png new file mode 100644 index 0000000..8983acc Binary files /dev/null and b/database/bdd/schemas/bdd_simple.png differ diff --git a/database/bdd/schemas/bdd_type.png b/database/bdd/schemas/bdd_type.png new file mode 100644 index 0000000..69af700 Binary files /dev/null and b/database/bdd/schemas/bdd_type.png differ diff --git a/database/docker-compose.dev.yml b/database/docker-compose.dev.yml new file mode 100644 index 0000000..6c608be --- /dev/null +++ b/database/docker-compose.dev.yml @@ -0,0 +1,46 @@ +# Docker Compose pour développement local de la base de données uniquement +# Usage: docker compose -f docker-compose.dev.yml up -d + +services: + # Base de données PostgreSQL + postgres: + image: postgres:17 + container_name: ptitspas-postgres-standalone + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-admin} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123} + POSTGRES_DB: ${POSTGRES_DB:-ptitpas_db} + ports: + - "5433:5432" + volumes: + - ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql + - ./migrations/07_import.sql:/docker-entrypoint-initdb.d/07_import.sql + - ./bdd/data_test:/bdd/data_test + - postgres_standalone_data:/var/lib/postgresql/data + networks: + - ptitspas_dev + + # Interface d'administration DB + pgadmin: + image: dpage/pgadmin4:9.8 + container_name: ptitspas-pgadmin-standalone + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@ptits-pas.fr} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin123} + ports: + - "8081:80" + depends_on: + - postgres + networks: + - ptitspas_dev + volumes: + - ./pgadmin/servers.json:/pgadmin4/servers.json + +volumes: + postgres_standalone_data: + +networks: + ptitspas_dev: + driver: bridge diff --git a/database/docker-compose.yml b/database/docker-compose.yml new file mode 100644 index 0000000..d6babdc --- /dev/null +++ b/database/docker-compose.yml @@ -0,0 +1,42 @@ +services: + db: + image: postgres:17 + container_name: ynov-postgres + restart: unless-stopped + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin123 + POSTGRES_DB: ptitpas_db + volumes: + - ./migrations/01_init.sql:/docker-entrypoint-initdb.d/01_init.sql + - postgres_data:/var/lib/postgresql/data + networks: + - ynov_network + + pgadmin: + image: dpage/pgadmin4 + container_name: ynov-pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: admin@ptits-pas.fr + PGADMIN_DEFAULT_PASSWORD: admin123 + depends_on: + - db + labels: + - "traefik.enable=true" + - "traefik.http.routers.ynov-pgadmin.rule=Host(\"ynov-pgadmin.ptits-pas.fr\")" + - "traefik.http.routers.ynov-pgadmin.entrypoints=websecure" + - "traefik.http.routers.ynov-pgadmin.tls.certresolver=leresolver" + - "traefik.http.services.ynov-pgadmin.loadbalancer.server.port=80" + networks: + - ynov_network + - proxy_network + +volumes: + postgres_data: + +networks: + ynov_network: + driver: bridge + proxy_network: + external: true diff --git a/database/docs/ENUMS.md b/database/docs/ENUMS.md new file mode 100644 index 0000000..c24e516 --- /dev/null +++ b/database/docs/ENUMS.md @@ -0,0 +1,157 @@ + +# ENUMS.md — Référentiel des valeurs énumérées (Sprint 1) + +Ce document recense **toutes les valeurs énumérées** utilisées dans la base P’titsPas, leur **sens fonctionnel**, les **colonnes concernées** et les **transitions** attendues côté métier / API. + +> Objectif : garantir la **cohérence** entre la DB, le backend (NestJS) et le frontend (Flutter). +> Toute nouvelle valeur ou renommage **doit** être ajouté ici **avant** migration DB. + +--- + +## Conventions générales + +- Les valeurs ENUM sont **en minuscules** et **sans espace** (snake_case si nécessaire). +- Côté DB, elles sont implémentées via **types ENUM PostgreSQL** *ou* via `CHECK` (selon ce qui est en place dans `01_init.sql`). +- Côté API, ces valeurs sont **renvoyées telles quelles** et **documentées** dans l’OpenAPI / DTO. + +--- + +## 1) Rôle utilisateur — `role` + +**Tables/colonnes** : `utilisateurs.role` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `super_admin` | Compte technique initial / administration globale | +| `gestionnaire` | Gestion / validation des comptes, supervision | +| `parent` | Parent ou co-parent | +| `am` | Assistante maternelle | + +--- + +## 2) Statut utilisateur — `statut` + +**Tables/colonnes** : `utilisateurs.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `en_attente` | Compte créé mais non validé | +| `accepte` | Compte validé et actif | +| `rejete` | Demande refusée (peut être recréée ultérieurement) | + +--- + +## 3) Statut enfant — `statut` + +**Tables/colonnes** : `enfants.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `a_naitre` | Enfant à naître (date prévue renseignée) | +| `actif` | Enfant pris en charge / en cours de garde | +| `scolarise` | Enfant scolarisé, garde potentiellement périscolaire | + +**Contraintes associées** : +- `a_naitre` → **`date_prevue_naissance` obligatoire** +- `actif`/`scolarise` → **`date_naissance` obligatoire** + +--- + +## 4) Statut dossier — `statut` + +**Tables/colonnes** : `dossiers.statut` +**Valeurs autorisées (MVP)** : + +| Valeur | Description | +|---|---| +| `envoye` | Dossier soumis par le parent (état initial) | +| `en_cours` | Échanges en cours entre parent et AM | +| `clos` | Dossier clôturé (contrat généré ou abandon) | + +--- + +## 5) Statut contrat — `statut` + +**Tables/colonnes** : `contrats.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `brouillon` | Contrat en préparation | +| `valide` | Contrat finalisé (signatures complètes) | +| `archive` | Contrat obsolète / terminé | + +--- + +## 6) Statut avenant — `statut` + +**Tables/colonnes** : `avenants_contrats.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `propose` | Avenant proposé (en attente d’accord) | +| `valide` | Avenant accepté et appliqué | +| `rejete` | Avenant refusé | + +--- + +## 7) Type d’événement — `type` + +**Tables/colonnes** : `evenements.type` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `absence_enfant` | Enfant absent | +| `conge_am` | Congé de l’assistante maternelle | +| `conge_parent` | Congé du parent | +| `arret_maladie_am` | Arrêt maladie AM | +| `evenement_rpe` | Événement RPE | + +--- + +## 8) Statut d’événement — `statut` + +**Tables/colonnes** : `evenements.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `propose` | Événement proposé | +| `valide` | Événement validé | +| `rejete` | Événement refusé | + +--- + +## 9) Statut de validation compte — `statut` + +**Tables/colonnes** : `validations.statut` +**Valeurs autorisées** : + +| Valeur | Description | +|---|---| +| `accepte` | Compte validé | +| `rejete` | Compte refusé | + +--- + +## 10) Type de notification — `type` + +**Tables/colonnes** : `notifications.type` +**Valeurs proposées** : + +| Valeur | Description | +|---|---| +| `nouveau_message` | Nouveau message sur un dossier | +| `validation_compte` | Résultat de la validation de compte | +| `maj_contrat` | Contrat mis à jour (avenant / signature) | +| `evenement_a_venir` | Rappel d’un événement proche | + +--- + +**Mainteneur** : Équipe BDD +**Dernière mise à jour** : Sprint 1 — Ticket 9 (ENUMS) diff --git a/database/docs/FK_POLICIES.md b/database/docs/FK_POLICIES.md new file mode 100644 index 0000000..3be63b3 --- /dev/null +++ b/database/docs/FK_POLICIES.md @@ -0,0 +1,142 @@ + +# FK_POLICIES.md +**Politique des clés étrangères (ON DELETE / ON UPDATE)** – Sprint 1 + +## 🎯 Objectif +Documenter, de façon unique et partagée, les règles de suppression/mise à jour appliquées aux **clés étrangères** de la base P’titsPas pour : +- préserver l’**intégrité référentielle** ; +- conserver l’**historique** utile (messages, événements…) ; +- respecter les exigences **RGPD** (suppression en cascade lorsque pertinent). + +> Par défaut, **ON UPDATE = NO ACTION** (UUID immuables). +> Ce document couvre **ON DELETE** table par table. + +--- + +## 🧭 Principes généraux + +- **CASCADE** quand la donnée fille **n’a pas de sens sans le parent** + (ex. `dossiers` d’un parent, `avenants` d’un contrat). +- **SET NULL** quand on veut **préserver l’historique** mais que le référent peut disparaître + (ex. auteur d’un message supprimé, créateur d’un événement). +- **RESTRICT/NO ACTION** non utilisé ici pour éviter des blocages au nettoyage. + +--- + +## 📚 Récapitulatif rapide (matrice) + +| Table (colonne FK) → Référence | ON DELETE | Raison | +|---|---:|---| +| **assistantes_maternelles(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Profil AM supprimé avec son compte | +| **parents(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Extension parent supprimée avec son compte | +| **parents(id_co_parent)** → `utilisateurs(id)` | **SET NULL** | Conserver le parent principal si co-parent disparaît | +| **enfants_parents(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Nettoyage liaisons N:N | +| **enfants_parents(id_enfant)** → `enfants(id)` | **CASCADE** | Idem | +| **dossiers(id_parent)** → `parents(id_utilisateur)` | **CASCADE** | Dossier n’a pas de sens sans parent | +| **dossiers(id_enfant)** → `enfants(id)` | **CASCADE** | Dossier n’a pas de sens sans enfant | +| **messages(id_dossier)** → `dossiers(id)` | **CASCADE** | Messages détruits avec le dossier | +| **messages(id_expediteur)** → `utilisateurs(id)` | **SET NULL** | Garder l’historique des échanges | +| **contrats(id_dossier)** → `dossiers(id)` | **CASCADE** | 1:1, contrat détruit si dossier supprimé | +| **avenants_contrats(id_contrat)** → `contrats(id)` | **CASCADE** | Avenants détruits avec le contrat | +| **avenants_contrats(initie_par)** → `utilisateurs(id)` | **SET NULL** | Historiser l’avenant sans bloquer | +| **evenements(id_enfant)** → `enfants(id)` | **CASCADE** | Événements n’ont plus de sens | +| **evenements(id_am)** → `utilisateurs(id)` | **SET NULL** | Garder la trace même si AM supprimée | +| **evenements(id_parent)** → `parents(id_utilisateur)` | **SET NULL** | Garder la trace si parent supprimé | +| **evenements(cree_par)** → `utilisateurs(id)` | **SET NULL** | Conserver l’historique de création | +| **signalements_bugs(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Conserver le ticket même si compte supprimé | +| **uploads(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Fichier reste référencé sans l’auteur | +| **notifications(id_utilisateur)** → `utilisateurs(id)` | **CASCADE** | Notifications propres à l’utilisateur | +| **validations(id_utilisateur)** → `utilisateurs(id)` | **SET NULL** | Garder l’historique de décision | + +> **ON UPDATE** : **NO ACTION** partout (les UUID ne changent pas). + +--- + +## 🔎 Détail par domaine + +### Utilisateurs & extensions +- `assistantes_maternelles.id_utilisateur` → **CASCADE** +- `parents.id_utilisateur` → **CASCADE** +- `parents.id_co_parent` → **SET NULL** (interdit d’être co-parent de soi-même via CHECK déjà posé) + +### Enfants & liaisons +- `enfants_parents.id_parent` → **CASCADE** +- `enfants_parents.id_enfant` → **CASCADE** + +### Dossiers & échanges +- `dossiers.id_parent` → **CASCADE** +- `dossiers.id_enfant` → **CASCADE** +- `messages.id_dossier` → **CASCADE** +- `messages.id_expediteur` → **SET NULL** + +### Contrats & avenants +- `contrats.id_dossier` → **CASCADE** (unique 1:1) +- `avenants_contrats.id_contrat` → **CASCADE** +- `avenants_contrats.initie_par` → **SET NULL** + +### Événements +- `evenements.id_enfant` → **CASCADE** +- `evenements.id_am` → **SET NULL** +- `evenements.id_parent` → **SET NULL** +- `evenements.cree_par` → **SET NULL** + +### Divers +- `signalements_bugs.id_utilisateur` → **SET NULL** +- `uploads.id_utilisateur` → **SET NULL** +- `notifications.id_utilisateur` → **CASCADE** +- `validations.id_utilisateur` → **SET NULL** + +--- + +## 🧪 Scénarios de test (exemples) + +1. **Suppression d’un parent** + - Supprimer `utilisateurs(id=parentX)` + - Attendu : `parents` (CASCADE), ses `dossiers` (CASCADE), `messages` liés aux `dossiers` (CASCADE) sont supprimés. + +2. **Suppression d’un co-parent** + - Supprimer `utilisateurs(id=coParentY)` + - Attendu : `parents.id_co_parent` passe à **NULL**, aucun dossier supprimé. + +3. **Suppression d’un utilisateur auteur de messages** + - Supprimer `utilisateurs(id=uZ)` + - Attendu : les lignes `messages` **restent**, `id_expediteur` devient **NULL**. + +4. **Suppression d’un enfant** + - Supprimer `enfants(id=childA)` + - Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `evenements` du childA (CASCADE). + +5. **Suppression d’un utilisateur AM** + - Supprimer `utilisateurs(id=amB)` + - Attendu : `evenements.id_am` devient **NULL** (historique conservé). + +--- + +## 🛠 Migrations associées + +Les ajustements sont implémentés dans : +- **`/bdd/migrations/04_fk_policies.sql`** + – redéfinition des contraintes FK avec les bonnes politiques (**DROP puis ADD CONSTRAINT**), de façon idempotente. + +--- + +## 📄 Notes & futures évolutions + +- **RGPD (Sprint 2)** : si vous activez le **soft delete** (`deleted_at`) côté tables métier, ces politiques restent valides (les suppressions logiques se gèrent au niveau applicatif). +- **Audit** : si vous voulez tracer les suppressions, ajoutez des triggers d’audit (voir ticket Sprint 2 – Audit log). +- **Performance** : chaque FK doit être **indexée côté enfant** (cf. `02_indexes.sql`). + +--- + +## ✅ Checklist de conformité + +- [ ] Toutes les FK listées existent dans la base +- [ ] Politique **ON DELETE** conforme au tableau ci-dessus +- [ ] **ON UPDATE = NO ACTION** partout +- [ ] Tests de suppression réalisés sur une base seedée +- [ ] `04_fk_policies.sql` appliqué sans erreur + +--- + +**Mainteneur** : Équipe BDD +**Dernière mise à jour** : Sprint 1 – Politique FK consolidée diff --git a/database/pgadmin/servers.json b/database/pgadmin/servers.json new file mode 100644 index 0000000..b9f8a5e --- /dev/null +++ b/database/pgadmin/servers.json @@ -0,0 +1,13 @@ +{ +"Servers": { + "1": { + "Name": "Postgres Dev", + "Group": "Local", + "Host": "postgres", + "Port": 5432, + "Username": "admin", + "SSLMode": "prefer", + "MaintenanceDB": "ptitpas_db" + } + } +} diff --git a/database/seed/02_seed.sql b/database/seed/02_seed.sql new file mode 100644 index 0000000..c8ef3b4 --- /dev/null +++ b/database/seed/02_seed.sql @@ -0,0 +1,221 @@ +-- ============================================================ +-- 02_seed.sql : Données de test réalistes (Sprint 1) +-- A exécuter après : +-- 01_init.sql (création des tables) +-- 02_indexes.sql +-- 03_checks.sql +-- 04_fk_policies.sql +-- 05_triggers.sql +-- ============================================================ + +BEGIN; + +-- ------------------------------------------------------------ +-- Utilisateurs (super_admin, gestionnaire, 2 parents + co-parent, 1 AM) +-- ------------------------------------------------------------ + +-- UUIDs fixes pour faciliter les tests / jointures +-- super_admin +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('11111111-1111-1111-1111-111111111111', 'admin@ptits-pas.fr', '$2y$10$hashAdminIci', 'Super', 'Admin', 'super_admin', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- gestionnaire +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('22222222-2222-2222-2222-222222222222', 'gestion@ptits-pas.fr', '$2y$10$hashGestionIci', 'Gina', 'Gestion', 'gestionnaire', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- parent #1 +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('33333333-3333-3333-3333-333333333333', 'parent1@example.com', '$2y$10$hashParent1', 'Paul', 'Parent', 'parent', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- co-parent du parent #1 +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('44444444-4444-4444-4444-444444444444', 'coparent1@example.com', '$2y$10$hashCoParent1', 'Clara', 'CoParent', 'parent', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- parent #2 +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('55555555-5555-5555-5555-555555555555', 'parent2@example.com', '$2y$10$hashParent2', 'Nora', 'Parent', 'parent', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- assistante maternelle #1 +INSERT INTO utilisateurs (id, courriel, mot_de_passe_hash, prenom, nom, role, statut) +VALUES ('66666666-6666-6666-6666-666666666666', 'am1@example.com', '$2y$10$hashAM1', 'Alice', 'AM', 'am', 'accepte') +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Extensions de rôles (parents / AM) +-- ------------------------------------------------------------ + +-- parents (id_co_parent nullable) +INSERT INTO parents (id_utilisateur, id_co_parent) +VALUES ('33333333-3333-3333-3333-333333333333', '44444444-4444-4444-4444-444444444444') -- parent1 avec co-parent +ON CONFLICT (id_utilisateur) DO NOTHING; + +INSERT INTO parents (id_utilisateur, id_co_parent) +VALUES ('55555555-5555-5555-5555-555555555555', NULL) -- parent2 sans co-parent +ON CONFLICT (id_utilisateur) DO NOTHING; + +-- assistantes_maternelles +INSERT INTO assistantes_maternelles (id_utilisateur, numero_agrement, nb_max_enfants, disponible, ville_residence) +VALUES ('66666666-6666-6666-6666-666666666666', 'AGR-2025-0001', 3, true, 'Lille') +ON CONFLICT (id_utilisateur) DO NOTHING; + +-- ------------------------------------------------------------ +-- Enfants +-- - child A : déjà né (statut = 'actif' et date_naissance requise) +-- - child B : à naître (statut = 'a_naitre' et date_prevue_naissance requise) +-- ------------------------------------------------------------ + +INSERT INTO enfants (id, prenom, nom, statut, date_naissance, jumeau_multiple) +VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Léo', 'Parent', 'actif', '2022-04-12', false) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO enfants (id, prenom, nom, statut, date_prevue_naissance, jumeau_multiple) +VALUES ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'Mila', 'Parent', 'a_naitre', '2026-02-15', false) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Liaison N:N parents_enfants +-- - parent1 + co-parent ↔ enfant A & B +-- - parent2 ↔ enfant B +-- ------------------------------------------------------------ + +-- parent1 ↔ enfant A +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES ('33333333-3333-3333-3333-333333333333', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') +ON CONFLICT DO NOTHING; + +-- co-parent1 ↔ enfant A +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES ('44444444-4444-4444-4444-444444444444', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') +ON CONFLICT DO NOTHING; + +-- parent1 ↔ enfant B +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES ('33333333-3333-3333-3333-333333333333', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb') +ON CONFLICT DO NOTHING; + +-- parent2 ↔ enfant B +INSERT INTO enfants_parents (id_parent, id_enfant) +VALUES ('55555555-5555-5555-5555-555555555555', 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb') +ON CONFLICT DO NOTHING; + +-- ------------------------------------------------------------ +-- Dossier (parent1 ↔ enfant A) +-- ------------------------------------------------------------ +INSERT INTO dossiers (id, id_parent, id_enfant, presentation, type_contrat, repas, budget, planning_souhaite) +VALUES ( + 'dddddddd-dddd-dddd-dddd-dddddddddddd', + '33333333-3333-3333-3333-333333333333', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'Besoin garde périscolaire lundi/mardi/jeudi/vendredi.', + 'mensuel', + true, + 600.00, + '{"lun_ven":{"matin":false,"midi":true,"soir":true}}'::jsonb +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Messages (sur le dossier) +-- ------------------------------------------------------------ +INSERT INTO messages (id, id_dossier, id_expediteur, contenu) +VALUES ('m0000000-0000-0000-0000-000000000001', 'dddddddd-dddd-dddd-dddd-dddddddddddd', '33333333-3333-3333-3333-333333333333', 'Bonjour, nous cherchons une garde périscolaire.') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO messages (id, id_dossier, id_expediteur, contenu) +VALUES ('m0000000-0000-0000-0000-000000000002', 'dddddddd-dddd-dddd-dddd-dddddddddddd', '66666666-6666-6666-6666-666666666666', 'Bonjour, je suis disponible les soirs. Discutons du contrat.') +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Contrat (1:1 avec le dossier) +-- ------------------------------------------------------------ +INSERT INTO contrats (id, id_dossier, planning, tarif_horaire, indemnites_repas, date_debut, statut, signe_parent, signe_am) +VALUES ( + 'cccccccc-cccc-cccc-cccc-cccccccccccc', + 'dddddddd-dddd-dddd-dddd-dddddddddddd', + '{"lun_ven":{"17h-19h":true}}'::jsonb, + 12.50, + 3.50, + '2025-09-01', + 'brouillon', + false, + false +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Avenant de contrat +-- ------------------------------------------------------------ +INSERT INTO avenants_contrats (id, id_contrat, modifications, initie_par, statut) +VALUES ( + 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', + 'cccccccc-cccc-cccc-cccc-cccccccccccc', + '{"changement_horaire":{"vendredi":{"17h-20h":true}}}'::jsonb, + '33333333-3333-3333-3333-333333333333', + 'propose' +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Événement (absence enfant) +-- ------------------------------------------------------------ +INSERT INTO evenements (id, type, id_enfant, id_am, id_parent, cree_par, date_debut, date_fin, commentaires, statut, urgence) +VALUES ( + 'e0000000-0000-0000-0000-000000000001', + 'absence_enfant', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '66666666-6666-6666-6666-666666666666', + '33333333-3333-3333-3333-333333333333', + '33333333-3333-3333-3333-333333333333', + '2025-09-12', + '2025-09-12', + 'Enfant malade (rhume).', + 'propose', + false +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Upload (justificatif lié au dossier) +-- ------------------------------------------------------------ +INSERT INTO uploads (id, id_utilisateur, id_dossier_lie, fichier_url, type_fichier) +VALUES ( + 'u0000000-0000-0000-0000-000000000001', + '33333333-3333-3333-3333-333333333333', + 'dddddddd-dddd-dddd-dddd-dddddddddddd', + '/uploads/justificatifs/dossier_dddddddd_attestation.pdf', + 'application/pdf' +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Notification (pour le parent1) +-- ------------------------------------------------------------ +INSERT INTO notifications (id, id_utilisateur, type, contenu, lu) +VALUES ( + 'n0000000-0000-0000-0000-000000000001', + '33333333-3333-3333-3333-333333333333', + 'nouveau_message', + 'Vous avez un nouveau message sur le dossier #dddd…', + false +) +ON CONFLICT (id) DO NOTHING; + +-- ------------------------------------------------------------ +-- Validation (compte de l’AM validé par le gestionnaire) +-- ------------------------------------------------------------ +INSERT INTO validations (id, id_utilisateur, statut, commentaire, cree_le) +VALUES ( + 'v0000000-0000-0000-0000-000000000001', + '66666666-6666-6666-6666-666666666666', + 'accepte', + 'Dossier AM vérifié par gestionnaire.', + NOW() +) +ON CONFLICT (id) DO NOTHING; + +COMMIT; diff --git a/database/tests/sql/verify.sql b/database/tests/sql/verify.sql new file mode 100644 index 0000000..05593dd --- /dev/null +++ b/database/tests/sql/verify.sql @@ -0,0 +1,205 @@ + +-- ============================================================ +-- verify.sql — Jeux de requêtes de vérification (Sprint 1) +-- Objectifs : +-- 1) Vérifier l'intégrité fonctionnelle (joins, données seedées) +-- 2) Détecter rapidement des problèmes d'index/perf (EXPLAIN) +-- 3) Servir de référence pour le back/front (requêtes typiques) +-- +-- Usage (Docker) : +-- docker compose exec -T postgres \ +-- psql -U ${POSTGRES_USER} -d ${POSTGRES_DB} \ +-- -f /docker-entrypoint-initdb.d/tests/verify.sql +-- +-- Pré-requis : +-- - 01_init.sql +-- - 02_indexes.sql +-- - 03_checks.sql +-- - 04_fk_policies.sql +-- - 05_triggers.sql +-- - 02_seed.sql (pour résultats non vides) +-- ============================================================ + +\echo '=== 0) Version & horodatage ====================================' +SELECT version(); +SELECT NOW() AS executed_at; + +\echo '=== 1) Comptes & répartition par rôle ==========================' +SELECT role, COUNT(*) AS nb +FROM utilisateurs +GROUP BY role +ORDER BY nb DESC; + +\echo '=== 2) Utilisateurs en attente / acceptés / rejetés ============' +SELECT statut, COUNT(*) AS nb +FROM utilisateurs +GROUP BY statut +ORDER BY nb DESC; + +\echo '=== 3) Parents avec co-parents (NULL si pas de co-parent) ======' +SELECT p.id_utilisateur AS parent_id, + u.courriel AS parent_email, + p.id_co_parent, + uc.courriel AS co_parent_email +FROM parents p +JOIN utilisateurs u ON u.id = p.id_utilisateur +LEFT JOIN utilisateurs uc ON uc.id = p.id_co_parent +ORDER BY parent_email; + +\echo '=== 4) Enfants (statut, dates cohérentes) ======================' +SELECT id, prenom, nom, statut, date_naissance, date_prevue_naissance +FROM enfants +ORDER BY nom, prenom; + +\echo '=== 5) Liaison N:N parents_enfants =============================' +SELECT ep.id_parent, up.courriel AS parent_email, ep.id_enfant, e.prenom AS enfant +FROM enfants_parents ep +JOIN utilisateurs up ON up.id = ep.id_parent +JOIN enfants e ON e.id = ep.id_enfant +ORDER BY parent_email, enfant; + +\echo '=== 6) Dossiers (parent, enfant, statut) =======================' +SELECT d.id, up.courriel AS parent_email, e.prenom AS enfant, d.statut, d.budget +FROM dossiers d +JOIN utilisateurs up ON up.id = d.id_parent +JOIN enfants e ON e.id = d.id_enfant +ORDER BY d.cree_le DESC; + +\echo '=== 7) Messages par dossier (ordre chronologique) ==============' +SELECT m.id, m.id_dossier, m.id_expediteur, ue.courriel AS expediteur_email, m.contenu, m.cree_le +FROM messages m +LEFT JOIN utilisateurs ue ON ue.id = m.id_expediteur +ORDER BY m.id_dossier, m.cree_le; + +\echo '=== 8) Contrats 1:1 avec dossier + avenants ====================' +SELECT c.id AS contrat_id, c.id_dossier, c.statut, + COUNT(a.id) AS nb_avenants +FROM contrats c +LEFT JOIN avenants_contrats a ON a.id_contrat = c.id +GROUP BY c.id, c.id_dossier, c.statut +ORDER BY c.cree_le DESC; + +\echo '=== 9) Evénements par enfant (30 derniers jours) ==============' +SELECT ev.id, ev.type, ev.id_enfant, e.prenom AS enfant, ev.date_debut, ev.date_fin, ev.statut +FROM evenements ev +JOIN enfants e ON e.id = ev.id_enfant +WHERE ev.date_debut >= (NOW()::date - INTERVAL '30 days') +ORDER BY ev.date_debut DESC; + +\echo '=== 10) Uploads & notifications récentes =======================' +SELECT u.courriel, up.fichier_url, up.type_fichier, up.cree_le +FROM uploads up +LEFT JOIN utilisateurs u ON u.id = up.id_utilisateur +ORDER BY up.cree_le DESC; + +SELECT u.courriel, n.type, n.contenu, n.lu, n.cree_le +FROM notifications n +LEFT JOIN utilisateurs u ON u.id = n.id_utilisateur +ORDER BY n.cree_le DESC; + +\echo '=== 11) Validations (qui a validé quoi) ========================' +SELECT v.id, uu.courriel AS utilisateur_valide, + uv.courriel AS valide_par, v.statut, v.commentaire, v.cree_le +FROM validations v +LEFT JOIN utilisateurs uu ON uu.id = v.id_utilisateur +LEFT JOIN utilisateurs uv ON uv.id = v.valide_par +ORDER BY v.cree_le DESC; + +-- ============================================================ +-- Vérifications d'intégrité (requêtes de contrôle) +-- ============================================================ +\echo '=== 12) Orphelins potentiels (doivent renvoyer 0 ligne) =======' + +-- Messages orphelins (dossier manquant) +SELECT m.* +FROM messages m +LEFT JOIN dossiers d ON d.id = m.id_dossier +WHERE d.id IS NULL; + +-- Liaisons enfants_parents orphelines +SELECT ep.* +FROM enfants_parents ep +LEFT JOIN parents p ON p.id_utilisateur = ep.id_parent +LEFT JOIN enfants e ON e.id = ep.id_enfant +WHERE p.id_utilisateur IS NULL OR e.id IS NULL; + +-- Contrats sans dossier +SELECT c.* +FROM contrats c +LEFT JOIN dossiers d ON d.id = c.id_dossier +WHERE d.id IS NULL; + +-- Avenants sans contrat +SELECT a.* +FROM avenants_contrats a +LEFT JOIN contrats c ON c.id = a.id_contrat +WHERE c.id IS NULL; + +-- Evénements sans enfant +SELECT ev.* +FROM evenements ev +LEFT JOIN enfants e ON e.id = ev.id_enfant +WHERE e.id IS NULL; + +\echo '=== 13) Performance : EXPLAIN sur requêtes clés ===============' + +-- Messages par dossier (doit utiliser idx_messages_id_dossier_cree_le) +EXPLAIN ANALYZE +SELECT m.* +FROM messages m +WHERE m.id_dossier = 'dddddddd-dddd-dddd-dddd-dddddddddddd' +ORDER BY m.cree_le DESC +LIMIT 20; + +-- Evénements par enfant et période (idx_evenements_id_enfant_date_debut) +EXPLAIN ANALYZE +SELECT ev.* +FROM evenements ev +WHERE ev.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' + AND ev.date_debut >= '2025-01-01'; + +-- Notifications non lues (idx_notifications_user_lu_cree_le) +EXPLAIN ANALYZE +SELECT n.* +FROM notifications n +WHERE n.id_utilisateur = '33333333-3333-3333-3333-333333333333' + AND n.lu = false +ORDER BY n.cree_le DESC +LIMIT 20; + +-- Dossiers par parent/enfant (idx_dossiers_id_parent/id_enfant/statut) +EXPLAIN ANALYZE +SELECT d.* +FROM dossiers d +WHERE d.id_parent = '33333333-3333-3333-3333-333333333333' + AND d.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +ORDER BY d.cree_le DESC; + +\echo '=== 14) JSONB : exemples de filtrage ===========================' +-- Recherche de dossiers où planning_souhaite contient midi=true un jour ouvré +-- (Index GIN recommandé si usage intensif : cf. 02_indexes.sql) +SELECT d.id, d.planning_souhaite +FROM dossiers d +WHERE d.planning_souhaite @> '{"lun_ven":{"midi":true}}'; + +-- Contrats : présence d’un créneau donné +SELECT c.id, c.planning +FROM contrats c +WHERE c.planning @> '{"lun_ven":{"17h-19h":true}}'; + +\echo '=== 15) Sanity check final ====================================' +-- Quelques totaux utiles +SELECT + (SELECT COUNT(*) FROM utilisateurs) AS nb_utilisateurs, + (SELECT COUNT(*) FROM parents) AS nb_parents, + (SELECT COUNT(*) FROM assistantes_maternelles) AS nb_am, + (SELECT COUNT(*) FROM enfants) AS nb_enfants, + (SELECT COUNT(*) FROM enfants_parents) AS nb_liens_parent_enfant, + (SELECT COUNT(*) FROM dossiers) AS nb_dossiers, + (SELECT COUNT(*) FROM messages) AS nb_messages, + (SELECT COUNT(*) FROM contrats) AS nb_contrats, + (SELECT COUNT(*) FROM avenants_contrats) AS nb_avenants, + (SELECT COUNT(*) FROM evenements) AS nb_evenements, + (SELECT COUNT(*) FROM uploads) AS nb_uploads, + (SELECT COUNT(*) FROM notifications) AS nb_notifications, + (SELECT COUNT(*) FROM validations) AS nb_validations; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1e97b62 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,100 @@ +services: + # Base de données PostgreSQL + database: + image: postgres:17 + container_name: ptitspas-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - ./database/BDD.sql:/docker-entrypoint-initdb.d/01_init.sql + - postgres_data:/var/lib/postgresql/data + networks: + - ptitspas_network + + # Interface d'administration DB + pgadmin: + image: dpage/pgadmin4 + container_name: ptitspas-pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} + SCRIPT_NAME: /pgadmin + depends_on: + - database + labels: + - "traefik.enable=true" + - "traefik.http.routers.ptitspas-pgadmin.rule=Host(\"app.ptits-pas.fr\") && PathPrefix(\"/pgadmin\")" + - "traefik.http.routers.ptitspas-pgadmin.entrypoints=websecure" + - "traefik.http.routers.ptitspas-pgadmin.tls.certresolver=leresolver" + - "traefik.http.routers.ptitspas-pgadmin.priority=30" + - "traefik.http.services.ptitspas-pgadmin.loadbalancer.server.port=80" + networks: + - ptitspas_network + - proxy_network + + # Backend NestJS + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: ptitspas-backend + restart: unless-stopped + environment: + POSTGRES_HOST: ${POSTGRES_HOST} + POSTGRES_PORT: ${POSTGRES_PORT} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + API_PORT: ${API_PORT} + JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET} + JWT_ACCESS_EXPIRES: ${JWT_ACCESS_EXPIRES} + JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} + JWT_REFRESH_EXPIRES: ${JWT_REFRESH_EXPIRES} + NODE_ENV: ${NODE_ENV} + depends_on: + - database + labels: + - "traefik.enable=true" + - "traefik.http.routers.ptitspas-api.rule=Host(\"app.ptits-pas.fr\") && PathPrefix(\"/api\")" + - "traefik.http.routers.ptitspas-api.entrypoints=websecure" + - "traefik.http.routers.ptitspas-api.tls.certresolver=leresolver" + - "traefik.http.routers.ptitspas-api.priority=20" + - "traefik.http.services.ptitspas-api.loadbalancer.server.port=3000" + networks: + - ptitspas_network + - proxy_network + + # Frontend Flutter + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: ptitspas-frontend + restart: unless-stopped + environment: + API_URL: ${API_URL} + depends_on: + - backend + labels: + - "traefik.enable=true" + - "traefik.http.routers.ptitspas-front.rule=Host(\"app.ptits-pas.fr\")" + - "traefik.http.routers.ptitspas-front.entrypoints=websecure" + - "traefik.http.routers.ptitspas-front.tls.certresolver=leresolver" + - "traefik.http.routers.ptitspas-front.priority=10" + - "traefik.http.services.ptitspas-front.loadbalancer.server.port=80" + networks: + - ptitspas_network + - proxy_network + +volumes: + postgres_data: + +networks: + ptitspas_network: + driver: bridge + proxy_network: + external: true diff --git a/docs/00_INDEX.md b/docs/00_INDEX.md new file mode 100644 index 0000000..062e0f7 --- /dev/null +++ b/docs/00_INDEX.md @@ -0,0 +1,68 @@ +# 📚 Index de la Documentation - PtitsPas App + +Bienvenue dans la documentation complète de l'application PtitsPas. + +Ce fichier sert d'index pour naviguer dans toute la documentation du projet. + +## 📖 Table des matières + +### 📋 Cahier des Charges +- [**01 - Cahier des Charges**](./01_CAHIER-DES-CHARGES.md) - Cahier des charges complet du projet P'titsPas (V1.3 - 24/11/2025) + +### Architecture & Infrastructure +- [**02 - Architecture**](./02_ARCHITECTURE.md) - Vue d'ensemble de l'architecture mono-repo et multi-conteneurs +- [**03 - Déploiement**](./03_DEPLOYMENT.md) - Guide complet de déploiement et configuration CI/CD + +### Planification +- [**04 - Roadmap Générale**](./04_ROADMAP-GENERALE.md) - Roadmap complète du projet (Phases 1 à 5+) + +### Développement +- [**10 - Database Schema**](./10_DATABASE.md) - Schéma de la base de données et modèles +- [**11 - API Documentation**](./11_API.md) - Documentation complète des endpoints REST + +### Workflows Fonctionnels +- [**20 - Workflow Création de Compte**](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow complet de création et validation des comptes utilisateurs +- [**21 - Configuration Système**](./21_CONFIGURATION-SYSTEME.md) - Configuration on-premise dynamique +- [**22 - Documents Légaux**](./22_DOCUMENTS-LEGAUX.md) - Gestion CGU/Privacy avec versioning +- [**23 - Liste des Tickets**](./23_LISTE-TICKETS.md) - 61 tickets Phase 1 détaillés +- [**24 - Décisions Projet**](./24_DECISIONS-PROJET.md) - Décisions architecturales et fonctionnelles +- [**25 - Backlog Phase 2**](./25_PHASE-2-BACKLOG.md) - Fonctionnalités techniques reportées + +### Administration (À créer) +- [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs +- [**31 - Troubleshooting**](./31_TROUBLESHOOTING.md) - Résolution des problèmes courants + +### Frontend (À créer) +- [**40 - Frontend Flutter**](./40_FRONTEND.md) - Structure de l'application mobile/web + +### Audit & Analyse +- [**90 - Audit du projet YNOV**](./90_AUDIT.md) - Analyse complète du code étudiant et fonctionnalités + +## 🚀 Quick Start + +```bash +# Cloner le projet +git clone ssh://gitea-jmartin/jmartin/app.git ptitspas-app + +# Lancer l'environnement de développement +cd ptitspas-app +docker compose up -d + +# Accéder aux services +Frontend: https://app.ptits-pas.fr +API: https://app.ptits-pas.fr/api +PgAdmin: https://app.ptits-pas.fr/pgadmin +``` + +## 🔗 Liens utiles + +- **Gitea** : https://git.ptits-pas.fr +- **Production** : https://app.ptits-pas.fr +- **Mail** : https://mail.ptits-pas.fr + +## 📝 Maintenance + +Cette documentation est maintenue par Julien Martin (julien.martin@ptits-pas.fr). + +Dernière mise à jour : Novembre 2025 + diff --git a/docs/01_CAHIER-DES-CHARGES.md b/docs/01_CAHIER-DES-CHARGES.md new file mode 100644 index 0000000..dcd02f0 --- /dev/null +++ b/docs/01_CAHIER-DES-CHARGES.md @@ -0,0 +1,1225 @@ +--- +title: "P'titsPas - Cahier des Charges Fonctionnel" +author: "Julien MARTIN" +date: "Novembre 2025" +version: "v1.3" +--- + +# P'titsPas – Cahier des Charges Fonctionnel + +> **Objet :** Définir le périmètre fonctionnel, les rôles utilisateurs, les processus métiers et les exigences techniques de la plateforme P'titsPas, destinée à accompagner les collectivités locales dans la gestion de la garde d’enfants. + +--- + +## Historique des versions + +| Version | Date | Auteur | Commentaires | +|---------|------------|----------------|--------------------------------------| +| 1.0 | 15/03/2025 | Julien MARTIN | Version initiale | +| 1.1 | 24/04/2025 | Julien MARTIN | Ajouts : gestion multi-enfants, fin de contrat, tableau de bord étendu | +| 1.2 | 26/05/2025 | Julien MARTIN | Remplacement de "SuperNounou" par "P'titsPas" | +| 1.3 | 24/11/2025 | Julien MARTIN | Correction : retrait photo de profil parent (section 3.1.1) | + +--- + + +# Table des matières – P'titsPas + +## 1. Introduction + +## 2. Typologie des utilisateurs +### 2.1 Rôles +#### 2.1.1 Parents +#### 2.1.2 Assistantes maternelles +#### 2.1.3 Gestionnaires +#### 2.1.4 Administrateurs +### 2.2 Périmètres et responsabilités + +## 3. Création de compte et fiches utilisateur +### 3.1 Création de compte parent +### 3.2 Création de compte assistante maternelle +### 3.3 Création d’un gestionnaire +### 3.4 Création d’un administrateur +### 3.5 Fiche enfant +### 3.6 Authentification et sécurité + +## 4. Tableaux de bord +### 4.1 Vue d’ensemble +### 4.2 Tableau de bord des parents +#### 4.2.1 En-tête +#### 4.2.2 Zone enfants +#### 4.2.3 Événements à venir +#### 4.2.4 Contrats +#### 4.2.5 Messagerie +#### 4.2.6 Notifications +### 4.3 Tableau de bord des assistantes maternelles +#### 4.3.1 En-tête +#### 4.3.2 Dossiers reçus +#### 4.3.3 Enfants accueillis +#### 4.3.4 Agenda +#### 4.3.5 Heures supplémentaires +#### 4.3.6 Messagerie +### 4.4 Tableau de bord des gestionnaires +#### 4.4.1 Comptes à valider +#### 4.4.2 Liste des utilisateurs +#### 4.4.3 Contrats +#### 4.4.4 Messagerie +#### 4.4.5 Événements RPE +#### 4.4.6 Alertes +### 4.5 Tableau de bord des administrateurs +#### 4.5.1 Menu Profil +#### 4.5.2 Gestion des utilisateurs +#### 4.5.3 Gestion des enfants +#### 4.5.4 Paramètres de la plateforme +#### 4.5.5 Statistiques et supervision +#### 4.5.6 Sécurité et conformité +### 4.6 Menus, alertes et raccourcis visuels + +## 5. Workflows +### 5.1 Recherche d’une assistante maternelle +### 5.2 Traitement du dossier +### 5.3 Création du contrat +### 5.4 Avenants +### 5.5 Fin de contrat + +## 6. Outils partagés +### 6.1 Messagerie +### 6.2 Agenda +### 6.3 Fiche enfant +### 6.4 Contrats et avenants + +## 7. Suivi administratif et financier +### 7.1 Fiche de paie +### 7.2 Aide au remplissage Pajemploi +### 7.3 Heures supplémentaires +### 7.4 Historique +### 7.5 Solde de tout compte + +## 8. Administration technique et conformité +### 8.1 Sécurité et accès +### 8.2 Gouvernance multi-admins +### 8.3 Conformité RGPD +### 8.4 Intégration API +### 8.5 Architecture technique + +## 9. Améliorations futures possibles +### 9.1 Modules activables +#### 9.1.1 Blog RPE +#### 9.1.2 IA de reformulation +#### 9.1.3 Connexion Pajemploi +#### 9.1.4 Garde d’enfants “grands” +#### 9.1.5 Pré-remplissage dossiers scolaires +#### 9.1.6 Dossier fratrie unique +### 9.2 Stratégie d’activation + +## 10. Synthèse finale +### 10.1 Objectifs atteints +### 10.2 Couverture fonctionnelle +### 10.3 Valeur ajoutée +### 10.4 Perspectives d’évolution + +# 1. Introduction + +P'titsPas est une plateforme numérique conçue pour accompagner les collectivités dans la gestion des relations entre les parents, les assistantes maternelles et les relais petite enfance (RPE). Elle vise à simplifier, structurer et sécuriser les processus de mise en relation, de contractualisation, de suivi et de communication autour de la garde d’enfants. + +Elle s’inscrit dans une démarche publique fondée sur les valeurs de la République : **égalité**, **neutralité**, **accessibilité** et **transparence**. Contrairement aux plateformes privées, P'titsPas ne fonctionne ni sur la logique de notation, ni de sélection algorithmique opaque. + +## 1.1 Objectifs + +- Faciliter la recherche d’assistantes maternelles par les parents. +- Offrir un cadre clair et sécurisé à la gestion des contrats. +- Fluidifier la communication entre les acteurs (parents, nounous, gestionnaires). +- Permettre aux gestionnaires de suivre l’ensemble des situations. +- Centraliser les démarches dans un outil unique, intuitif et structuré. +- Valoriser le rôle des collectivités locales dans l’accompagnement de la petite enfance. + +## 1.2 Principes fondateurs + +- **Neutralité** : pas de notation, pas de préférence algorithmique. +- **Équité** : chaque parent a les mêmes droits d’accès aux services. +- **Protection des données** : respect strict du RGPD et du droit à l’oubli. +- **Accessibilité** : conçu pour être lisible sur smartphone comme sur ordinateur. + +## 1.3 Usagers concernés + +- **Parents** d’enfants de 0 à 3 ans à la recherche d’un mode de garde. +- **Assistantes maternelles** souhaitant gérer facilement leurs contrats. +- **Responsables de RPE** supervisant les démarches et accompagnant les familles. +- **Administrateurs** municipaux assurant le paramétrage de l’outil. + +## 1.4 Contexte de déploiement + +P'titsPas est destiné à être déployé à l’échelle d’une ou plusieurs communes. Il s’adapte aux réalités locales en permettant une configuration personnalisée (noms des structures, couleurs, logo, etc.). + +Le modèle de gouvernance repose sur une articulation claire entre : + +- Les **parents et assistantes maternelles** (usagers finaux) +- Les **gestionnaires** (opérateurs terrain) +- Les **administrateurs** (services de la mairie ou de la collectivité) + +# 2. Typologie des utilisateurs + +P'titsPas repose sur une structure multi-rôles permettant une répartition claire des responsabilités et des droits d’accès à l’application. Chaque profil a une interface adaptée à ses besoins et à ses fonctions. + +## 2.1 Rôles + +### 2.1.1 Parents + +Les parents sont les usagers finaux de la plateforme. Ils accèdent à un tableau de bord personnel leur permettant de : + +- Rechercher une assistante maternelle +- Créer et suivre des dossiers +- Consulter les informations liées à leurs enfants +- Visualiser et gérer les contrats de garde +- Communiquer avec l'assistante maternelle et le gestionnaire +- Déclarer des absences ou des événements +- Suivre les documents administratifs liés à la garde +- Accéder à des outils d’aide à la paie + +### 2.1.2 Assistantes maternelles + +Les assistantes maternelles peuvent : + +- Recevoir des dossiers de demande de garde +- Accepter ou refuser une proposition +- Fournir leurs disponibilités pour organiser un rendez-vous +- Gérer leurs contrats et leurs avenants +- Suivre les absences des enfants accueillis +- Déclarer des heures supplémentaires +- Participer à des événements organisés par le RPE +- Utiliser la messagerie intégrée + +### 2.1.3 Gestionnaires + +Les gestionnaires (responsables de relais petite enfance) disposent d’un tableau de bord de supervision. Ils peuvent : + +- Valider ou rejeter les demandes de création de compte +- Suivre les mises en relation et les contrats +- Organiser des événements ou des rendez-vous +- Gérer les conflits ou les fins de contrat +- Lancer des sondages ou modérer un blog RPE (si activé) +- Voir les historiques et statistiques liés à leur périmètre + +### 2.1.4 Administrateurs + +Les administrateurs sont les représentants techniques et institutionnels de la collectivité (DSI ou agents désignés). Ils peuvent : + +- Créer ou supprimer des comptes (gestionnaires, parents, assistantes maternelles) +- Personnaliser l’interface (logo, couleurs, nom de la ville) +- Activer ou désactiver des modules complémentaires +- Consulter les statistiques d’usage +- Exporter les données +- Superviser les accès et assurer la conformité RGPD +- Intégrer la plateforme aux SI de la collectivité + +## 2.2 Périmètres et responsabilités + +Chaque rôle dispose d’un périmètre fonctionnel propre, sans empiétement sur celui des autres : + +- Les parents et assistantes maternelles **échangent directement** entre eux via la messagerie, mais ne peuvent modifier les données contractuelles qu’avec accord mutuel. +- Les gestionnaires interviennent en **appui et en arbitrage**, sans être partie prenante dans les contrats. +- Les administrateurs disposent d’un **pouvoir global de paramétrage et de supervision**. + +# 3. Création de compte et fiches utilisateur + +La création de compte dans P'titsPas est structurée par profil, avec des parcours adaptés à chaque rôle. Tous les champs d'identité sont obligatoires, y compris les photos pour les assistantes maternelles et les enfants déjà nés. Le processus d’approbation garantit la sécurité des données et l’implication des acteurs concernés. + +## 3.1 Création de compte parent + +Le parcours de création d’un compte parent s’effectue en plusieurs étapes successives. + +### 3.1.1 Informations parent 1 +- Nom, prénom +- Adresse postale +- Téléphone (obligatoire) +- Adresse e-mail + +**Note** : Le parent 1 ne définit **pas** de mot de passe lors de l'inscription. Il recevra un email avec un lien pour créer son mot de passe après validation du gestionnaire. + +### 3.1.2 Informations parent 2 (facultatif) +- Possibilité d'ajouter un second parent +- Nom, prénom, email, téléphone +- Même adresse que le parent 1 (case à cocher ou champs alternatifs) +- Les deux parents sont ensuite liés dans l'application + +**Note** : Le parent 2 ne définit **pas** de mot de passe lors de l'inscription. Il recevra un email avec un lien pour créer son mot de passe après validation du gestionnaire. Cette approche est particulièrement adaptée aux situations de parents séparés ou divorcés où la communication peut être difficile. + +### 3.1.3 Informations sur l'enfant +- Prénom (facultatif si enfant à naître) +- Nom (hérité des parents) +- Genre (H / F) - obligatoire +- Date de naissance ou **date prévisionnelle de naissance** (si l'enfant n'est pas encore né, un switch modifie le label) +- Photo obligatoire si l'enfant est né +- Rattachement automatique aux deux parents + +### 3.1.4 Présentation du dossier +- Zone de texte libre permettant aux parents de décrire leur situation +- Ce champ peut être rendu obligatoire ou non par l’administrateur + +### 3.1.5 – Acceptation des CGU +- Les utilisateurs doivent cocher la case + « J’ai lu et j’accepte les Conditions Générales d’Utilisation et la Politique de confidentialité ». +- Un lien direct ouvre la version PDF des CGU. +- Le refus bloque la création de compte. + +### 3.1.6 Récapitulatif et validation +- Résumé des données saisies +- Vérification, puis envoi de la demande +- Les comptes parent sont soumis à validation par un gestionnaire avant activation +- Une fois validé, chaque parent (Parent 1 et Parent 2 si renseigné) reçoit un e-mail ou un SMS contenant un lien pour créer son mot de passe +- Le lien est valable pendant 7 jours +- Une fois le mot de passe créé, le parent peut se connecter à son espace + +## 3.2 Création de compte assistante maternelle + +Ce parcours est divisé en deux panneaux. + +### 3.2.1 Panneau 1 – Informations d'identité +- Nom, prénom +- Adresse postale +- Téléphone +- Adresse e-mail +- Photo (obligatoire si l'option *Photo obligatoire* est activée) +- Consentement photo : case à cocher confirmant l'accord pour stocker et afficher la photo (RGPD) + +**Note** : L'assistante maternelle ne définit **pas** de mot de passe lors de l'inscription. Elle recevra un email avec un lien pour créer son mot de passe après validation du gestionnaire. + +### 3.2.2 Panneau 2 – Informations professionnelles +- Date de naissance +- Ville et pays de naissance +- Numéro de Sécurité sociale (NIR) – obligatoire + - Saisi en clair (non masqué) + - Mention : « Utilisé uniquement pour la génération automatique du contrat » +- Numéro d'agrément – obligatoire +- Date d'obtention de l'agrément – obligatoire +- Nombre d'enfants pouvant être accueillis – obligatoire + +### 3.2.3 Présentation +- Champ libre : message à destination du gestionnaire +- Permet de justifier une demande ou d’ajouter des précisions + +### 3.1.4 – Acceptation des CGU +- Les utilisateurs doivent cocher la case + « J’ai lu et j’accepte les Conditions Générales d’Utilisation et la Politique de confidentialité ». +- Un lien direct ouvre la version PDF des CGU. +- Le refus bloque la création de compte. + +### 3.2.5 Récapitulatif et validation +- Résumé des données saisies +- Vérification, puis envoi de la demande +- Validation par un gestionnaire requise avant activation +- Une fois validé, l'assistante maternelle reçoit un e-mail ou un SMS contenant un lien pour créer son mot de passe +- Le lien est valable pendant 7 jours +- Une fois le mot de passe créé, l'assistante maternelle peut se connecter à son espace + +## 3.3 Création d’un gestionnaire + +- Réalisée par un administrateur +- Champs obligatoires : nom, prénom, adresse e-mail, mot de passe +- Affectation à un ou plusieurs relais petite enfance +- Le mot de passe doit être modifié lors de la première connexion + +## 3.4 Création d’un administrateur + +- Seuls les administrateurs existants peuvent créer de nouveaux comptes administrateurs +- Les droits sont équivalents (possibilité de restreindre par périmètre dans une version multi-mairies) +- Obligation de changer le mot de passe à la première connexion + +## 3.5 Fiche enfant + +Chaque enfant est représenté par une fiche : + +- Prénom (facultatif si enfant à naître) +- Nom +- Genre (H / F) - obligatoire +- Date de naissance ou date prévisionnelle +- Photo (obligatoire si l'enfant est né ET si l'option *Photo obligatoire* est activée) +- Consentement photo enregistré : valeur booléenne + horodatage liés à l'accord donné par le parent +- Statut : à naître / actif / scolarisé +- Indication possible : jumeaux, triplés, etc. +- Possibilité de rattacher un enfant à plusieurs parents (garde alternée) + +## 3.6 Authentification et sécurité + +- Tous les comptes utilisent une combinaison adresse e-mail + mot de passe +- Les gestionnaires et administrateurs doivent modifier leur mot de passe à la première connexion +- Des mécanismes de récupération sont disponibles en cas de perte + +Un lien direct vers les **Mentions légales** et la **Politique de confidentialité** est accessible en permanence depuis le pied de page, y compris avant la connexion. + +# 4. Tableaux de bord + +Chaque rôle utilisateur dispose d’un tableau de bord personnalisé, adapté à ses fonctions dans la plateforme. Ces interfaces sont pensées pour être lisibles, fonctionnelles et évolutives. + +## 4.1 Vue d’ensemble + +| Rôle | Accès au tableau de bord | +|------------------------|-------------------------------------------------------------------| +| Parents | Recherche d'assistante maternelle, gestion des enfants, contrat, agenda | +| Assistantes maternelles| Dossiers reçus, enfants accueillis, heures sup, agenda, messagerie| +| Gestionnaires | Validation de comptes, contrats, messagerie, événements RPE | +| Administrateurs | Paramètres globaux, gestion des utilisateurs, statistiques | + +Les tableaux de bord intègrent : +- Une **barre de navigation supérieure** (liens de navigation rapide) +- Une **zone centrale dynamique**, spécifique au rôle +- Une **zone de notifications ou de messagerie** +- Un **menu profil** propre à chaque type d'utilisateur (cf. section 4.6) + +Un système de **bulles d’aide contextuelle** (icône « ? ») doit être présent sur les champs ou boutons complexes ; un clic ouvre un court tooltip explicatif. Aucune base de connaissances complète n’est exigée en V1. + +#### Pied de page commun + +Chaque page de l’application affiche un **pied de page** fixe contenant : + +| Élément | Cible / comportement | +|---------|----------------------| +| **Contact support** | Ouvre une fenêtre de mailto :`support@P'titsPas.local` (adresse configurable dans l’admin). | +| **Signaler un bug** | Ouvre un formulaire modal minimal :
Champ texte, bouton « Envoyer » ⇒ crée une entrée dans la table `bug_report` (ou envoie un e-mail si aucune table n’est prévue). | +| **Mentions légales** | Lien vers la page statique `/legal` : informations éditeur, hébergeur, responsable du traitement. | +| **Politique de confidentialité** | Lien vers la page statique `/privacy` (le texte RGPD détaillé). | + +Le pied de page doit rester visible sur desktop et mobile ; sur écrans étroits, les liens peuvent être regroupés dans un menu « Informations ». + + +## 4.2 Tableau de bord des parents + +Le tableau de bord parent est conçu pour offrir une vue complète de la situation de la famille, avec des entrées rapides vers toutes les fonctionnalités utiles. + +### 4.2.1 En-tête + +- Logo de la ville +- Nom et prénom du parent connecté +- Accès au menu Profil +- Bouton “Rechercher une assistante maternelle” +- Notification si un contrat est en cours ou un dossier en traitement + +### 4.2.2 Zone enfants + +- Encadré pour chaque enfant : prénom, photo, statut +- Bouton “Ajouter un enfant” +- Accès à la fiche de l’enfant (dossiers, agenda, événements) + +### 4.2.3 Événements à venir + +Les événements sont affichés sous forme de **bulles colorées** : +- Vacances parents +- Absence enfant +- Activité RPE (confirmée ou suggérée) +- Congés assistante maternelle +- Pour un **arrêt maladie**, le parent destinataire dispose du bouton **« Arrêt bien reçu »** ; lorsqu’il le clique, l’événement est marqué **Validé** côté assistante maternelle. + +Chaque événement peut être : +- Confirmé ou refusé (si proposé par l’autre partie) +- Modifié par la partie créatrice +- Ajouté manuellement par le parent + +Un bouton “Accéder à l’agenda” ouvre une vue étendue : +- Liste complète des événements (passés et futurs) +- Calendrier annuel, mensuel ou hebdomadaire +- Ajout d’événements : absence, vacances, note personnelle + +### 4.2.4 Contrats + +- Encadré avec état du dossier (brouillon, validé, en cours, en fin) +- Consultation du contrat signé +- Bouton “Proposer un avenant” +- Accès à l’historique des modifications +- Lien vers l’outil d’aide Pajemploi (calculs et déclaration) + +### 4.2.5 Messagerie + +- Historique des conversations +- Possibilité de filtrer par enfant ou par assistante maternelle +- Les deux parents d’un enfant sont toujours intégrés ensemble dans une conversation +- Possibilité d’ajouter le gestionnaire à une conversation existante +- Icône spéciale si le message a été reformulé par IA (si module activé) + +### 4.2.6 Notifications + +- Liste visuelle avec icônes et codes couleur : + - Nouvel événement ajouté par l’assistante maternelle + - Dossier modifié + - Contrat en attente de validation + - Paiement en attente + - Message non lu + +## 4.3 Tableau de bord des assistantes maternelles + +L’assistante maternelle accède à une interface dédiée à la gestion de ses accueils, de ses disponibilités, et de ses échanges avec les familles et le RPE. + +### 4.3.1 En-tête + +- Nom et prénom +- Photo de profil +- Accès au menu Profil +- Message d’information (nouveau dossier reçu, contrat en cours, etc.) + +### 4.3.2 Dossiers reçus + +- Liste des demandes de garde +- Pour chaque dossier : nom de l’enfant, informations des parents, dates souhaitées +- Message personnalisé des parents affiché +- Bouton “Accepter le dossier” → propose ensuite des disponibilités de rendez-vous +- Historique des décisions prises + +### 4.3.3 Enfants accueillis + +- Liste des enfants avec fiche complète : + - Identité + - Contrat associé + - Agenda de garde + - Alertes importantes (allergies, horaires atypiques) + +### 4.3.4 Agenda + +- Vue des absences validées +- Ajout d’absences programmées (vacances, fermeture exceptionnelle) +- Ajout d’un **arrêt maladie** : + 1. L’assistante maternelle saisit la période et clique **« Arrêt transmis aux parents »** ; + 2. Elle sélectionne le parent à qui elle a remis le justificatif papier (Parent 1 ou Parent 2) ; + 3. L’événement passe au statut **« En attente de confirmation parent »** dans les deux agendas. +- Chaque événement peut contenir un commentaire +- Les absences doivent être validées par les parents + +### 4.3.5 Heures supplémentaires + +- Déclaration des jours concernés +- Saisie du nombre d’heures effectuées +- Commentaire facultatif +- Historique et régularisation automatique dans la fiche de paie + +### 4.3.6 Messagerie + +- Conversations avec les parents +- Conversations de groupe avec le gestionnaire (événements RPE) +- Notification lorsqu’un nouveau message est reçu +- Icône indiquant si le message a été reformulé par IA (si activée) + +--- + +## 4.4 Tableau de bord des gestionnaires + +Le gestionnaire RPE dispose d’une vision transversale sur les utilisateurs, les dossiers et les activités de la structure. Son rôle est d'accompagner, superviser, et arbitrer si nécessaire. + +### 4.4.1 Comptes à valider + +- File d’attente des demandes de création de compte +- Détails de chaque demande : parent, assistante maternelle +- Actions possibles : Valider / Refuser / Demander des précisions + +### 4.4.2 Liste des utilisateurs + +- Filtres par rôle, statut, date d’inscription +- Accès rapide aux informations et historiques +- Possibilité de contacter un utilisateur + +### 4.4.3 Contrats + +- Vue consolidée des contrats en cours, en validation, ou en fin de garde +- Historique des avenants +- Accès au détail d’un contrat en cas de besoin d’arbitrage + +> **Responsabilité du gestionnaire RPE** +> Le gestionnaire agit exclusivement comme **tiers de confiance** : +> - il vérifie la cohérence des informations fournies par les parents et l’assistante maternelle ; +> - il valide la conformité formelle du contrat (mentions légales, dates, taux). +> **Il n’est pas partie prenante au contrat de travail** et n’assume aucune responsabilité patrimoniale ou employeur ; son rôle se limite à un contrôle administratif et à la médiation en cas de désaccord. + +### 4.4.4 Messagerie + +- Possibilité d’entrer dans une conversation parent-assistante pour médiation +- Démarrer une nouvelle conversation avec un parent, une assistante maternelle, ou les deux +- Conversations de groupe avec plusieurs assistantes maternelles (organisation d’événements RPE) + +### 4.4.5 Événements RPE + +- Création et suivi des événements collectifs +- Sélection des assistantes concernées +- Confirmation de participation +- Ajout automatique dans les agendas parentaux si l’enfant est inscrit + +### 4.4.6 Alertes + +- Liste visuelle des événements nécessitant attention : + - Conflits signalés + - Absences non validées + - Fin de contrat en désaccord +- Code couleur selon niveau d’urgence +- Accès direct aux dossiers concernés + +##### Délais et relance – absences en attente +- Une absence saisie par la partie A doit être **validée sous 7 jours** par la partie B. +- Relance automatique : J+5 par notification (parent ou assistante maternelle). +- Si non validée au bout de 14 jours, alerte “Urgence” (rouge) dans le panneau Gestionnaire. +- Les délais sont configurables par l’administrateur (paramètre “ABSENCE_GRACE_PERIOD” en jours). + +## 4.5 Tableau de bord des administrateurs + +Le tableau de bord des administrateurs est dédié à la gestion technique et stratégique de la plateforme pour une collectivité. Il centralise tous les outils de supervision, d’administration des comptes et de paramétrage global. + +### 4.5.1 Menu Profil + +Le menu profil de l’administrateur comprend uniquement : +- Mon compte +- Déconnexion + +Toutes les fonctionnalités de gestion sont accessibles via des onglets distincts dans le tableau de bord. + +### 4.5.2 Gestion des utilisateurs + +L’administration des utilisateurs est organisée par panneaux distincts pour chaque type de profil. + +#### a. Gestion des gestionnaires +- Liste des gestionnaires existants +- Création (nom, prénom, e-mail, mot de passe) +- Attribution à un ou plusieurs RPE +- Réinitialisation du mot de passe +- Suppression du compte + +#### b. Gestion des parents +- Liste complète des parents enregistrés +- Recherche par nom, statut, enfants associés +- Modification des informations +- Suppression d’un compte +- Consultation du statut des dossiers liés + +#### c. Gestion des assistantes maternelles +- Liste des assistantes avec numéro d’agrément +- Modification ou suppression d’un compte +- Filtrage par zone géographique ou capacité + +#### d. Gestion des administrateurs +- Création de nouveaux comptes administrateurs +- Suivi des droits +- Obligation de modification du mot de passe à la première connexion + +### 4.5.3 Gestion des enfants + +Deux accès possibles à la gestion des enfants : + +#### a. Par fiche parent +- Consultation et édition des enfants associés à chaque parent + +#### b. Vue globale “Enfants” +- Liste complète avec : + - Nom, prénom, date de naissance ou prévisionnelle + - Statut (à naître, actif, scolarisé) + - Parents associés + - Contrat en cours (si applicable) +- Possibilité de modifier ou supprimer une fiche enfant + +### 4.5.4 Paramètres de la plateforme + +#### a. Modules activables +- Blog RPE +- IA de reformulation de messages +- Aide Pajemploi +- Dossiers scolaires en fin de garde +- Garde d’enfants “grands” +- Dossier fratrie unique + +#### b. Personnalisation visuelle +- Logo de la commune +- Thème couleurs +- Nom de la structure affichée + +#### c. Règles de consentement et médias +- Photo obligatoire (assistante maternelle & enfant) : *On / Off* + - *On* : le champ Photo devient requis pour l'assistante maternelle et l’enfant ; la case Consentement photo s’affiche. + - *Off* : la photo redevient facultative et le champ Consentement photo disparaît. +- Historique des changements avec horodatage et identifiant administrateur. + +### 4.5.5 Statistiques et supervision + +- Nombre de contrats actifs +- Nombre d’enfants enregistrés (par statut) +- Nombre de dossiers ouverts, validés, clôturés +- Utilisation des modules optionnels +- Export CSV disponibles : + - Activité par RPE + - Données sur les enfants + - Historique des absences + +### 4.5.6 Sécurité et conformité + +- Suivi des connexions et accès sensibles +- Politique de confidentialité consultable +- Gestion des consentements +- Interface de suppression/anonymisation des données +- Historique horodaté des actions d’administration + +## 4.6 Menus, alertes et raccourcis visuels + +Chaque type d’utilisateur dispose d’un menu “Profil” spécifique, d’un système d’alertes et de raccourcis permettant une navigation fluide et intuitive. + +### 4.6.1 Menus Profil par rôle + +#### a. Parents +- Mes informations +- Mon compte +- Déconnexion + +#### b. Assistantes maternelles +- Mon profil +- Mes informations professionnelles + - Permet de consulter et modifier : NIR, numéro d’agrément, capacité d’accueil, etc. + - Le NIR n’est accessible qu’à l’assistante maternelle elle-même ; aucun autre rôle ne le voit. +- Mon compte +- Déconnexion + +#### c. Gestionnaires +- Mon compte +- Déconnexion + +#### d. Administrateurs +- Mon compte +- Déconnexion + +### 4.6.2 Alertes visuelles + +Des alertes apparaissent sous forme de bulles ou encadrés colorés dans le tableau de bord : +- Nouvel événement ajouté par une autre partie +- Contrat en attente de validation +- Conflit signalé +- Absence à approuver +- Dossier refusé ou incomplet + +Les gestionnaires disposent d’un **panneau d’alertes consolidées**, codé par couleur : +- Rouge : urgence ou conflit +- Orange : action requise +- Vert : succès ou validation en attente + +### 4.6.3 Raccourcis fonctionnels + +Selon le rôle : +- **Parents** : bouton “Rechercher une assistante maternelle”, accès rapide à l’enfant principal +- **Assistantes maternelles** : “Déclarer une absence”, “Voir les enfants du jour” +- **Gestionnaires** : “Nouveaux comptes”, “Dossiers à traiter”, “Événements RPE” +- **Administrateurs** : accès direct aux sections de gestion des utilisateurs, enfants, et paramètres + +Les raccourcis sont affichés sous forme d’icônes dans la barre supérieure ou la colonne latérale, selon la taille d’écran. + +# 5. Workflows + +Ce chapitre décrit les différents processus métiers (workflows) au cœur de P'titsPas : de la recherche d’une assistante maternelle à la fin du contrat, en passant par le traitement des dossiers, la contractualisation, les avenants, et la séparation. + +## 5.1 Recherche d’une assistante maternelle + +### 5.1.1 Déclenchement + +- Un parent connecté accède à son tableau de bord. +- S’il n’a pas encore d'assistante maternelle, un panneau de recherche s’affiche automatiquement. +- Sinon, il peut cliquer sur le bouton “Rechercher une assistante maternelle”. + +### 5.1.2 Étape 1 – Informations du dossier + +Le parent complète un formulaire : +- Enfant concerné +- Type de contrat : année complète, incomplète, ou temporaire (remplacement) +- Fourniture des repas (oui/non) +- Budget maximum par poste (taux horaire, repas, indemnités) +- Plages horaires souhaitées pour chaque jour + +### 5.1.3 Étape 2 – Texte d’introduction + +Le parent rédige une courte présentation à destination des assistantes maternelles, expliquant sa situation. + +### 5.1.4 Étape 3 – Disponibilités pour un rendez-vous + +Le parent indique ses plages de disponibilité pour une rencontre. + +### 5.1.5 Étape 4 – Recherche et sélection + +- Une liste des assistantes maternelles est proposée, classée par **proximité géographique**. +- Chaque fiche contient des initiales (photo uniquement visible après rencontre). +- Une carte interactive affiche leur localisation. +- Le parent peut sélectionner une ou plusieurs nounous à qui envoyer un dossier. + +### 5.1.6 Étape 5 – Récapitulatif et envoi + +- Le parent valide le dossier complet +- Le dossier est transmis aux assistantes maternelles sélectionnées + +## 5.2 Traitement du dossier + +### 5.2.1 Réception par l'assistante maternelle + +- Le dossier s’affiche dans son tableau de bord +- Elle peut consulter : + - Fiche parent 1 et parent 2 (si existant) + - Fiche enfant + - Texte de présentation + - Informations pratiques et financières + - Plages de disponibilité proposées + +### 5.2.2 Réponse de l’assistante maternelle + +- Elle peut accepter, refuser, ou demander à échanger via messagerie. +- Si elle accepte, elle sélectionne un ou plusieurs créneaux compatibles avec ceux proposés. + +### 5.2.3 Proposition de rendez-vous + +- Le parent reçoit les créneaux suggérés. +- Il peut en choisir un, ou en proposer un autre à partir de l’agenda. +- L'assistante maternelle accepte, refuse ou modifie. +- Ce ping-pong se poursuit jusqu’à accord. + +### 5.2.4 Finalisation du rendez-vous + +- Une fois confirmé, les numéros de téléphone sont échangés. +- Le rendez-vous a lieu hors de la plateforme. + +## 5.3 Création du contrat + +### 5.3.1 Validation mutuelle + +- Après le rendez-vous, l'assistante maternelle et les parents sont invités à confirmer la poursuite du dossier. +- Chacun renseigne : + - Horaires de garde + - Taux horaire + - Indemnités repas et entretien + - Début du contrat + +### 5.3.2 Vérification croisée + +- L’outil compare les informations saisies par les deux parties. +- Si elles diffèrent : + - Nouvelle saisie demandée + - Le gestionnaire peut être notifié pour accompagner + +- Si elles concordent : + - Le contrat est généré automatiquement + +### 5.3.3 Validation finale + +Une fois le contrat généré et validé par le gestionnaire : + +1. **Téléchargement** : chaque partie récupère le PDF. +2. **Signature manuscrite** : les parents (employeurs) et l’assistante maternelle signent physiquement le document. +3. **Confirmation dans la plateforme** : + - Un bouton « J’ai signé » apparaît pour chaque partie. + - (Optionnel) Téléversement du contrat signé scanné ou photographié. +4. Dès que les deux confirmations sont saisies, **le contrat est considéré actif** ; l’application lie définitivement les comptes parent / assistante maternelle à ce contrat. + +> Lors de la génération du contrat, le NIR de l’assistante maternelle est injecté dans le PDF, mais **n’est affiché nulle part dans l’application**. + +## 5.4 Avenants + +### 5.4.1 Initiation + +- Un bouton “Proposer un avenant” est disponible dans le panneau Contrat +- Les deux parties peuvent initier un avenant + +### 5.4.2 Contenu + +- Modification des horaires +- Revalorisation du taux horaire +- Changement de type de contrat (année complète ↔ incomplète) +- Nombre de semaines de garde + +### 5.4.3 Validation + +- Même processus que pour le contrat initial : + - Chaque partie valide + - Vérification de cohérence + - Approbation finale du gestionnaire + +### 5.4.4 Amélioration future possible + +- Numérotation automatique des avenants +- Historique des modifications + +## 5.5 Fin de contrat + +### 5.5.1 Déclaration de fin + +- Initiée par l’une des parties +- Raison de la rupture (texte obligatoire) +- Option “Notifier le gestionnaire” (cas de désaccord) + +### 5.5.2 Intervention du gestionnaire + +- Peut proposer un rendez-vous de conciliation +- Possibilité de laisser des observations + +#### 5.5.2 bis Procédure en cas de désaccord persistant +- Si la médiation du gestionnaire n’aboutit pas : + 1. Création automatique d’un **dossier Litige** (statut “Ouvert”) visible du gestionnaire et des deux parties. + 2. Possibilité d’ajouter des commentaires horodatés et pièces justificatives (non obligatoires). + 3. Génération d’un **rapport PDF** exportable pour saisie d’un conciliateur ou des services juridiques de la mairie. +- Le dossier passe à “Clôturé” : + - par accord écrit des parties, + - ou après 90 jours sans activité (archivage automatique). + +### 5.5.3 Solde de tout compte + +- Calcul automatique du solde : + - Congés restants + - Indemnités + - Heures supplémentaires +- Génération d’un récapitulatif téléchargeable + + +# 6. Outils partagés + +Certains outils de la plateforme sont accessibles à plusieurs profils et favorisent la collaboration entre les utilisateurs tout en assurant la traçabilité des échanges. + +## 6.1 Messagerie + +- Intégrée dans tous les tableaux de bord (parents, assistantes maternelles, gestionnaires) +- Permet les échanges directs entre parents et assistantes maternelles +- Possibilité pour les parents ou l'assistante maternelle d’ajouter le gestionnaire à une conversation existante +- Le gestionnaire peut initier des conversations avec : + - Un parent + - Une assistante maternelle + - Les deux à la fois (en cas de médiation) +- Conversations de groupe possibles entre le gestionnaire et plusieurs assistantes maternelles (organisation d’événements) +- 📌 **Nota** : les parents ne participent jamais à ces groupes collectifs +- ✅ Si deux parents sont déclarés, ils sont **automatiquement intégrés** à toutes les conversations liées à leur enfant + +## 6.2 Agenda + +- Chaque rôle dispose d’un agenda intégré +- Permet d’ajouter, modifier ou valider : + - Absences enfants + - Congés des assistantes maternelles + - Vacances des parents + - Événements RPE +- Un résumé visuel est proposé (vue mois/semaine/année) +- Synchronisation automatique avec les événements validés par l’autre partie +- Récapitulatif hebdomadaire (ou quotidien) envoyé à l’assistante maternelle + +##### Workflow « Arrêt maladie » +- Fonctionne sans stockage de document médical : + 1. L’assistante maternelle clique **Arrêt transmis aux parents**. + 2. Le parent concerné valide via **Arrêt bien reçu**. +- Une fois les deux actions réalisées, l’arrêt maladie est confirmé dans les agendas des deux parties. + + +## 6.3 Fiche enfant + +- Visible et modifiable par les parents +- Consultable par : + - L’assistante maternelle (si liée à un contrat) + - Le gestionnaire (à titre d’information) +- Contient : + - Identité + - Photo (obligatoire si né) + - Statut (à naître / actif / scolarisé) + - Parents associés + - Mention s’il s’agit de jumeaux, triplés, etc. + +## 6.4 Contrats et avenants + +- Partagés entre les parents, l’assistante maternelle, et le gestionnaire +- Contrat actif consultable dans les tableaux de bord +- Avenants consultables dans l’historique +- Contrat généré uniquement après validation mutuelle et approbation du gestionnaire +- L’outil garantit que seules les versions validées sont actives et accessibles + + +# 7. Suivi administratif et financier + +La plateforme P'titsPas intègre plusieurs outils d’accompagnement pour simplifier la gestion administrative et financière des contrats entre parents et assistantes maternelles. + +## 7.1 Fiche de paie + +- Calcul automatique des éléments mensuels à reporter : + - Heures normales rémunérées + - Heures supplémentaires + - Indemnités repas et entretien + - Absences déduites +- Récapitulatif consultable dans l’espace Contrat du parent et de l’assistante maternelle +- Export possible sous format PDF pour usage personnel ou archivage +- Le NIR de l’assistante maternelle est utilisé pour pré-remplir les champs Pajemploi ; il n’est jamais affiché à l’écran. + + +## 7.2 Aide au remplissage Pajemploi + +- Affichage clair et guidé de la **feuille à remplir** sur le site Pajemploi +- P'titsPas préremplit chaque champ avec les valeurs calculées à partir du contrat +- Objectif : faciliter la saisie et éviter les erreurs de déclaration +- 📌 **Nota** : il ne s’agit pas d’une déclaration automatique + +## 7.3 Heures supplémentaires + +- L’assistante maternelle peut déclarer des heures supplémentaires via son tableau de bord : + - Sélection du jour concerné + - Nombre d’heures effectuées au-delà du contrat + - Commentaire facultatif +- Les heures apparaissent ensuite dans le récapitulatif mensuel +- Le tarif des heures supplémentaires est défini dans le contrat initial + +## 7.4 Historique + +- Chaque contrat conserve un historique : + - Avenants + - Déclarations mensuelles + - Modifications validées + - Absences notables +- Accessible aux parents, à l’assistante maternelle, et au gestionnaire + +## 7.5 Solde de tout compte + +- Lors de la fin de contrat, l’outil génère un récapitulatif : + - Jours de congés restants ou à indemniser + - Indemnités éventuelles + - Heures supplémentaires non réglées +- Permet d’établir un **solde de tout compte** clair, limitant les risques de conflit + + +# 8. Administration technique et conformité + +Ce chapitre regroupe les mécanismes garantissant la sécurité, la fiabilité et la conformité légale de la plateforme P'titsPas. Il s’adresse aux administrateurs techniques des collectivités. + +## 8.1 Sécurité et accès + +- Chaque utilisateur possède un identifiant personnel et un mot de passe +- Les administrateurs et gestionnaires doivent changer leur mot de passe à la première connexion +- Journalisation des accès sensibles (historique des connexions, changements de paramètres) +- Sécurisation HTTPS, stockage des mots de passe chiffré +- Limitation des tentatives de connexion frauduleuses (anti-brute-force) +- Chiffrement au repos : le NIR est stocké chiffré (AES-256, clé KMS). +- Accès restreint : seule l’assistante maternelle peut consulter ou modifier son NIR. +- Journalisation : toute modification du NIR est tracée (horodatage, ID utilisateur). +- Aucune pièce jointe médicale n’est conservée ; seules les métadonnées de validation des arrêts maladie (date, utilisateurs) sont journalisées. +- Les rapports créés via « Signaler un bug » sont stockés dans la table `bug_report` avec horodatage et identifiant utilisateur, accessibles uniquement aux administrateurs. + +> 🔗 **Sauvegarde & PRA** +> Les exigences détaillées sont décrites dans le SSS-SAU-001 « Sauvegarde & Plan de Reprise d’Activité ». +> Le présent CDC ne reprend que les objectifs RPO/RTO et la nécessité de tests semestriels. + + +## 8.2 Gouvernance multi-admins + +- Possibilité de créer plusieurs administrateurs par commune +- Droits identiques, sauf si future extension multi-mairies (non implémentée à ce stade) +- Suivi des actions d’administration horodatées pour audit interne + +## 8.3 Conformité RGPD + +- Affichage explicite des consentements lors de la création de compte +- Possibilité pour l’utilisateur de : + - Consulter les données stockées + - Demander la suppression de son compte +- Interface d’anonymisation des données sur demande +- Accès à la politique de confidentialité depuis chaque tableau de bord +- Archivage horodaté des modifications sensibles +- Stockage du consentement photo avec date, heure, utilisateur concerné. +- Retrait de consentement : masque immédiat de la photo, déclenchement d’une suppression définitive sous 30 jours. +- Registre des traitements mis à jour pour la finalité « affichage photo assistante maternelle / enfant ». + +#### 8.3.1 Fondement juridique +- Collecte du NIR fondée sur l’exécution du contrat de travail (art. 6-1-b RGPD). + +#### 8.3.2 Minimisation +- Le NIR est utilisé **exclusivement** pour générer le contrat PDF et la fiche de paie. + +#### 8.3.3 Conservation +- Conservation : 5 ans après la fin du dernier contrat, puis hachage irréversible ou suppression. + +#### 8.3.4 Consentement photo +- Stockage de l’accord avec date, heure, utilisateur. +- Retrait de consentement : masque immédiat, suppression définitive sous 30 jours. +- Registre des traitements mis à jour : finalité « Affichage photo assistante maternelle / enfant ». + +#### 8.3.5 Droits des personnes et suppression de compte +- Les parents et assistantes maternelles peuvent **supprimer** leur compte à tout moment depuis le menu Profil. +- La suppression entraîne : + - désactivation immédiate de l’accès, + - effacement des données personnelles hors obligations légales (contrats archivés 5 ans). +- **Anonymisation totale non applicable** : le lien employeur – salarié impose de conserver certaines informations nominatives dans les contrats et historiques de paie conformément au Code du Travail. + +## 8.4 Intégration API + +- Possibilité future d’intégration aux systèmes d’information municipaux : + - Statistiques + - Données anonymisées d’activité + - Synchronisation éventuelle avec les portails familles ou services petite enfance +- ⚠️ Préparation automatique des déclarations Pajemploi via API non disponible par défaut : + - Cette fonctionnalité nécessite une habilitation spécifique de l’URSSAF + - Elle est donc classée comme amélioration future possible + +### 8.5 Architecture technique + +L’équipe de développement est libre de choisir la pile technologique ; seules les règles suivantes sont impératives. + +> 🔗 Les aspects techniques (sauvegarde, PRA, CI/CD, API, déploiement) sont détaillés dans le document **SSS-001 – Spécification technique & opérationnelle unifiée**. + +#### 8.5.1 Principes directeurs +- **Conformité RGPD** : minimisation, chiffrement au repos et en transit. +- **Sécurité by-design** : respect OWASP Top 10, revue de code, tests de pénétration. +- **Modularité & maintenabilité** : séparation claire frontend / backend / base. +- **Portabilité** : conteneurisation (Docker ou équivalent) pour exécution locale et serveur communal. +- **Traçabilité** : journaux horodatés, corrélables (API, base, authentification). +- **Documentation** : README d’installation, diagrammes d’architecture, description API. + +#### 8.5.2 Contraintes non fonctionnelles _(valeurs indicatives)_ +- Disponibilité mensuelle ≥ **98 %**. +- Temps de réponse **P95 < 500 ms** sur les opérations courantes. +- Capacité de test : **≈ 50 sessions simultanées**. +- Compatibilité navigateurs : Chrome, Edge, Firefox, Safari (versions N, N-1). +- Accessibilité : niveau **RGAA AA**. +- Agenda utilisable hors-ligne ; synchronisation sous 24 h. + +#### 8.5.3 Déploiement (instance communale) +- L’équipe **propose** la méthode de packaging (ex. Docker Compose, VM image, .deb) permettant une installation sur un serveur Linux municipal **en < 1 h**. +- Un **script “update / rollback”** doit être livré (ou procédure équivalente). +- Un **pipeline CI/CD** minimal (tests + build image) est requis ; l’école fournit son dépôt Git / runner. +- Les journaux applicatifs et les métriques doivent pouvoir être branchés sur l’outillage de supervision de la commune (format libre, à documenter). + +#### 8.5.4 Mode faible connectivité +Service Worker : mise en cache des ressources critiques et file d’attente locale (≤ 24 h) pour l’agenda et la messagerie. + +#### 8.5.5 Sécurité technique +TLS 1.3, en-tête HSTS 12 mois, scan d’images conteneurs (CVE > 7 bloquantes), secrets via Vault ou fichier .env chiffré, rotation 90 j. + +# 9. Améliorations futures possibles + +Certaines fonctionnalités ne sont pas prioritaires dans la première version de P'titsPas, mais sont identifiées comme des évolutions possibles à forte valeur ajoutée pour les utilisateurs et les collectivités. + +## 9.1 Modules activables + +Ces modules peuvent être activés ou non par les administrateurs, selon la politique de la mairie ou les ressources disponibles. + +### 9.1.1 Blog RPE + +- Le gestionnaire peut publier des actualités dans un blog visible depuis le tableau de bord des parents et des assistantes maternelles. +- Il peut activer ou désactiver les commentaires. +- Il peut aussi publier des sondages ou cibler certaines publications (parents uniquement, assistantes maternelles uniquement, ou tous). +- La mairie (via l’administrateur) peut également publier dans ce flux. + +### 9.1.2 IA de reformulation + +- Un système d’intelligence artificielle peut proposer une reformulation des messages écrits avant envoi. +- Utile pour clarifier un message contractuel ou technique. +- Le message IA est signalé comme tel. +- Une API externe (ChatGPT, Claude, etc.) peut être configurée par l’administrateur. + +### 9.1.3 Connexion Pajemploi (API URSSAF) + +- Permettrait un envoi automatique des données vers Pajemploi. +- ⚠️ Cette API nécessite une habilitation spécifique par l’URSSAF. +- Requiert un partenariat de confiance entre la collectivité et l’institution. + +### 9.1.4 Garde d’enfants “grands” + +- Permet à une assistante maternelle d’accueillir des enfants scolarisés en périscolaire. +- Requiert un agrément spécifique, indiqué dans le profil. +- Les parents peuvent déclarer un enfant comme scolarisé. + +### 9.1.5 Pré-remplissage des dossiers scolaires + +- À la fin d’un contrat, un rappel automatique peut être envoyé aux parents sur les dates d’inscription à l’école. +- Pièces jointes : formulaire d’inscription, prérempli avec les données de la fiche enfant. + +### 9.1.6 Dossier fratrie unique + +- Un seul dossier pour plusieurs enfants (fratrie simultanée). +- Par défaut, chaque enfant dispose actuellement de son propre dossier. +- Cette fonctionnalité regrouperait les démarches dans un flux unique. +- Contrats séparés, workflow mutualisé + - Un seul **dossier de recherche** et de mise en relation couvre l’ensemble de la fratrie. + - Au moment de la génération, **un contrat distinct** est créé pour chaque enfant (obligations légales inchangées), mais les étapes du workflow (recherche, rendez-vous, validation) ne sont effectuées qu’une fois. + - Les parents et l’assistante maternelle n’ont donc qu’une seule demande à gérer, tandis que la plateforme conserve la granularité nécessaire pour la paie et les déclarations Pajemploi. + - Les écrans doivent afficher un badge « Fratrie » permettant de basculer d’un contrat à l’autre. + +#### 9.1.7 Signature électronique des contrats +- Intégration d’un prestataire de signature qualifiée (eIDAS) pour rendre le processus 100 % dématérialisé. +- Bénéfices : gain de temps, valeur probante renforcée, archivage automatisé. +- Complexité : coûts de licence, vérification d’identité, circuit API. + +#### 9.1.8 Intégration LDAP / SSO (agents municipaux) +- Connexion via annuaire LDAP ou fournisseur SAML 2. +- Les comptes parents / nounous restent sur le login interne. + +#### 9.1.9 Base de données externe PostgreSQL +- Possibilité de connecter l’application à un cluster PostgreSQL déjà géré par la DSI. +- Scripts d’initialisation et de migration fournis (Flyway/Liquibase). + +#### 9.1.10 Offre SaaS mutualisée +- Hébergement centralisé pour les communes sans infrastructure locale. +- Facturation à l’usage ; supervision et sauvegardes opérées par l’éditeur. + +#### 9.1.11 Mise en conformité RGAA (niveau AA) +- Audit complet : contrastes, navigation clavier, rôles ARIA. +- Correctifs design et tests utilisateurs. +- Priorité : **amélioration future**. + +#### 9.1.12 Centre d’aide / support utilisateur +- FAQ consultable + formulaire de ticket. +- Tableau de bord « Support » pour la mairie. +- Possibilité d’intégrer un chatbot d’assistance. +- Priorité : **amélioration future**. + +## 9.2 Stratégie d’activation + +- Chaque module peut être activé depuis le tableau de bord administrateur +- Une section “Modules complémentaires” permet : + - De consulter les modules disponibles + - D’activer/désactiver chaque fonctionnalité + - D’ajouter les API externes nécessaires (IA, URSSAF…) + +- Les modules activés sont ensuite visibles dans l’interface utilisateur concernée (parents, assistantes maternelles, gestionnaires). + +# 10. Synthèse finale + +P'titsPas est bien plus qu’un simple outil de gestion de la garde d’enfants. C’est une plateforme publique, inclusive et responsable, au service des familles, des assistantes maternelles et des collectivités locales. + +## 10.1 Valeurs portées + +P'titsPas s’inscrit dans une démarche de service public, avec des fondements clairs : + +- **Neutralité** : pas de système de notation, pas de mise en concurrence biaisée. +- **Équité** : tous les utilisateurs bénéficient des mêmes droits d’usage. +- **Transparence** : chaque processus est lisible, traçable, et validé par les deux parties. +- **Accessibilité** : l’interface est pensée pour les téléphones comme pour les ordinateurs. + +## 10.2 Bénéfices pour les utilisateurs + +| Acteur | Bénéfices directs | +|-------------------------|---------------------------------------------------------------------------| +| **Parents** | Simplicité de recherche, centralisation des échanges, aide Pajemploi | +| **Assistantes maternelles** | Gain de temps, traçabilité des contrats, autonomie de gestion | +| **Gestionnaires** | Suivi global des situations, outils de médiation, organisation d’événements | +| **Administrateurs** | Supervision complète, gouvernance multi-rôle, contrôle des données | + +## 10.3 Périmètre fonctionnel de la V1 + +Fonctionnalités incluses dès la première version : +- Création de comptes +- Recherche et sélection de nounous +- Suivi des dossiers +- Génération de contrat et avenants +- Messagerie +- Gestion des événements et agenda +- Aide à la paie Pajemploi +- Gestion des heures supplémentaires +- Suivi RGPD et administration + +## 10.4 Perspectives + +La structure du produit permet une montée en charge progressive : +- Activation des modules complémentaires (blog, IA, API Pajemploi) +- Adaptation aux nouvelles attentes des mairies +- Déploiement intercommunal possible +- Intégration avec les portails citoyens des collectivités + +P'titsPas incarne une nouvelle manière de soutenir les parcours de garde dans les territoires. C’est une application qui place **l’humain**, **la clarté** et **le lien de confiance** au cœur du service. + +## 10.5 Propriété intellectuelle et licence de l’application + +- Tout le code source, la marque « P'titsPas » et la documentation associée demeurent la **propriété exclusive de l’éditeur** (Julien). +- Les communes clientes reçoivent une **licence d’utilisation non exclusive, non transférable** : + - droit d’installer et d’exécuter l’application dans leur infrastructure, + - droit d’effectuer les sauvegardes nécessaires à l’exploitation, + - interdiction de distribuer, vendre ou mettre le code à disposition de tiers sans accord écrit. +- Le code source n’est pas publié sous une licence open-source ; il peut être remis **sous clause d’escrow** (tiers séquestre) pour garantir la maintenance en cas de défaillance de l’éditeur. +- Les étudiants ou prestataires contribuant au projet cèdent leurs droits patrimoniaux à l’éditeur via un accord de cession signé avant livraison. +- Toute personnalisation réalisée pour une commune fait partie intégrante du produit et est couverte par la licence ci-dessus, sauf stipulation contraire. + +--- + +*Fin du document* + diff --git a/docs/02_ARCHITECTURE.md b/docs/02_ARCHITECTURE.md new file mode 100644 index 0000000..6bf0516 --- /dev/null +++ b/docs/02_ARCHITECTURE.md @@ -0,0 +1,72 @@ +# 🏗️ Architecture du projet P'tits Pas + +## 📁 Structure du projet + +Ce projet est organisé en **3 dépôts Git distincts** : + +- **`ptitspas-frontend`** : Application Flutter (interface utilisateur) +- **`ptitspas-backend`** : API NestJS (serveur backend) +- **`ptitspas-database`** : Configuration PostgreSQL et migrations + +## 🌍 Deux environnements supportés + +### 🚀 **Environnement de production (serveur)** +- **Localisation** : `/home/ynov/project/` sur le serveur +- **Configuration** : `docker-compose.yml` + `.env` +- **Domaine** : `https://ynov.ptits-pas.fr` +- **Reverse proxy** : Traefik avec SSL automatique +- **Gestion** : Déploiement automatique via webhooks Gitea + +### 💻 **Environnement de développement (local)** +- **Configuration** : `docker-compose.dev.yml` + `.env.example` dans chaque dépôt +- **Accès** : Ports locaux (3000, 8000, 8080, 5432) +- **Hot reload** : Modifications de code prises en compte instantanément +- **Base locale** : PostgreSQL indépendante avec PgAdmin + +## 🏃‍♂️ Guide de démarrage développeur + +### 1. **Backend** (à démarrer en premier) +```bash +git clone +cd ptitspas-backend +cp .env.example .env +docker compose -f docker-compose.dev.yml up -d +``` +**Accès** : http://localhost:3000/api + +### 2. **Frontend** +```bash +git clone +cd ptitspas-frontend +cp .env.example .env +docker compose -f docker-compose.dev.yml up -d +``` +**Accès** : http://localhost:8000 + +### 3. **PgAdmin** (inclus avec le backend) +**Accès** : http://localhost:8080 +- Email : admin@localhost +- Mot de passe : admin123 + +## 🔄 Workflow de développement + +1. **Développer localement** avec les `docker-compose.dev.yml` +2. **Tester les modifications** sur http://localhost:8000 +3. **Commiter et pousser** dans les dépôts respectifs +4. **Déploiement automatique** sur le serveur via webhooks + +## 🗂️ Fichiers de configuration + +| Fichier | Usage | Localisation | +|---------|-------|--------------| +| `docker-compose.yml` | Production (serveur) | `/home/ynov/project/` | +| `docker-compose.dev.yml` | Développement (local) | Chaque dépôt | +| `.env` | Variables production | Serveur uniquement | +| `.env.example` | Template développement | Chaque dépôt | + +## 🔒 Sécurité + +- ✅ Fichiers `.env` **non versionnés** (ajoutés au `.gitignore`) +- ✅ Variables sensibles **externalisées** +- ✅ Configurations **séparées** dev/prod +- ✅ SSL automatique en production via Let's Encrypt diff --git a/docs/03_DEPLOYMENT.md b/docs/03_DEPLOYMENT.md new file mode 100644 index 0000000..2e94c33 --- /dev/null +++ b/docs/03_DEPLOYMENT.md @@ -0,0 +1,144 @@ +# 🚀 Déploiement YNOV - Architecture Complète + +## 🌐 URLs d'accès + +- **Application principale** : https://ynov.ptits-pas.fr/ +- **API Backend** : https://ynov.ptits-pas.fr/api/ +- **Administration DB** : https://ynov.ptits-pas.fr/pgadmin/ + +## 🏗️ Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Frontend │ │ Backend │ │ Database │ +│ (Flutter) │───▶│ (NestJS) │───▶│ (PostgreSQL) │ +│ ynov-frontend │ │ ynov-backend │ │ ynov-postgres │ +│ Port: 80 │ │ Port: 3000 │ │ Port: 5432 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + └───────────────────────┼───────────────────────┘ + │ + ynov_network (réseau interne) + │ + proxy_network (Traefik) + │ + Internet +``` + +## 📁 Structure des fichiers + +``` +/home/ynov/project/ +├── docker-compose.yml # Orchestration globale +├── backend/ +│ ├── Dockerfile # Build NestJS +│ ├── src/ # Code source +│ └── package.json # Dépendances +├── frontend/ +│ └── frontend/ +│ ├── Dockerfile # Build Flutter +│ ├── pubspec.yaml # Config Flutter +│ └── lib/ # Code source +└── database/ + ├── docker-compose.yml # Config spécifique DB + └── migrations/ # Scripts SQL +``` + +## 🔑 Credentials + +### Base de données +- **Host** : `ynov-postgres` (interne) ou `localhost:5433` (externe) +- **User** : `admin` +- **Password** : `admin123` +- **Database** : `ptitpas_db` + +### PgAdmin +- **Email** : `admin@ynov.local` +- **Password** : `admin123` + +## 🚀 Commandes de déploiement + +### Déploiement complet +```bash +cd /home/ynov/project +docker compose up -d --build +``` + +### Déploiement par service +```bash +# Base de données seulement +docker compose up -d database pgadmin + +# Backend seulement +docker compose up -d --build backend + +# Frontend seulement +docker compose up -d --build frontend +``` + +### Monitoring +```bash +# Voir les logs +docker compose logs -f + +# Statut des services +docker compose ps + +# Redémarrer un service +docker compose restart backend +``` + +## 🔧 Configuration réseau + +### Réseaux Docker +- **ynov_network** : Communication interne entre services +- **proxy_network** : Exposition via Traefik + +### Priorités Traefik +- **Frontend** : Priority 10 (plus basse = défaut) +- **Backend** : Priority 20 +- **PgAdmin** : Priority 30 (plus haute = prioritaire) + +## 📝 Variables d'environnement + +### Backend +- `DATABASE_URL` : Connexion PostgreSQL +- `NODE_ENV` : Mode production + +### Frontend +- `API_URL` : URL de l'API backend + +## 🔄 Webhooks de déploiement + +### Repository Frontend +- **Trigger** : Push sur `main` +- **Action** : `docker compose up -d --build frontend` + +### Repository Backend +- **Trigger** : Push sur `main` +- **Action** : `docker compose up -d --build backend` + +### Repository Database +- **Trigger** : Push sur `main` +- **Action** : `docker compose up -d --build database` + +## 🛠️ Dépannage + +### Logs par service +```bash +docker compose logs frontend +docker compose logs backend +docker compose logs database +``` + +### Rebuild complet +```bash +docker compose down +docker compose up -d --build --force-recreate +``` + +### Vérifier la connectivité réseau +```bash +docker network ls +docker network inspect project_ynov_network +``` diff --git a/docs/04_ROADMAP-GENERALE.md b/docs/04_ROADMAP-GENERALE.md new file mode 100644 index 0000000..35c587d --- /dev/null +++ b/docs/04_ROADMAP-GENERALE.md @@ -0,0 +1,330 @@ +# 🗺️ Roadmap Générale - Projet P'titsPas + +**Version** : 1.0 +**Date** : 28 Novembre 2025 +**Auteur** : Équipe PtitsPas + +--- + +## ⚠️ Avertissement + +Les **Phases 2, 3, 4+** sont des **ébauches indicatives** qui seront affinées au fur et à mesure du développement et des retours utilisateurs. Certaines fonctionnalités mentionnées (comme la facturation) ne seront peut-être pas développées ou seront remplacées par d'autres priorités. + +**Seule la Phase 1 est détaillée et validée.** + +--- + +## 🎯 Vue d'ensemble + +| Phase | Focus | Estimation | Statut | +|-------|-------|------------|--------| +| **Phase 1** | Comptes & Auth | ~173h | ✅ Détaillée (61 tickets) | +| **Phase 2** | Recherche & Contact | ~100h | 📋 Ébauche | +| **Phase 3** | Contrats & Planning | ~120h | 📋 Ébauche | +| **Phase 4** | Suivi & Avancé | ~140h+ | 📋 Ébauche | +| **Phase 5+** | Optimisations | ~200h+ | 📋 Ébauche | +| **TOTAL** | | **~733h+** | | + +--- + +## 📦 Phase 1 (v1.0.0) - 🔐 Création de comptes & Authentification + +**Objectif** : MVP fonctionnel avec gestion des utilisateurs + +### Fonctionnalités + +- ✅ Configuration système (on-premise) +- ✅ Authentification & Sécurité +- ✅ Inscription Parents (workflow 6 étapes) +- ✅ Inscription Assistantes Maternelles (workflow 5 panneaux) +- ✅ Validation par Gestionnaires (dashboard 2 onglets) +- ✅ Documents légaux (CGU/Privacy avec versioning) +- ✅ Upload photos (enfants, AM) +- ✅ Notifications email (validation, refus, création MDP) +- ✅ Logging & Monitoring +- ✅ Tests & Documentation + +### Versions incrémentales + +| Version | Objectif | Tickets | Estimation | +|---------|----------|---------|------------| +| **0.1.0** | MVP Fonctionnel | ~21 | ~45h | +| **0.2.0** | Sécurité & RGPD | ~10 | ~35h | +| **0.3.0** | Interfaces Complètes | ~17 | ~52h | +| **0.4.0** | Tests & Documentation | ~6 | ~24h | +| **0.5.0** | Monitoring & Optimisations | ~7 | ~17h | +| **1.0.0** | 🎉 **Release Phase 1** | **61** | **~173h** | + +### Livrable + +Application installable avec création et validation de comptes utilisateurs. + +**Référence** : [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md) + +--- + +## 📦 Phase 2 (v2.0.0) - 🤝 Mise en relation & Communication + +**Objectif** : Permettre aux parents de trouver et contacter des assistantes maternelles + +### Fonctionnalités (ébauche) + +- 🔍 **Recherche d'AM** + - Recherche par critères (ville, capacité, disponibilité, tarifs) + - Filtres avancés + - Géolocalisation (optionnel) + +- 👤 **Profils détaillés AM** + - Présentation complète + - Photos du lieu de garde + - Expérience et qualifications + - Avis/Témoignages (optionnel) + +- 💬 **Messagerie interne** + - Conversations sécurisées Parent ↔ AM + - Pièces jointes + - Historique des échanges + +- 📨 **Demandes de contact** + - Workflow de demande Parent → AM + - Validation/Refus par AM + - Notifications + +- ⭐ **Favoris/Shortlist** + - AM sauvegardées par parents + - Comparaison de profils + +### Estimation + +~100h (à affiner) + +### Livrable + +Parents peuvent trouver, consulter et contacter des assistantes maternelles. + +--- + +## 📦 Phase 3 (v3.0.0) - 📄 Contrats & Planning + +**Objectif** : Formaliser les gardes et gérer les plannings + +### Fonctionnalités (ébauche) + +- 📄 **Gestion des contrats** + - Création contrats (modèle type personnalisable) + - Signature électronique ou validation + - Stockage documents contractuels (PDF) + - Historique des contrats + - Renouvellement/Modification + +- 📅 **Planning & Disponibilités** + - Calendrier AM (disponibilités, absences, congés) + - Réservations/Demandes de garde + - Validation/Refus par AM + - Vue planning Parent (enfants gardés) + - Alertes conflits de planning + - Export calendrier (iCal) + +### Estimation + +~120h (à affiner) + +### Livrable + +Contrats formalisés + Planning opérationnel pour gérer les gardes. + +--- + +## 📦 Phase 4 (v4.0.0) - 📊 Suivi & Fonctionnalités avancées + +**Objectif** : Suivi quotidien des enfants et fonctionnalités complémentaires + +### Fonctionnalités (ébauche) + +- 📔 **Suivi des Enfants (Carnet de liaison numérique)** + - Activités quotidiennes (repas, sieste, jeux) + - Photos/Vidéos sécurisées (partage Parent ↔ AM) + - Notes/Observations + - Suivi médical (médicaments, allergies, vaccins) + - Historique complet par enfant + - Export PDF (bilan mensuel) + +- 🎯 **Autres fonctionnalités à définir** + - ⚠️ **Pas de facturation** (décision validée) + - Fonctionnalités à déterminer selon retours utilisateurs Phase 2 et 3 + +### Estimation + +~140h+ (à affiner) + +### Livrable + +Suivi quotidien des enfants + Fonctionnalités complémentaires. + +--- + +## 📦 Phase 5+ (v5.0.0+) - 🚀 Optimisations & Améliorations + +**Objectif** : Optimisations, monitoring, et fonctionnalités premium + +### Fonctionnalités (ébauche) + +#### 📊 Statistiques & Reporting +- Dashboard gestionnaire (stats inscriptions, validations) +- Rapports collectivité (CSV/PDF) +- Graphiques évolution +- Tableaux de bord personnalisés + +#### 🔒 RGPD avancé +- Droit à l'oubli (suppression compte) +- Export données personnelles (portabilité) +- Anonymisation automatique comptes inactifs +- Audit trail complet + +#### 📈 Monitoring & Infrastructure +- Métriques système (CPU, RAM, BDD) +- Dashboard monitoring admin +- Sauvegarde automatique BDD (cron) +- Procédures de restauration +- Alertes automatiques + +#### 📚 Documentation & Formation +- Guides utilisateur (Gestionnaire, Parent, AM) +- Vidéos tutoriels +- FAQ interactive +- Base de connaissances + +#### 🎨 Améliorations UX +- Mode sombre +- Notifications push (PWA) +- Accessibilité (WCAG 2.1) +- Multi-langue (i18n) +- Responsive avancé + +#### 🌟 Fonctionnalités Premium (optionnel) +- Géolocalisation AM (carte interactive) +- Système d'avis/notation +- Badges/Certifications AM +- Intégrations tierces (CAF, etc.) +- Application mobile native + +### Estimation + +~200h+ (à affiner) + +### Livrable + +Application mature, optimisée et riche en fonctionnalités. + +**Référence** : [25_PHASE-2-BACKLOG.md](./25_PHASE-2-BACKLOG.md) (anciennes fonctionnalités techniques) + +--- + +## 🎯 Logique de progression + +``` +Phase 1 : "Je peux créer un compte" + ↓ +Phase 2 : "Je peux trouver et contacter une AM" + ↓ +Phase 3 : "Je peux signer un contrat et gérer le planning" + ↓ +Phase 4 : "Je peux suivre mon enfant au quotidien" + ↓ +Phase 5+ : "L'application est optimisée et riche en fonctionnalités" +``` + +--- + +## 🔢 Schéma de versioning + +``` +X.Y.Z + +X = Phase majeure (0 = dev Phase 1, 1 = Phase 1 livrée, 2 = Phase 2 livrée, etc.) +Y = Version incrémentale dans la phase (0.1, 0.2, 0.3... → 1.0) +Z = Patch/Hotfix (0 par défaut, incrémenté pour corrections) + +Exemples : +- 0.1.0 → Phase 1 en dev, Version 1 (MVP) +- 0.1.1 → Phase 1 en dev, Version 1, Patch 1 (correction bug) +- 0.2.0 → Phase 1 en dev, Version 2 (Sécurité) +- 1.0.0 → Livraison finale Phase 1 +- 1.0.1 → Patch Phase 1 +- 2.0.0 → Livraison finale Phase 2 +- 3.0.0 → Livraison finale Phase 3 +``` + +--- + +## 📅 Critères de passage entre phases + +### Phase 1 → Phase 2 +- ✅ Phase 1 terminée (61 tickets) +- ✅ Application déployée en production (au moins 1 collectivité) +- ✅ Utilisateurs réels (au moins 10 comptes validés) +- ✅ Feedback terrain collecté +- ✅ Bugs critiques corrigés + +### Phase 2 → Phase 3 +- ✅ Phase 2 terminée +- ✅ Recherche et messagerie utilisées activement +- ✅ Au moins 5 mises en relation réussies +- ✅ Feedback utilisateurs positif +- ✅ Besoin de formalisation des contrats exprimé + +### Phase 3 → Phase 4 +- ✅ Phase 3 terminée +- ✅ Contrats et planning utilisés activement +- ✅ Au moins 10 contrats signés +- ✅ Feedback utilisateurs positif +- ✅ Besoin de suivi quotidien exprimé + +### Phase 4 → Phase 5+ +- ✅ Phase 4 terminée +- ✅ Application stable en production +- ✅ Base utilisateurs significative (50+ comptes actifs) +- ✅ Demandes d'optimisations et fonctionnalités avancées + +--- + +## 📝 Notes importantes + +1. **Flexibilité** : Cette roadmap est indicative et sera ajustée en fonction : + - Des retours utilisateurs + - Des priorités des collectivités + - Des contraintes techniques découvertes + - Des évolutions réglementaires + +2. **Priorisation** : Les fonctionnalités de chaque phase peuvent être réorganisées selon : + - L'urgence métier + - La valeur ajoutée + - La complexité technique + - Les dépendances + +3. **Décisions actées** : + - ❌ Pas de facturation automatique (gestion externe) + - ❌ Pas de SMS (email uniquement) + - ✅ Application on-premise (auto-hébergée) + - ✅ Configuration dynamique (pas de hardcoding) + +4. **Documentation** : Chaque phase aura sa propre documentation détaillée avant démarrage. + +--- + +## 📚 Documents de référence + +- [00_INDEX.md](./00_INDEX.md) - Index général de la documentation +- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md) - Cahier des charges v1.3 +- [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow création de comptes +- [23_LISTE-TICKETS.md](./23_LISTE-TICKETS.md) - Liste des 61 tickets Phase 1 +- [24_DECISIONS-PROJET.md](./24_DECISIONS-PROJET.md) - Décisions architecturales +- [25_PHASE-2-BACKLOG.md](./25_PHASE-2-BACKLOG.md) - Anciennes fonctionnalités techniques + +--- + +**Dernière mise à jour** : 28 Novembre 2025 +**Version** : 1.0 +**Statut** : 📋 Roadmap indicative - Phase 1 détaillée et validée + + diff --git a/docs/10_DATABASE.md b/docs/10_DATABASE.md new file mode 100644 index 0000000..1a95975 --- /dev/null +++ b/docs/10_DATABASE.md @@ -0,0 +1,421 @@ +# 🗄️ Documentation Base de Données + +## Vue d'ensemble + +L'application PtitsPas utilise **PostgreSQL 14** avec l'extension **pgcrypto** pour la gestion des UUID. + +**Nom de la base** : `ptitpas_db` +**Port** : `5432` +**Conteneur Docker** : `ptitspas-postgres` + +## Schéma de la base de données + +### Types ENUM + +La base de données utilise plusieurs types énumérés PostgreSQL : + +| Type ENUM | Valeurs possibles | Usage | +|-----------|------------------|-------| +| `role_type` | `parent`, `gestionnaire`, `super_admin`, `assistante_maternelle`, `administrateur` | Rôles des utilisateurs | +| `genre_type` | `H`, `F`, `Autre` | Genre des utilisateurs et enfants | +| `statut_utilisateur_type` | `en_attente`, `actif`, `suspendu` | Statut du compte utilisateur | +| `statut_enfant_type` | `a_naitre`, `actif`, `scolarise` | Statut de l'enfant | +| `statut_dossier_type` | `envoye`, `accepte`, `refuse` | Statut de la candidature | +| `statut_contrat_type` | `brouillon`, `en_attente_signature`, `valide`, `resilie` | Statut du contrat | +| `statut_avenant_type` | `propose`, `accepte`, `refuse` | Statut des avenants au contrat | +| `type_evenement_type` | `absence_enfant`, `conge_am`, `conge_parent`, `arret_maladie_am`, `evenement_rpe` | Type d'événement | +| `statut_evenement_type` | `propose`, `valide`, `refuse` | Statut de l'événement | +| `statut_validation_type` | `en_attente`, `valide`, `refuse` | Statut de validation générique | + +--- + +## Tables + +### 1. `utilisateurs` + +Table centrale pour tous les types d'utilisateurs. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `email` | VARCHAR(255) | NOT NULL, UNIQUE | Email (avec validation regex) | +| `password` | TEXT | NOT NULL | Mot de passe hashé (bcrypt) | +| `prenom` | VARCHAR(100) | | Prénom | +| `nom` | VARCHAR(100) | | Nom de famille | +| `genre` | genre_type | | Genre de l'utilisateur | +| `role` | role_type | NOT NULL | Rôle de l'utilisateur | +| `statut` | statut_utilisateur_type | DEFAULT 'en_attente' | Statut du compte | +| `telephone` | VARCHAR(20) | | Téléphone principal | +| `adresse` | TEXT | | Adresse complète | +| `photo_url` | TEXT | | URL de la photo de profil | +| `consentement_photo` | BOOLEAN | DEFAULT false | Consentement photo | +| `date_consentement_photo` | TIMESTAMPTZ | | Date du consentement | +| `changement_mdp_obligatoire` | BOOLEAN | DEFAULT false | Force changement de MDP | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date de création | +| `modifie_le` | TIMESTAMPTZ | DEFAULT now() | Dernière modification | +| `ville` | VARCHAR(150) | | Ville | +| `code_postal` | VARCHAR(10) | | Code postal | +| `mobile` | VARCHAR(20) | | Téléphone mobile | +| `telephone_fixe` | VARCHAR(20) | | Téléphone fixe | +| `profession` | VARCHAR(150) | | Profession | +| `situation_familiale` | VARCHAR(50) | | Situation familiale | +| `date_naissance` | DATE | | Date de naissance | + +**Contraintes** : +- Email validé par regex : `^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$` + +--- + +### 2. `assistantes_maternelles` + +Extension de la table `utilisateurs` pour les assistantes maternelles. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id_utilisateur` | UUID | PRIMARY KEY, FK → utilisateurs(id) | Référence à l'utilisateur | +| `numero_agrement` | VARCHAR(50) | | Numéro d'agrément | +| `nir_chiffre` | CHAR(15) | | NIR (Sécurité sociale) | +| `nb_max_enfants` | INT | | Capacité maximale d'accueil | +| `biographie` | TEXT | | Présentation | +| `disponible` | BOOLEAN | DEFAULT true | Disponibilité | +| `ville_residence` | VARCHAR(100) | | Ville de résidence | +| `date_agrement` | DATE | | Date d'obtention de l'agrément | +| `annee_experience` | SMALLINT | | Années d'expérience | +| `specialite` | VARCHAR(100) | | Spécialités | +| `place_disponible` | INT | | Nombre de places disponibles | + +**Cascade** : `ON DELETE CASCADE` (suppression si utilisateur supprimé) + +--- + +### 3. `parents` + +Extension de la table `utilisateurs` pour les parents. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id_utilisateur` | UUID | PRIMARY KEY, FK → utilisateurs(id) | Référence à l'utilisateur | +| `id_co_parent` | UUID | FK → utilisateurs(id) | Référence au co-parent (optionnel) | + +**Cascade** : `ON DELETE CASCADE` + +--- + +### 4. `enfants` + +Table des enfants pris en charge. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `statut` | statut_enfant_type | | Statut de l'enfant | +| `prenom` | VARCHAR(100) | | Prénom | +| `nom` | VARCHAR(100) | | Nom | +| `genre` | genre_type | | Genre | +| `date_naissance` | DATE | | Date de naissance | +| `date_prevue_naissance` | DATE | | Date prévue (si à naître) | +| `photo_url` | TEXT | | URL de la photo | +| `consentement_photo` | BOOLEAN | DEFAULT false | Consentement photo | +| `date_consentement_photo` | TIMESTAMPTZ | | Date du consentement | +| `est_multiple` | BOOLEAN | DEFAULT false | Indique si grossesse multiple | + +--- + +### 5. `enfants_parents` + +Table de liaison entre enfants et parents (relation N:N). + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id_parent` | UUID | FK → parents(id_utilisateur) | Référence au parent | +| `id_enfant` | UUID | FK → enfants(id) | Référence à l'enfant | + +**Clé primaire composite** : `(id_parent, id_enfant)` +**Cascade** : `ON DELETE CASCADE` + +--- + +### 6. `dossiers` + +Dossiers de candidature des parents pour une assistante maternelle. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `id_parent` | UUID | FK → parents(id_utilisateur) | Parent demandeur | +| `id_enfant` | UUID | FK → enfants(id) | Enfant concerné | +| `presentation` | TEXT | | Présentation de la demande | +| `type_contrat` | VARCHAR(50) | | Type de contrat souhaité | +| `repas` | BOOLEAN | DEFAULT false | Demande de repas | +| `budget` | NUMERIC(10,2) | | Budget disponible | +| `planning_souhaite` | JSONB | | Planning souhaité (format JSON) | +| `statut` | statut_dossier_type | DEFAULT 'envoye' | Statut du dossier | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date de création | +| `modifie_le` | TIMESTAMPTZ | DEFAULT now() | Dernière modification | + +**Cascade** : `ON DELETE CASCADE` + +--- + +### 7. `messages` + +Messages échangés dans le cadre d'un dossier. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `id_dossier` | UUID | FK → dossiers(id) | Dossier lié | +| `id_expediteur` | UUID | FK → utilisateurs(id) | Expéditeur | +| `contenu` | TEXT | | Contenu du message | +| `re_redige_par_ia` | BOOLEAN | DEFAULT false | Message réécrit par IA | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date d'envoi | + +**Cascade** : `ON DELETE CASCADE` + +--- + +### 8. `contrats` + +Contrats conclus entre parents et assistantes maternelles. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `id_dossier` | UUID | UNIQUE, FK → dossiers(id) | Dossier source (1:1) | +| `planning` | JSONB | | Planning défini (format JSON) | +| `tarif_horaire` | NUMERIC(6,2) | | Tarif horaire | +| `indemnites_repas` | NUMERIC(6,2) | | Indemnités repas | +| `date_debut` | DATE | | Date de début du contrat | +| `statut` | statut_contrat_type | DEFAULT 'brouillon' | Statut du contrat | +| `signe_parent` | BOOLEAN | DEFAULT false | Signature parent | +| `signe_am` | BOOLEAN | DEFAULT false | Signature assistante maternelle | +| `finalise_le` | TIMESTAMPTZ | | Date de finalisation | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date de création | +| `modifie_le` | TIMESTAMPTZ | DEFAULT now() | Dernière modification | + +**Cascade** : `ON DELETE CASCADE` + +--- + +### 9. `avenants_contrats` + +Modifications apportées aux contrats existants. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `id_contrat` | UUID | FK → contrats(id) | Contrat modifié | +| `modifications` | JSONB | | Détails des modifications (JSON) | +| `initie_par` | UUID | FK → utilisateurs(id) | Utilisateur initiateur | +| `statut` | statut_avenant_type | DEFAULT 'propose' | Statut de l'avenant | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date de création | +| `modifie_le` | TIMESTAMPTZ | DEFAULT now() | Dernière modification | + +**Cascade** : `ON DELETE CASCADE` + +--- + +### 10. `evenements` + +Événements liés au planning (absences, congés, etc.). + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `type` | type_evenement_type | | Type d'événement | +| `id_enfant` | UUID | FK → enfants(id) | Enfant concerné | +| `id_am` | UUID | FK → utilisateurs(id) | Assistante maternelle | +| `id_parent` | UUID | FK → parents(id_utilisateur) | Parent | +| `cree_par` | UUID | FK → utilisateurs(id) | Créateur de l'événement | +| `date_debut` | TIMESTAMPTZ | | Date de début | +| `date_fin` | TIMESTAMPTZ | | Date de fin | +| `commentaires` | TEXT | | Commentaires | +| `statut` | statut_evenement_type | DEFAULT 'propose' | Statut de l'événement | +| `delai_grace` | TIMESTAMPTZ | | Délai de grâce | +| `urgent` | BOOLEAN | DEFAULT false | Événement urgent | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date de création | +| `modifie_le` | TIMESTAMPTZ | DEFAULT now() | Dernière modification | + +**Cascade** : `ON DELETE CASCADE` + +--- + +### 11. `signalements_bugs` + +Signalements de bugs par les utilisateurs. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `id_utilisateur` | UUID | FK → utilisateurs(id) | Utilisateur signalant | +| `description` | TEXT | | Description du bug | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date du signalement | + +--- + +### 12. `uploads` + +Fichiers téléversés par les utilisateurs. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `id_utilisateur` | UUID | FK → utilisateurs(id), ON DELETE SET NULL | Utilisateur | +| `fichier_url` | TEXT | NOT NULL | URL du fichier | +| `type` | VARCHAR(50) | | Type de fichier | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date d'upload | + +--- + +### 13. `notifications` + +Notifications envoyées aux utilisateurs. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `id_utilisateur` | UUID | FK → utilisateurs(id) | Destinataire | +| `contenu` | TEXT | | Contenu de la notification | +| `lu` | BOOLEAN | DEFAULT false | Statut de lecture | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date de création | + +**Cascade** : `ON DELETE CASCADE` + +--- + +### 14. `validations` + +Validations génériques de données utilisateur. + +| Colonne | Type | Contraintes | Description | +|---------|------|-------------|-------------| +| `id` | UUID | PRIMARY KEY | Identifiant unique | +| `id_utilisateur` | UUID | FK → utilisateurs(id) | Utilisateur à valider | +| `type` | VARCHAR(50) | | Type de validation | +| `statut` | statut_validation_type | DEFAULT 'en_attente' | Statut | +| `cree_le` | TIMESTAMPTZ | DEFAULT now() | Date de demande | +| `modifie_le` | TIMESTAMPTZ | DEFAULT now() | Dernière modification | +| `valide_par` | UUID | FK → utilisateurs(id) | Validateur | +| `commentaire` | TEXT | | Commentaire du validateur | + +--- + +## Relations principales + +``` +utilisateurs (1) ──┬──> (1) assistantes_maternelles + ├──> (1) parents + └──> (N) messages + +parents (1) ───> (N) enfants_parents <─── (N) enfants + +parents (1) ───> (N) dossiers <─── (1) enfants +dossiers (1) ───> (N) messages +dossiers (1) ───> (1) contrats +contrats (1) ───> (N) avenants_contrats + +enfants (1) ───> (N) evenements +``` + +--- + +## Données initiales (SEED) + +### Super Administrateur par défaut + +**Email** : `admin@ptits-pas.fr` +**Mot de passe** : `4dm1n1strateur` +**Rôle** : `super_admin` +**Statut** : `actif` + +> ⚠️ **Sécurité** : Le mot de passe est hashé avec bcrypt (`$2b$12$...`). +> Il est **impératif** de changer ce mot de passe en production. + +--- + +## Migrations + +Les migrations sont gérées manuellement via le fichier SQL : + +**Fichier** : `/database/migrations/01_init.sql` + +### Appliquer les migrations + +```bash +# Depuis le conteneur backend +npx prisma migrate deploy + +# Ou manuellement depuis psql +psql -U admin -d ptitpas_db -f /database/migrations/01_init.sql +``` + +--- + +## Accès à la base de données + +### Via PgAdmin + +**URL** : `https://app.ptits-pas.fr/pgadmin` +**Email** : `admin@ptits-pas.fr` +**Mot de passe** : `admin123` + +**Configuration serveur** : +- Host : `ptitspas-postgres` +- Port : `5432` +- Database : `ptitpas_db` +- Username : `admin` +- Password : `admin123` + +### Via terminal (Docker) + +```bash +# Connexion au conteneur PostgreSQL +docker exec -it ptitspas-postgres psql -U admin -d ptitpas_db + +# Lister les tables +\dt + +# Voir le schéma d'une table +\d utilisateurs + +# Quitter +\q +``` + +--- + +## Recommandations de sécurité + +1. ✅ **Mots de passe hashés** avec bcrypt +2. ✅ **Validation email** via regex +3. ⚠️ **Changer les credentials par défaut en production** +4. ⚠️ **Créer un utilisateur read-only pour les analytics** +5. ⚠️ **Activer SSL/TLS pour les connexions PostgreSQL** +6. ✅ **Utiliser des UUID** plutôt que des identifiants séquentiels + +--- + +## Maintenance + +### Backup de la base + +```bash +docker exec ptitspas-postgres pg_dump -U admin ptitpas_db > backup.sql +``` + +### Restauration + +```bash +docker exec -i ptitspas-postgres psql -U admin ptitpas_db < backup.sql +``` + +### Vérifier la taille de la base + +```sql +SELECT pg_size_pretty(pg_database_size('ptitpas_db')); +``` + +--- + +**Dernière mise à jour** : Novembre 2025 + diff --git a/docs/11_API.md b/docs/11_API.md new file mode 100644 index 0000000..272e6a9 --- /dev/null +++ b/docs/11_API.md @@ -0,0 +1,873 @@ +# 🌐 Documentation API + +## Vue d'ensemble + +L'API PtitsPas est une API REST construite avec **NestJS**. + +**URL de base** : `https://app.ptits-pas.fr/api/v1` +**Format** : JSON +**Authentication** : JWT Bearer Token + +--- + +## Authentication + +Toutes les routes (sauf `/auth/login`, `/auth/register`, `/auth/refresh`) nécessitent un token JWT dans le header : + +```http +Authorization: Bearer +``` + +### Tokens + +L'API utilise deux types de tokens : +- **Access Token** : Durée de vie courte, utilisé pour les requêtes API +- **Refresh Token** : Durée de vie longue, utilisé pour renouveler l'access token + +--- + +## Endpoints + +### 🔐 Authentification (`/auth`) + +#### POST `/auth/login` + +Connexion d'un utilisateur. + +**Public** : ✅ Oui + +**Request Body** : +```json +{ + "email": "admin@ptits-pas.fr", + "password": "4dm1n1strateur" +} +``` + +**Response** (200) : +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "user": { + "id": "uuid", + "email": "admin@ptits-pas.fr", + "role": "super_admin", + "prenom": "Admin", + "nom": "Système" + } +} +``` + +**Errors** : +- `401` : Email ou mot de passe incorrect +- `403` : Compte suspendu ou en attente de validation + +--- + +#### POST `/auth/register` + +Inscription d'un nouvel utilisateur (parent ou assistante maternelle). + +**Public** : ✅ Oui + +**Request Body** : +```json +{ + "email": "parent@example.com", + "password": "motdepasse123", + "prenom": "Jean", + "nom": "Dupont", + "role": "parent", + "telephone": "0601020304" +} +``` + +**Response** (201) : +```json +{ + "message": "Inscription réussie. Votre compte est en attente de validation.", + "userId": "uuid" +} +``` + +**Errors** : +- `409` : Email déjà utilisé +- `400` : Données invalides + +--- + +#### POST `/auth/refresh` + +Rafraîchir l'access token à l'aide du refresh token. + +**Public** : ✅ Oui + +**Request Body** : +```json +{ + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +**Response** (200) : +```json +{ + "access_token": "nouveau_token...", + "refresh_token": "nouveau_refresh_token..." +} +``` + +**Errors** : +- `401` : Token de rafraîchissement invalide ou expiré + +--- + +#### GET `/auth/me` + +Récupérer le profil de l'utilisateur connecté. + +**Auth** : 🔒 Requis + +**Response** (200) : +```json +{ + "id": "uuid", + "email": "admin@ptits-pas.fr", + "role": "super_admin", + "prenom": "Admin", + "nom": "Système", + "statut": "actif" +} +``` + +--- + +#### POST `/auth/logout` + +Déconnexion (invalide le refresh token). + +**Auth** : 🔒 Requis + +**Response** (200) : +```json +{ + "message": "Déconnexion réussie" +} +``` + +--- + +### 👥 Utilisateurs (`/users`) + +**Auth** : 🔒 Requis pour toutes les routes + +#### GET `/users` + +Liste tous les utilisateurs. + +**Rôles autorisés** : `super_admin` + +**Response** (200) : +```json +[ + { + "id": "uuid", + "email": "user@example.com", + "role": "parent", + "prenom": "Jean", + "nom": "Dupont", + "statut": "actif", + "cree_le": "2025-01-15T10:30:00Z" + } +] +``` + +--- + +#### GET `/users/:id` + +Récupérer un utilisateur par son ID. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Response** (200) : +```json +{ + "id": "uuid", + "email": "user@example.com", + "role": "parent", + "prenom": "Jean", + "nom": "Dupont", + "telephone": "0601020304", + "statut": "actif" +} +``` + +**Errors** : +- `404` : Utilisateur non trouvé +- `403` : Accès refusé + +--- + +#### POST `/users` + +Créer un nouvel utilisateur. + +**Rôles autorisés** : `super_admin` + +**Request Body** : +```json +{ + "email": "newuser@example.com", + "password": "password123", + "role": "gestionnaire", + "prenom": "Marie", + "nom": "Martin" +} +``` + +**Response** (201) : +```json +{ + "id": "uuid", + "email": "newuser@example.com", + "role": "gestionnaire" +} +``` + +--- + +#### PATCH `/users/:id` + +Mettre à jour un utilisateur. + +**Rôles autorisés** : `super_admin` + +**Request Body** (tous les champs sont optionnels) : +```json +{ + "prenom": "Jean-Claude", + "telephone": "0612345678", + "adresse": "1 rue de la Paix, 75001 Paris" +} +``` + +**Response** (200) : +```json +{ + "id": "uuid", + "email": "user@example.com", + "prenom": "Jean-Claude", + "telephone": "0612345678" +} +``` + +--- + +#### PATCH `/users/:id/valider` + +Valider un compte utilisateur en attente. + +**Rôles autorisés** : `super_admin`, `gestionnaire`, `administrateur` + +**Request Body** : +```json +{ + "comment": "Compte validé après vérification des documents" +} +``` + +**Response** (200) : +```json +{ + "message": "Compte validé avec succès", + "userId": "uuid" +} +``` + +--- + +#### PATCH `/users/:id/suspendre` + +Suspendre un compte utilisateur. + +**Rôles autorisés** : `super_admin`, `gestionnaire`, `administrateur` + +**Request Body** : +```json +{ + "comment": "Compte suspendu pour non-respect des conditions" +} +``` + +**Response** (200) : +```json +{ + "message": "Compte suspendu", + "userId": "uuid" +} +``` + +--- + +#### DELETE `/users/:id` + +Supprimer un utilisateur. + +**Rôles autorisés** : `super_admin` + +**Response** (200) : +```json +{ + "message": "Utilisateur supprimé" +} +``` + +--- + +### 🏢 Gestionnaires (`/gestionnaires`) + +**Auth** : 🔒 Requis pour toutes les routes + +#### GET `/gestionnaires` + +Liste tous les gestionnaires. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Response** (200) : +```json +[ + { + "id": "uuid", + "email": "gestionnaire@ptits-pas.fr", + "role": "gestionnaire", + "prenom": "Sophie", + "nom": "Leroy" + } +] +``` + +--- + +#### GET `/gestionnaires/:id` + +Récupérer un gestionnaire par son ID. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Response** (200) : Identique à `/users/:id` + +--- + +#### POST `/gestionnaires` + +Créer un nouveau gestionnaire. + +**Rôles autorisés** : `super_admin` + +**Request Body** : +```json +{ + "email": "nouveau.gestionnaire@ptits-pas.fr", + "password": "password123", + "prenom": "Laurent", + "nom": "Bernard" +} +``` + +**Response** (201) : +```json +{ + "id": "uuid", + "email": "nouveau.gestionnaire@ptits-pas.fr", + "role": "gestionnaire" +} +``` + +--- + +#### PATCH `/gestionnaires/:id` + +Mettre à jour un gestionnaire. + +**Rôles autorisés** : `super_admin` + +**Request Body** : Identique à `/users/:id` (PATCH) + +--- + +### 👶 Parents (`/parents`) + +**Auth** : 🔒 Requis pour toutes les routes + +#### GET `/parents` + +Liste tous les parents. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Response** (200) : +```json +[ + { + "id_utilisateur": "uuid", + "id_co_parent": "uuid", + "utilisateur": { + "email": "parent@example.com", + "prenom": "Jean", + "nom": "Dupont" + } + } +] +``` + +--- + +#### GET `/parents/:id` + +Récupérer un parent par son ID utilisateur. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Response** (200) : +```json +{ + "id_utilisateur": "uuid", + "id_co_parent": "uuid", + "utilisateur": { + "email": "parent@example.com", + "prenom": "Jean", + "nom": "Dupont", + "telephone": "0601020304" + } +} +``` + +--- + +#### POST `/parents` + +Créer un nouveau parent. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Request Body** : +```json +{ + "email": "parent@example.com", + "password": "password123", + "prenom": "Jean", + "nom": "Dupont", + "telephone": "0601020304", + "id_co_parent": "uuid" // optionnel +} +``` + +**Response** (201) : Identique à GET + +--- + +#### PATCH `/parents/:id` + +Mettre à jour un parent. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Request Body** : +```json +{ + "id_co_parent": "uuid", + "telephone": "0612345678" +} +``` + +--- + +### 👧 Enfants (`/enfants`) + +**Auth** : 🔒 Requis pour toutes les routes + +#### GET `/enfants` + +Liste tous les enfants. + +**Rôles autorisés** : `super_admin`, `gestionnaire`, `administrateur` + +**Response** (200) : +```json +[ + { + "id": "uuid", + "prenom": "Alice", + "nom": "Dupont", + "genre": "F", + "date_naissance": "2020-05-15", + "statut": "actif" + } +] +``` + +--- + +#### GET `/enfants/:id` + +Récupérer un enfant par son ID. + +**Rôles autorisés** : `super_admin`, `gestionnaire`, `administrateur`, `parent` (si c'est son enfant) + +**Response** (200) : +```json +{ + "id": "uuid", + "prenom": "Alice", + "nom": "Dupont", + "genre": "F", + "date_naissance": "2020-05-15", + "statut": "actif", + "consentement_photo": false +} +``` + +--- + +#### POST `/enfants` + +Créer un nouvel enfant. + +**Rôles autorisés** : `parent` + +**Request Body** : +```json +{ + "prenom": "Alice", + "nom": "Dupont", + "genre": "F", + "date_naissance": "2020-05-15", + "statut": "actif" +} +``` + +**Response** (201) : Identique à GET + +--- + +#### PATCH `/enfants/:id` + +Mettre à jour un enfant. + +**Rôles autorisés** : `super_admin`, `administrateur`, `parent` (si c'est son enfant) + +**Request Body** : +```json +{ + "statut": "scolarise", + "consentement_photo": true +} +``` + +--- + +#### DELETE `/enfants/:id` + +Supprimer un enfant. + +**Rôles autorisés** : `super_admin` + +**Response** (200) : +```json +{ + "message": "Enfant supprimé" +} +``` + +--- + +### 👩‍🍼 Assistantes Maternelles (`/assistantes-maternelles`) + +**Auth** : 🔒 Requis pour toutes les routes + +#### GET `/assistantes-maternelles` + +Liste toutes les assistantes maternelles. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Response** (200) : +```json +[ + { + "id_utilisateur": "uuid", + "numero_agrement": "AM123456", + "nb_max_enfants": 4, + "place_disponible": 2, + "disponible": true, + "utilisateur": { + "email": "am@example.com", + "prenom": "Marie", + "nom": "Martin" + } + } +] +``` + +--- + +#### GET `/assistantes-maternelles/:id` + +Récupérer une assistante maternelle par son ID utilisateur. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Response** (200) : +```json +{ + "id_utilisateur": "uuid", + "numero_agrement": "AM123456", + "nir_chiffre": "123456789012345", + "nb_max_enfants": 4, + "place_disponible": 2, + "biographie": "Assistante maternelle depuis 10 ans...", + "disponible": true, + "ville_residence": "Paris", + "date_agrement": "2015-06-01", + "annee_experience": 10, + "specialite": "Pédagogie Montessori" +} +``` + +--- + +#### POST `/assistantes-maternelles` + +Créer une nouvelle assistante maternelle. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Request Body** : +```json +{ + "email": "am@example.com", + "password": "password123", + "prenom": "Marie", + "nom": "Martin", + "numero_agrement": "AM123456", + "nb_max_enfants": 4, + "ville_residence": "Paris" +} +``` + +**Response** (201) : Identique à GET + +--- + +#### PATCH `/assistantes-maternelles/:id` + +Mettre à jour une assistante maternelle. + +**Rôles autorisés** : `super_admin`, `gestionnaire` + +**Request Body** : +```json +{ + "place_disponible": 1, + "disponible": true, + "biographie": "Nouvelle bio..." +} +``` + +--- + +#### DELETE `/assistantes-maternelles/:id` + +Supprimer une assistante maternelle. + +**Rôles autorisés** : `super_admin`, `gestionnaire`, `administrateur` + +**Response** (200) : +```json +{ + "message": "Assistante maternelle supprimée" +} +``` + +--- + +### 🏠 Root (`/`) + +#### GET `/` + +Aperçu de l'API. + +**Public** : ✅ Oui + +**Response** (200) : +```json +{ + "message": "Bienvenue sur l'API PtitsPas", + "version": "1.0.0", + "endpoints": { + "auth": "/api/v1/auth", + "users": "/api/v1/users", + "parents": "/api/v1/parents", + "enfants": "/api/v1/enfants", + "assistantes-maternelles": "/api/v1/assistantes-maternelles", + "gestionnaires": "/api/v1/gestionnaires" + } +} +``` + +--- + +#### GET `/hello` + +Endpoint de test. + +**Public** : ✅ Oui + +**Response** (200) : +```text +Hello World! +``` + +--- + +## Codes d'erreur HTTP + +| Code | Signification | Explication | +|------|---------------|-------------| +| `200` | OK | Requête réussie | +| `201` | Created | Ressource créée | +| `400` | Bad Request | Données invalides | +| `401` | Unauthorized | Non authentifié ou token invalide | +| `403` | Forbidden | Accès refusé (rôle insuffisant) | +| `404` | Not Found | Ressource non trouvée | +| `409` | Conflict | Conflit (ex: email déjà existant) | +| `500` | Internal Server Error | Erreur serveur | + +--- + +## Format des réponses d'erreur + +```json +{ + "statusCode": 400, + "message": "Validation failed", + "error": "Bad Request" +} +``` + +Ou avec détails : + +```json +{ + "statusCode": 403, + "message": "Accès refusé : rôle insuffisant", + "error": "Forbidden" +} +``` + +--- + +## Authentification et Rôles + +### Hiérarchie des rôles + +1. **super_admin** : Accès total +2. **administrateur** : Gestion des utilisateurs +3. **gestionnaire** : Gestion des dossiers et validation +4. **assistante_maternelle** : Accès à ses propres données +5. **parent** : Accès à ses propres données et enfants + +### Guards + +L'API utilise deux guards : +- **AuthGuard** : Vérifie la validité du token JWT +- **RolesGuard** : Vérifie que l'utilisateur a le bon rôle + +### Décorateurs + +- `@Public()` : Route accessible sans authentification +- `@Roles(...roles)` : Restreint l'accès aux rôles spécifiés +- `@User()` : Injecte l'utilisateur connecté dans le contrôleur + +--- + +## Swagger / OpenAPI + +L'API expose automatiquement sa documentation Swagger : + +**URL** : `https://app.ptits-pas.fr/api/docs` + +Cette documentation interactive permet de : +- Visualiser tous les endpoints +- Tester les requêtes directement +- Voir les schémas de données + +--- + +## Exemples d'utilisation + +### Connexion et récupération du profil + +```bash +# 1. Login +curl -X POST https://app.ptits-pas.fr/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"admin@ptits-pas.fr","password":"4dm1n1strateur"}' + +# Response +{ + "access_token": "eyJhbGc...", + "refresh_token": "eyJhbGc..." +} + +# 2. Récupérer son profil +curl https://app.ptits-pas.fr/api/v1/auth/me \ + -H "Authorization: Bearer eyJhbGc..." +``` + +### Créer un parent + +```bash +curl -X POST https://app.ptits-pas.fr/api/v1/parents \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "email": "parent@example.com", + "password": "password123", + "prenom": "Jean", + "nom": "Dupont", + "telephone": "0601020304" + }' +``` + +--- + +## Limitations et Rate Limiting + +**Actuellement** : Aucune limitation n'est active. + +**À implémenter** : +- Rate limiting (ex: 100 requêtes/minute) +- Pagination pour les listes longues +- Filtrage et tri sur les endpoints de liste + +--- + +## Évolutions prévues + +- [ ] Endpoints pour les dossiers (`/dossiers`) +- [ ] Endpoints pour les contrats (`/contrats`) +- [ ] Endpoints pour les événements (`/evenements`) +- [ ] Endpoints pour les messages (`/messages`) +- [ ] Endpoints pour les notifications (`/notifications`) +- [ ] WebSocket pour les notifications en temps réel +- [ ] Upload de fichiers (`/uploads`) +- [ ] Génération de PDF (contrats) + +--- + +**Dernière mise à jour** : Novembre 2025 + diff --git a/docs/20_WORKFLOW-CREATION-COMPTE.md b/docs/20_WORKFLOW-CREATION-COMPTE.md new file mode 100644 index 0000000..df2ddc8 --- /dev/null +++ b/docs/20_WORKFLOW-CREATION-COMPTE.md @@ -0,0 +1,2288 @@ +# 📋 Documentation Technique - Workflow de Création de Compte + +**Version** : 1.0 +**Date** : 24 Novembre 2025 +**Auteur** : Équipe PtitsPas +**Référence CDC** : Cahier des Charges P'titsPas V1.3 + +--- + +## 📖 Table des matières + +1. [Vue d'ensemble](#vue-densemble) +2. [Acteurs](#acteurs) +3. [Workflow détaillé](#workflow-détaillé) +4. [Diagrammes de séquence](#diagrammes-de-séquence) +5. [Spécifications techniques](#spécifications-techniques) +6. [APIs utilisées](#apis-utilisées) +7. [Modèles de données](#modèles-de-données) +8. [Templates d'emails](#templates-demails) +9. [Tests et validation](#tests-et-validation) + +--- + +## 🎯 Vue d'ensemble + +### Objectif + +Implémenter le workflow complet de création et validation des comptes utilisateurs pour l'application P'titsPas, permettant : +- La création de gestionnaires par le super administrateur +- L'inscription autonome des parents et assistantes maternelles +- La validation des comptes par les gestionnaires +- La notification par email des décisions + +### Périmètre fonctionnel + +Ce workflow couvre la **Phase 1** du projet, correspondant au premier use case critique : +> "Permettre au super admin de créer des gestionnaires, qui pourront ensuite valider les inscriptions des parents et assistantes maternelles." + +### Référence CDC + +**Section 3.1** : Gestion des utilisateurs +**Section 3.2** : Processus de validation +**Section 4.5** : Notifications email + +--- + +## 👥 Acteurs + +### 1. Super Administrateur (`super_admin`) + +**Rôle** : Administrateur système avec tous les droits +**Identifiants initiaux** : +- Email : `admin@ptits-pas.fr` +- Mot de passe : `4dm1n1strateur` + +**Responsabilités** : +- Créer les comptes gestionnaires +- Accéder au panneau d'administration +- Gérer la configuration système + +**Référence** : Table `utilisateurs` avec `role = 'super_admin'` et `statut = 'actif'` + +--- + +### 2. Gestionnaire (`gestionnaire`) + +**Rôle** : Validateur des inscriptions +**Création** : Par le super administrateur uniquement + +**Responsabilités** : +- Consulter les demandes d'inscription en attente +- Valider ou refuser les comptes parents +- Valider ou refuser les comptes assistantes maternelles +- Consulter les informations des demandeurs + +**Référence** : Table `utilisateurs` avec `role = 'gestionnaire'` et `statut = 'actif'` + +--- + +### 3. Parent (`parent`) + +**Rôle** : Utilisateur final cherchant une assistante maternelle +**Création** : Inscription autonome via formulaire public + +**Workflow** : +1. S'inscrit via le formulaire public +2. Statut initial : `en_attente` +3. Attend la validation d'un gestionnaire +4. Reçoit un email de notification (validé/refusé) +5. Si validé : peut se connecter et accéder à l'application + +**Référence** : +- Table `utilisateurs` avec `role = 'parent'` +- Table `parents` (relation 1:1 avec `utilisateurs`) + +--- + +### 4. Assistante Maternelle (`assistante_maternelle`) + +**Rôle** : Professionnelle proposant ses services de garde +**Création** : Inscription autonome via formulaire public + +**Workflow** : Identique aux parents +1. S'inscrit via le formulaire public +2. Statut initial : `en_attente` +3. Attend la validation d'un gestionnaire +4. Reçoit un email de notification (validé/refusé) +5. Si validée : peut se connecter et accéder à l'application + +**Référence** : +- Table `utilisateurs` avec `role = 'assistante_maternelle'` +- Table `assistantes_maternelles` (relation 1:1 avec `utilisateurs`) + +--- + +## ⚠️ Points d'attention - Conformité CDC + +Avant de détailler le workflow, voici les points critiques pour assurer la conformité avec le Cahier des Charges : + +### Inscription Parent (CDC 3.1) +- ✅ **6 étapes obligatoires** : Parent 1, Parent 2 (opt), Enfant, Présentation, CGU, Récapitulatif +- ✅ **Photo obligatoire** si enfant déjà né +- ✅ **Acceptation CGU** avec horodatage +- ✅ **Notification** : Email **OU** SMS après validation + +### Inscription Assistante Maternelle (CDC 3.2) +- ✅ **NIR obligatoire** (Numéro de Sécurité sociale - 15 chiffres) +- ✅ **Photo obligatoire** (si option activée) +- ✅ **Consentement photo** avec horodatage (RGPD) +- ✅ **Date et lieu de naissance** obligatoires +- ✅ **Acceptation CGU** avec horodatage +- ✅ **Notification** : Email **OU** SMS après validation + +### Création Gestionnaire (CDC 3.3) +- ✅ **Changement de mot de passe obligatoire** à la première connexion +- ✅ Créé par un administrateur uniquement + +### Création Administrateur (CDC 3.4) +- ✅ **Changement de mot de passe obligatoire** à la première connexion +- ✅ Créé par un administrateur existant uniquement + +### Champs Base de Données Manquants +Les champs suivants doivent être ajoutés à la base de données : +- `nir_chiffre` (assistantes_maternelles) - **OBLIGATOIRE** +- `ville_naissance` (utilisateurs) +- `pays_naissance` (utilisateurs) +- `date_acceptation_cgu` (utilisateurs) +- `presentation_dossier` (parents ou table dédiée) +- `date_consentement_photo` (utilisateurs) - **Existe déjà** ✅ + +--- + +## 🔄 Workflow détaillé + +### Étape 1 : Initialisation du système + +**Prérequis** : Base de données initialisée avec le super admin + +```sql +-- Seed initial (01_init.sql) +INSERT INTO utilisateurs (email, password, prenom, nom, role, statut) +VALUES ( + 'admin@ptits-pas.fr', + '$2b$12$Fo5ly1YlTj3O6lXf.IUgoeUqEebBGpmoM5zLbzZx.CueorSE7z2E2', -- Hash de "4dm1n1strateur" + 'Admin', + 'Système', + 'super_admin', + 'actif' +); +``` + +**État** : ✅ Implémenté + +--- + +### Étape 2 : Création d'un gestionnaire + +**Acteur** : Super Administrateur +**Interface** : Panneau d'administration - Section "Créer un gestionnaire" + +#### 2.1 Interface utilisateur + +**Écran** : Formulaire ultra-simple avec les champs suivants : + +| Champ | Type | Validation | Obligatoire | +|-------|------|------------|-------------| +| Nom | Text | 2-100 caractères | ✅ | +| Prénom | Text | 2-100 caractères | ✅ | +| Email | Email | Format email valide | ✅ | +| Mot de passe | Password | Min 8 caractères, 1 majuscule, 1 chiffre | ✅ | + +**Bouton** : "Soumettre" + +**Wireframe** : +``` +┌─────────────────────────────────────────┐ +│ Créer un Gestionnaire │ +├─────────────────────────────────────────┤ +│ │ +│ Nom : [________________] │ +│ │ +│ Prénom : [________________] │ +│ │ +│ Email : [________________] │ +│ (personnel ou collectivité)│ +│ │ +│ Mot de passe : [________________] │ +│ │ +│ [ Soumettre ] │ +│ │ +└─────────────────────────────────────────┘ +``` + +#### 2.2 Flux technique + +**Frontend → Backend** + +```typescript +// Frontend (Flutter) +POST /api/v1/gestionnaires +Headers: { + Authorization: Bearer + Content-Type: application/json +} +Body: { + "email": "lucas.moreau@ptits-pas.fr", + "password": "password", + "prenom": "Lucas", + "nom": "MOREAU", + "telephone": "06 87 23 45 67" +} +``` + +**Backend → Database** + +```typescript +// Backend (NestJS) +// 1. Validation du token (AuthGuard) +// 2. Vérification du rôle (RolesGuard - super_admin uniquement) +// 3. Validation des données (DTO) +// 4. Hash du mot de passe (bcrypt, 12 rounds) +// 5. Création de l'utilisateur + +INSERT INTO utilisateurs ( + email, password, prenom, nom, telephone, role, statut, + changement_mdp_obligatoire, cree_le +) VALUES ( + 'lucas.moreau@ptits-pas.fr', + '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5iIdYvYvOYvOy', -- Hash bcrypt de "password" + 'Lucas', + 'MOREAU', + '06 87 23 45 67', + 'gestionnaire', + 'actif', -- Statut actif immédiatement + TRUE, -- Changement de mot de passe obligatoire à la première connexion (CDC 3.3) + NOW() +); +``` + +**Réponse** + +```json +{ + "id": "uuid-lucas-moreau", + "email": "lucas.moreau@ptits-pas.fr", + "prenom": "Lucas", + "nom": "MOREAU", + "telephone": "06 87 23 45 67", + "role": "gestionnaire", + "statut": "actif", + "cree_le": "2025-11-24T10:30:00Z" +} +``` + +**État** : 🟠 Backend implémenté, Frontend à créer + +--- + +### Étape 3 : Inscription d'un parent + +**Acteur** : Parent (non authentifié) +**Interface** : Page publique d'inscription parent +**Référence CDC** : Section 3.1 + +Le processus d'inscription parent se déroule en **6 étapes** conformément au cahier des charges. + +#### 3.1 Étape 1 : Informations Parent 1 + +**Écran** : Formulaire d'identité du parent principal + +| Champ | Type | Validation | Obligatoire | +|-------|------|------------|-------------| +| Nom | Text | 2-100 caractères | ✅ | +| Prénom | Text | 2-100 caractères | ✅ | +| Adresse postale | Text | Adresse complète | ✅ | +| Code postal | Text | 5 chiffres | ✅ | +| Ville | Text | 2-150 caractères | ✅ | +| Téléphone | Tel | Format français | ✅ | +| Email | Email | Format email valide | ✅ | + +**Note importante** : Le Parent 1 ne définit **pas** de mot de passe lors de l'inscription. Il recevra un email avec un lien pour créer son mot de passe après validation du gestionnaire. + +**Bouton** : "Suivant" + +--- + +#### 3.2 Étape 2 : Informations Parent 2 (facultatif) + +**Écran** : Ajout d'un co-parent (optionnel) + +**Question** : "Souhaitez-vous ajouter un second parent ?" +- ⭕ Oui +- ⭕ Non + +**Si Oui** : + +| Champ | Type | Validation | Obligatoire | +|-------|------|------------|-------------| +| Nom | Text | 2-100 caractères | ✅ | +| Prénom | Text | 2-100 caractères | ✅ | +| Email | Email | Format email valide | ✅ | +| Téléphone | Tel | Format français | ✅ | +| Même adresse que Parent 1 | Checkbox | - | - | + +**Si "Même adresse" non coché** : Afficher les champs adresse, code postal, ville + +**Note importante** : Le Parent 2 ne définit **pas** de mot de passe lors de l'inscription. Il recevra un email avec un lien pour créer son mot de passe après validation du gestionnaire. Cette approche est particulièrement adaptée aux situations de parents séparés ou divorcés où la communication peut être difficile. + +**Bouton** : "Suivant" + +--- + +#### 3.3 Étape 3 : Informations sur l'enfant + +**Écran** : Fiche enfant + +**Question** : "L'enfant est-il déjà né ?" +- ⭕ Oui → Afficher "Date de naissance" +- ⭕ Non → Afficher "Date prévisionnelle de naissance" + +| Champ | Type | Validation | Obligatoire | +|-------|------|------------|-------------| +| Prénom | Text | 2-100 caractères | ❌ (si à naître) | +| Nom | Text | Hérité des parents | Auto | +| Date de naissance | Date | Format JJ/MM/AAAA | ✅ (si né) | +| Date prévisionnelle | Date | Format JJ/MM/AAAA | ✅ (si à naître) | +| Genre | Select | H / F | ✅ | +| Photo | File | Image (JPEG/PNG), max 5MB | ✅ (si né) | +| Grossesse multiple | Checkbox | Jumeaux, triplés, etc. | ❌ | + +**Bouton** : "Ajouter un autre enfant" (optionnel) +**Bouton** : "Suivant" + +**Note** : Rattachement automatique aux deux parents si Parent 2 renseigné. + +--- + +#### 3.4 Étape 4 : Présentation du dossier + +**Écran** : Zone de texte libre + +| Champ | Type | Validation | Obligatoire | +|-------|------|------------|-------------| +| Présentation | Textarea | Max 2000 caractères | ⚙️ Configurable | + +**Exemple de texte d'aide** : +> "Décrivez votre situation familiale, vos besoins de garde, vos contraintes horaires, etc. Cette présentation sera visible par les assistantes maternelles et le gestionnaire." + +**Bouton** : "Suivant" + +--- + +#### 3.5 Étape 5 : Acceptation des CGU + +**Écran** : Conditions Générales d'Utilisation + +| Élément | Type | Obligatoire | +|---------|------|-------------| +| CGU | Checkbox | ✅ | + +**Texte** : +☐ J'ai lu et j'accepte les [Conditions Générales d'Utilisation](./cgu.pdf) et la [Politique de confidentialité](./privacy.pdf) + +**Comportement** : +- Les liens ouvrent les documents PDF dans un nouvel onglet +- Le refus bloque la création de compte +- La date d'acceptation est enregistrée en base + +**Bouton** : "Suivant" + +--- + +#### 3.6 Étape 6 : Récapitulatif et validation + +**Écran** : Résumé de toutes les informations saisies + +**Sections affichées** : +1. **Parent 1** : Nom, prénom, email, téléphone, adresse +2. **Parent 2** (si renseigné) : Nom, prénom, email, téléphone +3. **Enfant(s)** : Prénom, date de naissance/prévisionnelle, photo +4. **Présentation** : Extrait du texte (100 premiers caractères) + +**Boutons** : +- "Modifier" (retour aux étapes précédentes) +- "Valider et envoyer ma demande" + +**Message de confirmation** : +> "Votre demande d'inscription a été envoyée. Elle sera examinée par un gestionnaire. Vous recevrez un email ou un SMS une fois votre compte validé." + +**Bouton** : "Terminer" + +#### 3.2 Flux technique + +**Frontend → Backend** + +```typescript +// Frontend (Flutter) - Route publique +// Exemple : Claire MARTIN (parent avec triplés) +// Note : Pas de mot de passe lors de l'inscription +POST /api/v1/auth/register +Headers: { + Content-Type: application/json +} +Body: { + "email": "claire.martin@ptits-pas.fr", + "prenom": "Claire", + "nom": "MARTIN", + "telephone": "06 89 56 78 90", + "role": "parent" +} +``` + +**Backend → Database** + +```typescript +// Backend (NestJS) +// 1. Validation des données (DTO) +// 2. Vérification que l'email n'existe pas déjà +// 3. Génération d'un token de création de mot de passe (UUID) +// 4. Transaction : Créer utilisateur + entité métier + +BEGIN TRANSACTION; + +-- Création de l'utilisateur (sans mot de passe) +INSERT INTO utilisateurs ( + email, password, prenom, nom, telephone, role, statut, + adresse, code_postal, ville, profession, situation_familiale, date_naissance, + password_reset_token, password_reset_expires, cree_le +) VALUES ( + 'claire.martin@ptits-pas.fr', + NULL, -- Pas de mot de passe lors de l'inscription + 'Claire', + 'MARTIN', + '06 89 56 78 90', + 'parent', + 'en_attente', -- Statut en attente de validation + '5 Avenue du Général de Gaulle', + '95870', + 'Bezons', + 'Infirmière', + 'Mariée', + '1990-04-03', + gen_random_uuid(), -- Token de création de mot de passe + NOW() + INTERVAL '7 days', -- Expiration du token (7 jours) + NOW() +) RETURNING id; + +-- Création de l'entité métier parent +-- Note : id_co_parent sera renseigné plus tard si Thomas MARTIN s'inscrit +INSERT INTO parents (id_utilisateur, id_co_parent) +VALUES ('uuid-claire-martin', NULL); + +COMMIT; +``` + +**Réponse** + +```json +{ + "message": "Inscription réussie. Votre compte est en attente de validation par un gestionnaire. Vous recevrez un email une fois votre compte validé.", + "userId": "uuid-claire-martin" +} +``` + +**État** : 🔴 À implémenter complètement selon CDC + +--- + +### Étape 3bis : Inscription d'une assistante maternelle + +**Acteur** : Assistante Maternelle (non authentifiée) +**Interface** : Page publique d'inscription assistante maternelle +**Référence CDC** : Section 3.2 + +Le processus d'inscription assistante maternelle se déroule en **5 étapes** avec 2 panneaux principaux. + +#### 3bis.1 Panneau 1 : Informations d'identité + +**Écran** : Formulaire d'identité + +| Champ | Type | Validation | Obligatoire | +|-------|------|------------|-------------| +| Nom | Text | 2-100 caractères | ✅ | +| Prénom | Text | 2-100 caractères | ✅ | +| Adresse postale | Text | Adresse complète | ✅ | +| Code postal | Text | 5 chiffres | ✅ | +| Ville | Text | 2-150 caractères | ✅ | +| Téléphone | Tel | Format français | ✅ | +| Email | Email | Format email valide | ✅ | +| Photo de profil | File | Image (JPEG/PNG), max 5MB | ⚙️ Configurable | + +**Consentement photo** (si photo uploadée) : + +☐ J'autorise le stockage et l'affichage de ma photo sur la plateforme (RGPD) + +**Note importante** : L'assistante maternelle ne définit **pas** de mot de passe lors de l'inscription. Elle recevra un email avec un lien pour créer son mot de passe après validation du gestionnaire. + +**Note** : La date du consentement est enregistrée automatiquement. + +**Bouton** : "Suivant" + +--- + +#### 3bis.2 Panneau 2 : Informations professionnelles + +**Écran** : Informations professionnelles + +| Champ | Type | Validation | Obligatoire | +|-------|------|------------|-------------| +| Date de naissance | Date | Format JJ/MM/AAAA | ✅ | +| Ville de naissance | Text | 2-150 caractères | ✅ | +| Pays de naissance | Select | Liste des pays | ✅ | +| Numéro de Sécurité sociale (NIR) | Text | 15 chiffres | ✅ | +| Numéro d'agrément | Text | Format libre | ✅ | +| Date d'obtention de l'agrément | Date | Format JJ/MM/AAAA | ✅ | +| Nombre d'enfants pouvant être accueillis | Number | 1-6 | ✅ | + +**Note importante affichée** : +> ⚠️ Le numéro de Sécurité sociale (NIR) est saisi en clair et utilisé uniquement pour la génération automatique du contrat. Il est stocké de manière sécurisée et conforme au RGPD. + +**Bouton** : "Suivant" + +--- + +#### 3bis.3 Présentation + +**Écran** : Message au gestionnaire + +| Champ | Type | Validation | Obligatoire | +|-------|------|------------|-------------| +| Présentation | Textarea | Max 2000 caractères | ❌ | + +**Exemple de texte d'aide** : +> "Présentez-vous et expliquez votre démarche. Vous pouvez ajouter des précisions sur votre expérience, votre méthode pédagogique, vos disponibilités, etc." + +**Bouton** : "Suivant" + +--- + +#### 3bis.4 Acceptation des CGU + +**Écran** : Conditions Générales d'Utilisation + +| Élément | Type | Obligatoire | +|---------|------|-------------| +| CGU | Checkbox | ✅ | + +**Texte** : +☐ J'ai lu et j'accepte les [Conditions Générales d'Utilisation](./cgu.pdf) et la [Politique de confidentialité](./privacy.pdf) + +**Comportement** : +- Les liens ouvrent les documents PDF dans un nouvel onglet +- Le refus bloque la création de compte +- La date d'acceptation est enregistrée en base + +**Bouton** : "Suivant" + +--- + +#### 3bis.5 Récapitulatif et validation + +**Écran** : Résumé de toutes les informations saisies + +**Sections affichées** : +1. **Identité** : Nom, prénom, email, téléphone, adresse, photo +2. **Informations professionnelles** : + - Date de naissance, lieu de naissance + - NIR (masqué : XXX XX XX XX XXX 123) + - Numéro d'agrément + - Capacité d'accueil +3. **Présentation** : Extrait du texte (100 premiers caractères) + +**Boutons** : +- "Modifier" (retour aux étapes précédentes) +- "Valider et envoyer ma demande" + +**Message de confirmation** : +> "Votre demande d'inscription a été envoyée. Elle sera examinée par un gestionnaire. Vous recevrez un email ou un SMS une fois votre compte validé." + +**Bouton** : "Terminer" + +**État** : 🔴 À implémenter complètement selon CDC + +--- + +### Étape 4 : Consultation des demandes par le gestionnaire + +**Acteur** : Gestionnaire +**Interface** : Dashboard gestionnaire avec 2 onglets + +#### 4.1 Interface utilisateur + +**Écran** : Dashboard avec navigation par onglets + +``` +┌─────────────────────────────────────────────────────────┐ +│ Dashboard Gestionnaire │ +├─────────────────────────────────────────────────────────┤ +│ [ Parents ] [ Assistantes Maternelles ] │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 📋 Demandes en attente (3) │ +│ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ 👤 Jean MARTIN │ │ +│ │ 📧 jean.martin@example.com │ │ +│ │ 📱 06 01 02 03 04 │ │ +│ │ 📅 Inscrit le : 20/11/2025 │ │ +│ │ │ │ +│ │ [ ✅ Valider ] [ ❌ Refuser ] │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ 👤 Sophie DURAND │ │ +│ │ ... │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +#### 4.2 Flux technique - Liste des parents + +**Frontend → Backend** + +```typescript +// Frontend (Flutter) +GET /api/v1/parents +Headers: { + Authorization: Bearer +} +Query params: { + statut: "en_attente" // Filtrer uniquement les comptes en attente +} +``` + +**Backend → Database** + +```sql +-- Backend (NestJS) +SELECT + u.id, + u.email, + u.prenom, + u.nom, + u.telephone, + u.statut, + u.cree_le, + p.id_co_parent +FROM utilisateurs u +LEFT JOIN parents p ON u.id = p.id_utilisateur +WHERE u.role = 'parent' + AND u.statut = 'en_attente' +ORDER BY u.cree_le DESC; +``` + +**Réponse** + +```json +[ + { + "id": "uuid-claire-martin", + "email": "claire.martin@ptits-pas.fr", + "prenom": "Claire", + "nom": "MARTIN", + "telephone": "06 89 56 78 90", + "ville": "Bezons", + "profession": "Infirmière", + "situation_familiale": "Mariée", + "statut": "en_attente", + "cree_le": "2025-11-20T14:30:00Z" + }, + { + "id": "uuid-david-lecomte", + "email": "david.lecomte@ptits-pas.fr", + "prenom": "David", + "nom": "LECOMTE", + "telephone": "06 45 56 67 78", + "ville": "Bezons", + "profession": "Développeur web", + "situation_familiale": "Père célibataire", + "statut": "en_attente", + "cree_le": "2025-11-21T09:15:00Z" + } +] +``` + +#### 4.3 Flux technique - Liste des assistantes maternelles + +**Frontend → Backend** + +```typescript +// Frontend (Flutter) +GET /api/v1/assistantes-maternelles +Headers: { + Authorization: Bearer +} +Query params: { + statut: "en_attente" +} +``` + +**Backend → Database** + +```sql +-- Backend (NestJS) +SELECT + u.id, + u.email, + u.prenom, + u.nom, + u.telephone, + u.statut, + u.cree_le, + am.numero_agrement, + am.ville_residence +FROM utilisateurs u +LEFT JOIN assistantes_maternelles am ON u.id = am.id_utilisateur +WHERE u.role = 'assistante_maternelle' + AND u.statut = 'en_attente' +ORDER BY u.cree_le DESC; +``` + +**Réponse** + +```json +[ + { + "id": "uuid-marie-dubois", + "email": "marie.dubois@ptits-pas.fr", + "prenom": "Marie", + "nom": "DUBOIS", + "telephone": "06 96 34 56 78", + "ville": "Bezons", + "statut": "en_attente", + "cree_le": "2025-11-22T11:00:00Z", + "numero_agrement": null, + "ville_residence": "Bezons" + }, + { + "id": "uuid-fatima-elmansouri", + "email": "fatima.elmansouri@ptits-pas.fr", + "prenom": "Fatima", + "nom": "EL MANSOURI", + "telephone": "06 75 45 67 89", + "ville": "Bezons", + "statut": "en_attente", + "cree_le": "2025-11-22T15:30:00Z", + "numero_agrement": null, + "ville_residence": "Bezons" + } +] +``` + +**État** : 🟠 Backend implémenté, Frontend à connecter (actuellement en mock) + +--- + +### Étape 5 : Validation d'un compte + +**Acteur** : Gestionnaire +**Action** : Clic sur le bouton "Valider" + +#### 5.1 Flux technique - Validation + +**Frontend → Backend** + +```typescript +// Frontend (Flutter) +PATCH /api/v1/users/{userId}/valider +Headers: { + Authorization: Bearer + Content-Type: application/json +} +Body: { + "comment": "Dossier complet, compte validé" // Optionnel +} +``` + +**Backend → Database** + +```sql +-- Backend (NestJS) +-- Exemple : Lucas MOREAU valide Marie DUBOIS +BEGIN TRANSACTION; + +-- Mise à jour du statut utilisateur +UPDATE utilisateurs +SET statut = 'actif', + modifie_le = NOW() +WHERE id = 'uuid-marie-dubois'; + +-- Enregistrement de la validation +INSERT INTO validations ( + id_utilisateur, + type, + statut, + valide_par, + commentaire, + cree_le +) VALUES ( + 'uuid-marie-dubois', + 'validation_compte', + 'valide', + 'uuid-lucas-moreau', + 'Agrément vérifié - Profil complet', + NOW() +); + +COMMIT; +``` + +**Backend → Service Email** + +```typescript +// Envoi du lien de création de mot de passe pour tous les utilisateurs (Parents et AM) +await this.mailService.sendPasswordCreationLink({ + to: user.email, + prenom: user.prenom, + nom: user.nom, + token: user.password_reset_token, + expiresAt: user.password_reset_expires, + role: user.role // 'parent' ou 'assistante_maternelle' +}); + +// Si Parent 2 existe, envoi du même type d'email +if (user.role === 'parent' && parent2) { + await this.mailService.sendPasswordCreationLink({ + to: parent2.email, + prenom: parent2.prenom, + nom: parent2.nom, + token: parent2.password_reset_token, + expiresAt: parent2.password_reset_expires, + role: 'parent', + isCoParent: true // Indique qu'il s'agit du co-parent + }); +} +``` + +**Réponse** + +```json +{ + "message": "Compte validé avec succès", + "userId": "uuid-marie-dubois", + "emailSent": true +} +``` + +#### 5.2 Flux technique - Refus + +**Frontend → Backend** + +```typescript +// Frontend (Flutter) +PATCH /api/v1/users/{userId}/suspendre +Headers: { + Authorization: Bearer + Content-Type: application/json +} +Body: { + "comment": "Informations incomplètes" // Obligatoire pour un refus +} +``` + +**Backend → Database** + +```sql +-- Backend (NestJS) +-- Exemple : Lucas MOREAU refuse un compte (exemple fictif) +BEGIN TRANSACTION; + +-- Mise à jour du statut utilisateur +UPDATE utilisateurs +SET statut = 'suspendu', + modifie_le = NOW() +WHERE id = 'uuid-utilisateur-refuse'; + +-- Enregistrement du refus +INSERT INTO validations ( + id_utilisateur, + type, + statut, + valide_par, + commentaire, + cree_le +) VALUES ( + 'uuid-utilisateur-refuse', + 'validation_compte', + 'refuse', + 'uuid-lucas-moreau', + 'Informations incomplètes - Documents manquants', + NOW() +); + +COMMIT; +``` + +**Backend → Service Email** + +```typescript +// Envoi de l'email de notification +await this.mailService.sendAccountRejected({ + to: user.email, + prenom: user.prenom, + nom: user.nom, + role: user.role, + reason: comment +}); +``` + +**Réponse** + +```json +{ + "message": "Compte refusé", + "userId": "uuid-utilisateur-refuse", + "emailSent": true +} +``` + +**État** : 🔴 Backend partiellement implémenté (manque envoi email), Frontend à connecter + +--- + +### Étape 6 : Réception de la notification + +**Acteur** : Parent ou Assistante Maternelle +**Canal** : Email **OU** SMS (conformément au CDC 3.1.6 et 3.2.5) + +**Note** : Le choix du canal de notification (email/SMS) peut être : +- Configuré par l'administrateur (paramètre global) +- Choisi par l'utilisateur lors de l'inscription +- Les deux en parallèle pour plus de fiabilité + +#### 6.1 Email de validation - Parents (avec création de mot de passe) + +**Expéditeur** : `no-reply@ptits-pas.fr` +**Destinataire** : Email du parent +**Objet** : `Votre compte P'titsPas a été validé - Créez votre mot de passe ✅` + +**Template Parent 1** : + +```html + + + + + + + +
+
+

✅ Compte validé

+
+
+

Bonjour {{prenom}} {{nom}},

+ +

Bonne nouvelle ! Votre compte P'titsPas a été validé par notre équipe.

+ +

Pour finaliser votre inscription et accéder à votre espace parent, veuillez créer votre mot de passe en cliquant sur le bouton ci-dessous :

+ +

+ Créer mon mot de passe +

+ +
+ ⏰ Attention : Ce lien est valable pendant 7 jours (jusqu'au {{expires_date}}). +
+ +

Votre identifiant : {{email}}

+ +

Si vous avez des questions, n'hésitez pas à nous contacter à contact@ptits-pas.fr.

+ +

À très bientôt sur P'titsPas !

+ +

L'équipe P'titsPas

+
+ +
+ + +``` + +**Template Parent 2 (Co-parent)** : + +```html + + + + + + + +
+
+

✅ Bienvenue sur P'titsPas

+
+
+

Bonjour {{prenom}} {{nom}},

+ +

Votre co-parent vous a ajouté sur la plateforme P'titsPas et votre compte a été validé par notre équipe.

+ +

Pour accéder à votre espace parent et consulter les informations de vos enfants, veuillez créer votre mot de passe en cliquant sur le bouton ci-dessous :

+ +

+ Créer mon mot de passe +

+ +
+ ⏰ Attention : Ce lien est valable pendant 7 jours (jusqu'au {{expires_date}}). +
+ +

Votre identifiant : {{email}}

+ +

Si vous avez des questions ou si vous n'êtes pas à l'origine de cette inscription, contactez-nous à contact@ptits-pas.fr.

+ +

À très bientôt sur P'titsPas !

+ +

L'équipe P'titsPas

+
+ +
+ + +``` + +#### 6.2 Email de validation - Assistantes Maternelles (avec création de mot de passe) + +**Expéditeur** : `no-reply@ptits-pas.fr` +**Destinataire** : Email de l'assistante maternelle +**Objet** : `Votre compte P'titsPas a été validé - Créez votre mot de passe ✅` + +**Template** : + +```html + + + + + + + +
+
+

✅ Compte validé

+
+
+

Bonjour {{prenom}} {{nom}},

+ +

Bonne nouvelle ! Votre compte P'titsPas a été validé par notre équipe.

+ +

Pour finaliser votre inscription et accéder à votre espace assistante maternelle, veuillez créer votre mot de passe en cliquant sur le bouton ci-dessous :

+ +

+ Créer mon mot de passe +

+ +
+ ⏰ Attention : Ce lien est valable pendant 7 jours (jusqu'au {{expires_date}}). +
+ +

Votre identifiant : {{email}}

+ +

Si vous avez des questions, n'hésitez pas à nous contacter à contact@ptits-pas.fr.

+ +

À très bientôt sur P'titsPas !

+ +

L'équipe P'titsPas

+
+ +
+ + +``` + +#### 6.3 Email de refus + +**Expéditeur** : `no-reply@ptits-pas.fr` +**Destinataire** : Email de l'utilisateur +**Objet** : `Votre demande d'inscription P'titsPas` + +**Template** : + +```html + + + + + + + +
+
+

Votre demande d'inscription

+
+
+

Bonjour {{prenom}} {{nom}},

+ +

Nous avons bien reçu votre demande d'inscription sur P'titsPas.

+ +

Malheureusement, nous ne pouvons pas valider votre compte pour le moment.

+ +

Motif : {{reason}}

+ +

Si vous pensez qu'il s'agit d'une erreur ou si vous souhaitez plus d'informations, n'hésitez pas à nous contacter à contact@ptits-pas.fr.

+ +

Cordialement,

+ +

L'équipe P'titsPas

+
+ +
+ + +``` + +**État** : 🔴 À implémenter + +--- + +#### 6.3 SMS de validation (optionnel) + +**Expéditeur** : P'titsPas (nom court configurable) +**Destinataire** : Numéro de téléphone de l'utilisateur + +**Template SMS** : + +``` +P'titsPas : Votre compte a été validé ! Créez votre mot de passe : https://app.ptits-pas.fr/create-password?token={{token}} (valable 7 jours) +``` + +**Contraintes** : +- Maximum 160 caractères (SMS standard) +- Lien court pour l'URL +- Message clair et concis + +--- + +#### 6.4 SMS de refus (optionnel) + +**Expéditeur** : P'titsPas +**Destinataire** : Numéro de téléphone de l'utilisateur + +**Template SMS** : + +``` +P'titsPas : Votre demande d'inscription n'a pas pu être validée. Motif : {{reason_court}}. Contactez-nous : contact@ptits-pas.fr +``` + +**Contraintes** : +- Maximum 160 caractères +- Motif abrégé si nécessaire +- Contact pour plus d'informations + +**État** : 🔴 À implémenter (Service SMS à configurer : Twilio, OVH, etc.) + +--- + +### Étape 7 : Création du mot de passe + +**Acteur** : Parent (Parent 1 ou Parent 2) ou Assistante Maternelle +**Interface** : Page de création de mot de passe (lien reçu par email) + +#### 7.1 Flux technique + +**Frontend → Backend** + +```typescript +// Frontend (Flutter/Web) +// L'utilisateur clique sur le lien reçu par email +// URL : https://app.ptits-pas.fr/create-password?token= + +GET /api/v1/auth/verify-token?token= +// Vérification que le token est valide et non expiré + +// Si valide, affichage du formulaire de création de mot de passe +POST /api/v1/auth/create-password +Headers: { + Content-Type: application/json +} +Body: { + "token": "", + "password": "NouveauMotDePasse123!", + "password_confirmation": "NouveauMotDePasse123!" +} +``` + +**Backend → Database** + +```typescript +// Backend (NestJS) +// 1. Vérification du token (existe, non expiré, non utilisé) +// 2. Validation du mot de passe (min 8 caractères, 1 majuscule, 1 chiffre) +// 3. Hash du mot de passe (bcrypt, 12 rounds) +// 4. Mise à jour de l'utilisateur + +UPDATE utilisateurs +SET password = '$2b$12$...', -- Hash bcrypt du nouveau mot de passe + password_reset_token = NULL, -- Suppression du token + password_reset_expires = NULL, + statut = 'actif', -- Activation du compte + modifie_le = NOW() +WHERE password_reset_token = '' + AND password_reset_expires > NOW() + AND password IS NULL; -- Sécurité : uniquement si pas de mot de passe existant +``` + +**Réponse** + +```json +{ + "message": "Mot de passe créé avec succès. Vous pouvez maintenant vous connecter.", + "userId": "uuid-claire-martin" +} +``` + +**Frontend** : Redirection automatique vers la page de connexion avec un message de succès. + +**État** : 🔴 À implémenter (Frontend + Backend) + +--- + +### Étape 8 : Connexion de l'utilisateur validé + +**Acteur** : Parent ou Assistante Maternelle (compte validé et mot de passe défini) +**Interface** : Page de connexion + +#### 7.1 Flux technique + +**Frontend → Backend** + +```typescript +// Frontend (Flutter) +// Exemple : Claire MARTIN se connecte après validation +POST /api/v1/auth/login +Headers: { + Content-Type: application/json +} +Body: { + "email": "claire.martin@ptits-pas.fr", + "password": "Test1234!" +} +``` + +**Backend → Database** + +```sql +-- Backend (NestJS) +SELECT + id, email, password, prenom, nom, role, statut +FROM utilisateurs +WHERE email = 'jean.martin@example.com'; + +-- Vérification : +-- 1. L'utilisateur existe +-- 2. Le mot de passe correspond (bcrypt.compare) +-- 3. Le statut est 'actif' (sinon erreur 403) +``` + +**Réponse - Succès** + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "user": { + "id": "uuid-claire-martin", + "email": "claire.martin@ptits-pas.fr", + "role": "parent", + "prenom": "Claire", + "nom": "MARTIN", + "statut": "actif" + } +} +``` + +**Réponse - Compte en attente** + +```json +{ + "statusCode": 403, + "message": "Votre compte est en attente de validation. Vous recevrez un email une fois votre compte validé.", + "error": "Forbidden" +} +``` + +**Réponse - Compte refusé** + +```json +{ + "statusCode": 403, + "message": "Votre compte a été refusé. Contactez contact@ptits-pas.fr pour plus d'informations.", + "error": "Forbidden" +} +``` + +#### 7.2 Redirection après connexion + +**Parent validé** → Dashboard parent (page simple : "Vous êtes connecté") +**Assistante Maternelle validée** → Dashboard AM (page simple : "Vous êtes connecté") + +**État** : ✅ Backend implémenté, Frontend à adapter + +--- + +## 📊 Diagrammes de séquence + +### Diagramme 1 : Création d'un gestionnaire + +```mermaid +sequenceDiagram + participant SA as Super Admin
(Frontend) + participant API as Backend API
(NestJS) + participant Auth as AuthGuard
+ RolesGuard + participant DB as PostgreSQL + + SA->>API: POST /gestionnaires
{nom, prenom, email, password} + API->>Auth: Vérifier token JWT + Auth->>Auth: Vérifier role = super_admin + Auth-->>API: ✅ Autorisé + + API->>API: Valider DTO (CreateGestionnaireDto) + API->>API: Hash password (bcrypt, 12 rounds) + + API->>DB: INSERT INTO utilisateurs
(role='gestionnaire', statut='actif') + DB-->>API: ✅ Utilisateur créé (id, email, ...) + + API-->>SA: 201 Created
{id, email, prenom, nom, role} + + SA->>SA: Afficher message de succès +``` + +--- + +### Diagramme 2 : Inscription d'un parent + +```mermaid +sequenceDiagram + participant P as Parent
(Frontend) + participant API as Backend API
(NestJS) + participant DB as PostgreSQL + + P->>API: POST /auth/register
{email, password, prenom, nom, role='parent'} + + API->>API: Valider DTO (RegisterDto) + + API->>DB: SELECT * FROM utilisateurs
WHERE email = ? + DB-->>API: ❌ Aucun résultat + + API->>API: Hash password (bcrypt, 12 rounds) + + API->>DB: BEGIN TRANSACTION + + API->>DB: INSERT INTO utilisateurs
(role='parent', statut='en_attente') + DB-->>API: ✅ id_utilisateur + + API->>DB: INSERT INTO parents
(id_utilisateur) + DB-->>API: ✅ Parent créé + + API->>DB: COMMIT + + API-->>P: 201 Created
{message, userId} + + P->>P: Afficher message:
"Inscription réussie.
Votre compte est en attente
de validation." +``` + +--- + +### Diagramme 3 : Validation d'un compte par le gestionnaire + +```mermaid +sequenceDiagram + participant G as Gestionnaire
(Frontend) + participant API as Backend API
(NestJS) + participant Auth as AuthGuard
+ RolesGuard + participant DB as PostgreSQL + participant Mail as Service Email
(Nodemailer) + participant SMTP as Serveur SMTP
(mail.ptits-pas.fr) + participant U as Utilisateur
(Email) + + G->>API: GET /parents?statut=en_attente + API->>Auth: Vérifier token + role gestionnaire + Auth-->>API: ✅ Autorisé + API->>DB: SELECT * FROM utilisateurs
WHERE role='parent' AND statut='en_attente' + DB-->>API: Liste des parents en attente + API-->>G: 200 OK
[{id, email, prenom, nom, ...}] + + G->>G: Afficher liste + G->>G: Clic sur "Valider" + + G->>API: PATCH /users/{id}/valider
{comment: "Dossier complet"} + API->>Auth: Vérifier token + role gestionnaire + Auth-->>API: ✅ Autorisé + + API->>DB: BEGIN TRANSACTION + API->>DB: UPDATE utilisateurs
SET statut='actif'
WHERE id=? + DB-->>API: ✅ 1 row updated + + API->>DB: INSERT INTO validations
(type, statut, valide_par, ...) + DB-->>API: ✅ Validation enregistrée + + API->>DB: COMMIT + + API->>Mail: sendAccountValidated(user) + Mail->>Mail: Générer HTML depuis template + Mail->>SMTP: SMTP SEND
From: no-reply@ptits-pas.fr
To: user.email
Subject: Compte validé + SMTP->>U: 📧 Email de validation + SMTP-->>Mail: ✅ Email envoyé + Mail-->>API: ✅ emailSent: true + + API-->>G: 200 OK
{message, userId, emailSent} + + G->>G: Retirer l'utilisateur de la liste
Afficher notification de succès + + U->>U: 📧 Reçoit l'email + U->>U: Clic sur "Se connecter" +``` + +--- + +### Diagramme 4 : Connexion d'un utilisateur validé + +```mermaid +sequenceDiagram + participant U as Utilisateur
(Frontend) + participant API as Backend API
(NestJS) + participant DB as PostgreSQL + + U->>API: POST /auth/login
{email, password} + + API->>DB: SELECT * FROM utilisateurs
WHERE email = ? + DB-->>API: Utilisateur trouvé + + API->>API: bcrypt.compare(password, hash) + API->>API: ✅ Mot de passe correct + + API->>API: Vérifier statut + + alt Statut = 'actif' + API->>API: Générer JWT
(access_token + refresh_token) + API-->>U: 200 OK
{access_token, refresh_token, user} + U->>U: Stocker tokens + U->>U: Redirection vers dashboard + else Statut = 'en_attente' + API-->>U: 403 Forbidden
"Compte en attente de validation" + U->>U: Afficher message d'attente + else Statut = 'suspendu' + API-->>U: 403 Forbidden
"Compte refusé" + U->>U: Afficher message + contact + end +``` + +--- + +## 🔧 Spécifications techniques + +### Architecture Backend + +**Framework** : NestJS 10.x +**Langage** : TypeScript 5.x +**ORM** : TypeORM 0.3.x +**Validation** : class-validator + class-transformer +**Authentication** : JWT (jsonwebtoken + @nestjs/jwt) +**Password Hashing** : bcrypt (12 rounds) +**Email** : Nodemailer (à installer) + +### Architecture Frontend + +**Framework** : Flutter 3.19.0 +**Langage** : Dart 3.x +**State Management** : Provider / Riverpod +**HTTP Client** : Dio / http +**Storage** : flutter_secure_storage (tokens) + +### Architecture Database + +**SGBD** : PostgreSQL 17 +**Schema** : Voir [DATABASE.md](./DATABASE.md) +**Tables concernées** : +- `utilisateurs` (table principale) +- `parents` (extension pour parents) +- `assistantes_maternelles` (extension pour AM) +- `validations` (historique des validations) + +### Configuration Email + +**Serveur SMTP** : `mail.ptits-pas.fr` +**Port** : 25 (STARTTLS) +**Expéditeur** : `no-reply@ptits-pas.fr` +**Authentification** : Aucune (serveur interne) + +--- + +## 📡 APIs utilisées + +### API 1 : Créer un gestionnaire + +**Endpoint** : `POST /api/v1/gestionnaires` +**Authentification** : Bearer Token (super_admin uniquement) +**Référence** : [API.md - Gestionnaires](./API.md#post-gestionnaires) + +**Request** : +```json +{ + "email": "lucas.moreau@ptits-pas.fr", + "password": "password", + "prenom": "Lucas", + "nom": "MOREAU" +} +``` + +**Response 201** : +```json +{ + "id": "uuid-lucas-moreau", + "email": "lucas.moreau@ptits-pas.fr", + "role": "gestionnaire", + "prenom": "Lucas", + "nom": "MOREAU" +} +``` + +**Errors** : +- `401 Unauthorized` : Token manquant ou invalide +- `403 Forbidden` : Rôle insuffisant (pas super_admin) +- `409 Conflict` : Email déjà utilisé +- `400 Bad Request` : Données invalides + +--- + +### API 2 : Inscription (parent ou AM) + +**Endpoint** : `POST /api/v1/auth/register` +**Authentification** : Aucune (route publique) +**Référence** : [API.md - Authentification](./API.md#post-authregister) + +**Request** : +```json +{ + "email": "claire.martin@ptits-pas.fr", + "password": "password", + "prenom": "Claire", + "nom": "MARTIN", + "telephone": "01 39 98 89 01", + "role": "parent" +} +``` + +**Response 201** : +```json +{ + "message": "Inscription réussie. Votre compte est en attente de validation.", + "userId": "uuid-claire-martin" +} +``` + +**Errors** : +- `409 Conflict` : Email déjà utilisé +- `400 Bad Request` : Données invalides + +--- + +### API 3 : Liste des parents en attente + +**Endpoint** : `GET /api/v1/parents` +**Authentification** : Bearer Token (gestionnaire ou super_admin) +**Référence** : [API.md - Parents](./API.md#get-parents) + +**Query Params** : +``` +?statut=en_attente +``` + +**Response 200** : +```json +[ + { + "id": "uuid", + "email": "parent@example.com", + "prenom": "Jean", + "nom": "Martin", + "telephone": "0601020304", + "statut": "en_attente", + "cree_le": "2025-11-20T14:30:00Z" + } +] +``` + +**Errors** : +- `401 Unauthorized` : Token manquant ou invalide +- `403 Forbidden` : Rôle insuffisant + +--- + +### API 4 : Liste des assistantes maternelles en attente + +**Endpoint** : `GET /api/v1/assistantes-maternelles` +**Authentification** : Bearer Token (gestionnaire ou super_admin) +**Référence** : [API.md - Assistantes Maternelles](./API.md#get-assistantes-maternelles) + +**Query Params** : +``` +?statut=en_attente +``` + +**Response 200** : +```json +[ + { + "id": "uuid", + "email": "am@example.com", + "prenom": "Marie", + "nom": "Leblanc", + "telephone": "0698765432", + "statut": "en_attente", + "cree_le": "2025-11-22T11:00:00Z" + } +] +``` + +--- + +### API 5 : Valider un compte + +**Endpoint** : `PATCH /api/v1/users/{id}/valider` +**Authentification** : Bearer Token (gestionnaire, administrateur ou super_admin) +**Référence** : [API.md - Utilisateurs](./API.md#patch-usersidvalider) + +**Request** : +```json +{ + "comment": "Dossier complet, compte validé" +} +``` + +**Response 200** : +```json +{ + "message": "Compte validé avec succès", + "userId": "uuid", + "emailSent": true +} +``` + +**Errors** : +- `401 Unauthorized` : Token manquant ou invalide +- `403 Forbidden` : Rôle insuffisant +- `404 Not Found` : Utilisateur introuvable +- `400 Bad Request` : ID invalide + +--- + +### API 6 : Refuser un compte + +**Endpoint** : `PATCH /api/v1/users/{id}/suspendre` +**Authentification** : Bearer Token (gestionnaire, administrateur ou super_admin) +**Référence** : [API.md - Utilisateurs](./API.md#patch-usersidsuspendre) + +**Request** : +```json +{ + "comment": "Informations incomplètes" +} +``` + +**Response 200** : +```json +{ + "message": "Compte suspendu", + "userId": "uuid", + "emailSent": true +} +``` + +--- + +### API 7 : Connexion + +**Endpoint** : `POST /api/v1/auth/login` +**Authentification** : Aucune (route publique) +**Référence** : [API.md - Authentification](./API.md#post-authlogin) + +**Request** : +```json +{ + "email": "parent@example.com", + "password": "MotDePasse123" +} +``` + +**Response 200** : +```json +{ + "access_token": "eyJhbGc...", + "refresh_token": "eyJhbGc...", + "user": { + "id": "uuid", + "email": "parent@example.com", + "role": "parent", + "prenom": "Jean", + "nom": "Martin", + "statut": "actif" + } +} +``` + +**Errors** : +- `401 Unauthorized` : Email ou mot de passe incorrect +- `403 Forbidden` : Compte en attente ou suspendu + +--- + +## 🗄️ Modèles de données + +### Utilisateur + +**Table** : `utilisateurs` +**Référence** : [DATABASE.md - Table utilisateurs](./DATABASE.md#1-utilisateurs) + +```typescript +interface Utilisateur { + id: UUID; // Identifiant unique + email: string; // Email (unique) + password: string; // Hash bcrypt + prenom: string; // Prénom + nom: string; // Nom + genre?: 'H' | 'F' | 'Autre'; // Genre (optionnel) + role: RoleType; // Rôle (voir enum ci-dessous) + statut: StatutUtilisateurType; // Statut (voir enum ci-dessous) + telephone?: string; // Téléphone + adresse?: string; // Adresse complète + photo_url?: string; // URL photo de profil + consentement_photo: boolean; // Consentement photo + date_consentement_photo?: Date; // Date du consentement + changement_mdp_obligatoire: boolean; // Force changement MDP + cree_le: Date; // Date de création + modifie_le: Date; // Dernière modification + ville?: string; // Ville + code_postal?: string; // Code postal + telephone?: string; // Téléphone + profession?: string; // Profession + situation_familiale?: string; // Situation familiale + date_naissance?: Date; // Date de naissance +} +``` + +**Enums** : + +```typescript +enum RoleType { + PARENT = 'parent', + GESTIONNAIRE = 'gestionnaire', + SUPER_ADMIN = 'super_admin', + ASSISTANTE_MATERNELLE = 'assistante_maternelle', + ADMINISTRATEUR = 'administrateur' +} + +enum StatutUtilisateurType { + EN_ATTENTE = 'en_attente', + ACTIF = 'actif', + SUSPENDU = 'suspendu' +} +``` + +--- + +### Parent + +**Table** : `parents` +**Référence** : [DATABASE.md - Table parents](./DATABASE.md#3-parents) + +```typescript +interface Parent { + id_utilisateur: UUID; // FK → utilisateurs(id) + id_co_parent?: UUID; // FK → utilisateurs(id) - Co-parent optionnel +} +``` + +**Relation** : 1:1 avec `utilisateurs` + +--- + +### Assistante Maternelle + +**Table** : `assistantes_maternelles` +**Référence** : [DATABASE.md - Table assistantes_maternelles](./DATABASE.md#2-assistantes_maternelles) + +```typescript +interface AssistanteMaternelle { + id_utilisateur: UUID; // FK → utilisateurs(id) + numero_agrement?: string; // Numéro d'agrément + nir_chiffre?: string; // NIR (15 caractères) + nb_max_enfants?: number; // Capacité maximale + biographie?: string; // Présentation + disponible: boolean; // Disponibilité + ville_residence?: string; // Ville + date_agrement?: Date; // Date d'agrément + annee_experience?: number; // Années d'expérience + specialite?: string; // Spécialités + place_disponible?: number; // Places disponibles +} +``` + +**Relation** : 1:1 avec `utilisateurs` + +--- + +### Validation + +**Table** : `validations` +**Référence** : [DATABASE.md - Table validations](./DATABASE.md#14-validations) + +```typescript +interface Validation { + id: UUID; // Identifiant unique + id_utilisateur: UUID; // FK → utilisateurs(id) + type: string; // Type de validation (ex: 'validation_compte') + statut: StatutValidationType; // Statut (voir enum ci-dessous) + cree_le: Date; // Date de demande + modifie_le: Date; // Dernière modification + valide_par?: UUID; // FK → utilisateurs(id) - Validateur + commentaire?: string; // Commentaire du validateur +} +``` + +**Enum** : + +```typescript +enum StatutValidationType { + EN_ATTENTE = 'en_attente', + VALIDE = 'valide', + REFUSE = 'refuse' +} +``` + +--- + +## 📧 Templates d'emails + +### Configuration Nodemailer + +**Installation** : +```bash +npm install nodemailer +npm install -D @types/nodemailer +``` + +**Configuration** (`backend/src/mail/mail.config.ts`) : + +```typescript +import { MailerOptions } from '@nestjs-modules/mailer'; +import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter'; + +export const mailConfig: MailerOptions = { + transport: { + host: 'mail.ptits-pas.fr', + port: 25, + secure: false, + tls: { + rejectUnauthorized: false + } + }, + defaults: { + from: '"P\'titsPas" ', + }, + template: { + dir: __dirname + '/templates', + adapter: new HandlebarsAdapter(), + options: { + strict: true, + }, + }, +}; +``` + +### Service Email + +**Fichier** : `backend/src/mail/mail.service.ts` + +```typescript +import { Injectable } from '@nestjs/common'; +import { MailerService } from '@nestjs-modules/mailer'; + +@Injectable() +export class MailService { + constructor(private mailerService: MailerService) {} + + async sendAccountValidated(user: { + email: string; + prenom: string; + nom: string; + role: string; + }): Promise { + try { + const roleLabel = user.role === 'parent' ? 'Parent' : 'Assistante Maternelle'; + + await this.mailerService.sendMail({ + to: user.email, + subject: 'Votre compte P\'titsPas a été validé ✅', + template: './account-validated', + context: { + prenom: user.prenom, + nom: user.nom, + email: user.email, + role_label: roleLabel, + }, + }); + + return true; + } catch (error) { + console.error('Error sending validation email:', error); + return false; + } + } + + async sendAccountRejected(user: { + email: string; + prenom: string; + nom: string; + role: string; + reason: string; + }): Promise { + try { + await this.mailerService.sendMail({ + to: user.email, + subject: 'Votre demande d\'inscription P\'titsPas', + template: './account-rejected', + context: { + prenom: user.prenom, + nom: user.nom, + reason: user.reason, + }, + }); + + return true; + } catch (error) { + console.error('Error sending rejection email:', error); + return false; + } + } +} +``` + +### Templates Handlebars + +**Fichier** : `backend/src/mail/templates/account-validated.hbs` + +Voir le template HTML dans la section [Étape 6.1](#61-email-de-validation) + +**Fichier** : `backend/src/mail/templates/account-rejected.hbs` + +Voir le template HTML dans la section [Étape 6.2](#62-email-de-refus) + +--- + +## ✅ Tests et validation + +### Tests unitaires (Backend) + +**Framework** : Jest + +**Fichiers de test** : +- `backend/src/routes/auth/auth.service.spec.ts` +- `backend/src/routes/user/user.service.spec.ts` +- `backend/src/mail/mail.service.spec.ts` + +**Scénarios de test** : + +1. **Création de gestionnaire** + - ✅ Création réussie avec données valides + - ❌ Échec si email déjà existant + - ❌ Échec si utilisateur non super_admin + - ❌ Échec si données invalides + +2. **Inscription** + - ✅ Inscription parent réussie + - ✅ Inscription AM réussie + - ✅ Création de l'entité métier (parent/AM) + - ❌ Échec si email déjà existant + - ❌ Échec si mot de passe trop faible + +3. **Validation de compte** + - ✅ Validation réussie + changement statut + - ✅ Email envoyé + - ✅ Enregistrement dans table validations + - ❌ Échec si utilisateur non gestionnaire + - ❌ Échec si utilisateur déjà validé + +4. **Refus de compte** + - ✅ Refus réussi + changement statut + - ✅ Email envoyé avec motif + - ✅ Enregistrement dans table validations + - ❌ Échec si commentaire manquant + +5. **Connexion** + - ✅ Connexion réussie si compte actif + - ❌ Échec si compte en attente + - ❌ Échec si compte suspendu + - ❌ Échec si mot de passe incorrect + +### Tests d'intégration (Backend) + +**Framework** : Supertest + Jest + +**Scénarios de test** : + +1. **Workflow complet - Parent** + ```typescript + it('should complete full parent workflow', async () => { + // 1. Super admin crée un gestionnaire + const gestionnaire = await createGestionnaire(); + + // 2. Parent s'inscrit + const parent = await registerParent(); + + // 3. Gestionnaire liste les parents en attente + const pendingParents = await getPendingParents(gestionnaire.token); + expect(pendingParents).toContainEqual(expect.objectContaining({ + email: parent.email, + statut: 'en_attente' + })); + + // 4. Gestionnaire valide le parent + await validateUser(gestionnaire.token, parent.id); + + // 5. Parent se connecte + const loginResponse = await loginUser(parent.email, parent.password); + expect(loginResponse.access_token).toBeDefined(); + }); + ``` + +2. **Workflow complet - Assistante Maternelle** + - Identique au workflow parent + +3. **Workflow refus** + ```typescript + it('should handle account rejection', async () => { + const gestionnaire = await createGestionnaire(); + const parent = await registerParent(); + + await rejectUser(gestionnaire.token, parent.id, 'Informations incomplètes'); + + const loginResponse = await loginUser(parent.email, parent.password); + expect(loginResponse.statusCode).toBe(403); + }); + ``` + +### Tests E2E (Frontend + Backend) + +**Framework** : Flutter Integration Tests + Mockito + +**Scénarios de test** : + +1. **Création gestionnaire (UI)** + - Remplir le formulaire + - Soumettre + - Vérifier le message de succès + - Vérifier que le gestionnaire peut se connecter + +2. **Inscription parent (UI)** + - Remplir le formulaire d'inscription + - Soumettre + - Vérifier le message "en attente de validation" + - Vérifier que la connexion est refusée + +3. **Dashboard gestionnaire (UI)** + - Se connecter en tant que gestionnaire + - Vérifier l'affichage des 2 onglets + - Vérifier l'affichage des comptes en attente + - Cliquer sur "Valider" + - Vérifier que le compte disparaît de la liste + +4. **Réception email (Manuel)** + - Vérifier la réception de l'email de validation + - Vérifier le contenu de l'email + - Cliquer sur le lien de connexion + - Vérifier la redirection vers l'application + +### Checklist de validation + +**Phase 1 : Backend** +- [ ] Endpoint création gestionnaire fonctionne +- [ ] Flag `changement_mdp_obligatoire` = TRUE pour gestionnaires/admins +- [ ] Endpoint inscription parent fonctionne (6 étapes) + - [ ] Création Parent 1 + - [ ] Création Parent 2 (optionnel) + - [ ] Création Enfant(s) + - [ ] Enregistrement présentation + - [ ] Enregistrement acceptation CGU avec horodatage + - [ ] Récapitulatif +- [ ] Endpoint inscription AM fonctionne (5 étapes) + - [ ] Panneau 1 : Identité + Photo + Consentement photo + - [ ] Panneau 2 : NIR + Agrément + Infos pro + - [ ] Présentation + - [ ] Acceptation CGU + - [ ] Récapitulatif +- [ ] Entité métier créée lors de l'inscription +- [ ] Endpoint validation fonctionne +- [ ] Endpoint refus fonctionne +- [ ] Email de validation envoyé +- [ ] Email de refus envoyé +- [ ] SMS de validation envoyé (optionnel) +- [ ] SMS de refus envoyé (optionnel) +- [ ] Connexion bloquée si compte en attente +- [ ] Connexion autorisée si compte actif +- [ ] Changement de mot de passe forcé à la première connexion (gestionnaires/admins) + +**Phase 2 : Frontend** +- [ ] Écran création gestionnaire implémenté +- [ ] Formulaire d'inscription parent implémenté (6 étapes) + - [ ] Étape 1 : Informations Parent 1 + - [ ] Étape 2 : Informations Parent 2 (optionnel) + - [ ] Étape 3 : Informations Enfant(s) avec upload photo + - [ ] Étape 4 : Présentation du dossier + - [ ] Étape 5 : Acceptation CGU (liens PDF) + - [ ] Étape 6 : Récapitulatif +- [ ] Formulaire d'inscription AM implémenté (5 étapes) + - [ ] Panneau 1 : Identité + Upload photo + Consentement + - [ ] Panneau 2 : NIR + Date/lieu naissance + Agrément + - [ ] Présentation + - [ ] Acceptation CGU (liens PDF) + - [ ] Récapitulatif +- [ ] Dashboard gestionnaire avec 2 onglets +- [ ] Liste des parents en attente affichée (avec enfants) +- [ ] Liste des AM en attente affichée (avec NIR masqué) +- [ ] Boutons Valider/Refuser fonctionnels +- [ ] Messages de feedback utilisateur +- [ ] Gestion des erreurs +- [ ] Écran de changement de mot de passe forcé (première connexion) + +**Phase 3 : Intégration** +- [ ] Workflow complet parent testé +- [ ] Workflow complet AM testé +- [ ] Workflow refus testé +- [ ] Emails reçus et corrects +- [ ] Connexion après validation OK +- [ ] Redirection vers dashboard OK + +--- + +## 📚 Références + +### Documentation interne + +- [API.md](./API.md) - Documentation complète des endpoints +- [DATABASE.md](./DATABASE.md) - Schéma de la base de données +- [AUDIT.md](./AUDIT.md) - Audit du projet YNOV +- [README-ARCHITECTURE.md](./README-ARCHITECTURE.md) - Architecture du projet +- [README-DEPLOYMENT.md](./README-DEPLOYMENT.md) - Guide de déploiement + +### Documentation externe + +- [NestJS Documentation](https://docs.nestjs.com/) +- [TypeORM Documentation](https://typeorm.io/) +- [Flutter Documentation](https://flutter.dev/docs) +- [PostgreSQL Documentation](https://www.postgresql.org/docs/) +- [Nodemailer Documentation](https://nodemailer.com/) + +### Cahier des Charges + +**Section 3.1** : Gestion des utilisateurs +**Section 3.2** : Processus de validation +**Section 4.5** : Notifications email + +--- + +## 📋 Résumé des modifications (Conformité CDC) + +Ce document a été mis à jour pour être conforme au **Cahier des Charges v1.3** (Section 3). + +### Principales modifications + +#### Inscription Parent +- ✅ Passage de 1 formulaire simple à **6 étapes complètes** +- ✅ Ajout de la gestion du **Parent 2** (co-parent) +- ✅ Ajout de la création des **enfants** lors de l'inscription +- ✅ Ajout du champ **Présentation du dossier** +- ✅ Ajout de l'**Acceptation des CGU** avec horodatage +- ✅ Ajout d'un **Récapitulatif** avant validation + +#### Inscription Assistante Maternelle +- ✅ Passage de 1 formulaire simple à **5 étapes avec 2 panneaux** +- ✅ Ajout du **NIR** (Numéro de Sécurité sociale) - **OBLIGATOIRE** +- ✅ Ajout de la **Photo obligatoire** (si option activée) +- ✅ Ajout du **Consentement photo** avec horodatage (RGPD) +- ✅ Ajout de la **Date et lieu de naissance** +- ✅ Ajout de l'**Acceptation des CGU** avec horodatage +- ✅ Ajout d'un **Récapitulatif** avant validation + +#### Création Gestionnaire/Administrateur +- ✅ Ajout du **Changement de mot de passe obligatoire** à la première connexion + +#### Notifications +- ✅ Ajout de la possibilité d'envoi par **SMS** en plus de l'email + +### Champs Base de Données à Ajouter + +```sql +-- Table utilisateurs +ALTER TABLE utilisateurs ADD COLUMN ville_naissance VARCHAR(150); +ALTER TABLE utilisateurs ADD COLUMN pays_naissance VARCHAR(100); +ALTER TABLE utilisateurs ADD COLUMN date_acceptation_cgu TIMESTAMPTZ; + +-- Table assistantes_maternelles +-- Le champ nir_chiffre existe déjà mais doit être rendu OBLIGATOIRE +ALTER TABLE assistantes_maternelles ALTER COLUMN nir_chiffre SET NOT NULL; + +-- Table parents ou nouvelle table +-- Option 1 : Ajouter à la table parents +ALTER TABLE parents ADD COLUMN presentation_dossier TEXT; + +-- Option 2 : Créer une table dédiée +CREATE TABLE dossiers_inscription ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + presentation TEXT, + cree_le TIMESTAMPTZ DEFAULT now() +); +``` + +--- + +**Dernière mise à jour** : 24 Novembre 2025 +**Version** : 1.0 +**Statut** : ✅ Document validé - Conforme CDC v1.3 + diff --git a/docs/21_CONFIGURATION-SYSTEME.md b/docs/21_CONFIGURATION-SYSTEME.md new file mode 100644 index 0000000..fc438b5 --- /dev/null +++ b/docs/21_CONFIGURATION-SYSTEME.md @@ -0,0 +1,712 @@ +# 🔧 Documentation Technique - Configuration Système On-Premise + +**Version** : 1.0 +**Date** : 25 Novembre 2025 +**Auteur** : Équipe PtitsPas +**Référence** : Architecture On-Premise + +--- + +## 📖 Table des matières + +1. [Vue d'ensemble](#vue-densemble) +2. [Architecture de configuration](#architecture-de-configuration) +3. [Table configuration](#table-configuration) +4. [Service Configuration](#service-configuration) +5. [Workflow Setup Initial](#workflow-setup-initial) +6. [APIs Configuration](#apis-configuration) +7. [Interface Admin](#interface-admin) +8. [Exemples de configuration](#exemples-de-configuration) + +--- + +## 🎯 Vue d'ensemble + +### Problématique + +L'application P'titsPas est déployée **on-premise** chez différentes collectivités. Chaque collectivité a : +- Son propre serveur SMTP +- Ses propres ports et configurations réseau +- Son propre nom de domaine +- Sa propre charte graphique + +**Solution** : Configuration dynamique stockée en base de données, modifiable via interface web. + +### Principes + +1. ✅ **Pas de hardcoding** : Aucune valeur en dur dans le code +2. ✅ **Pas de redéploiement** : Modification sans rebuild Docker +3. ✅ **Sécurité** : Mots de passe chiffrés en AES-256 +4. ✅ **Traçabilité** : Qui a modifié quoi et quand +5. ✅ **Setup wizard** : Configuration guidée à la première connexion +6. ✅ **Validation** : Test SMTP avant sauvegarde + +--- + +## 🏗️ Architecture de configuration + +### Flux de données + +``` +┌─────────────────────────────────────────────────────────┐ +│ Application │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ Frontend │─────▶│ Backend │ │ +│ │ (Admin) │ │ ConfigAPI │ │ +│ └──────────────┘ └──────┬───────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ ConfigService│ │ +│ │ (Cache) │ │ +│ └──────┬───────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ PostgreSQL │ │ +│ │ configuration│ │ +│ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### Composants + +1. **Table `configuration`** : Stockage clé/valeur en BDD +2. **ConfigService** : Cache en mémoire + chiffrement +3. **ConfigAPI** : Endpoints REST pour CRUD +4. **Guard Setup** : Redirection forcée si config incomplète +5. **Interface Admin** : Formulaire de configuration + +--- + +## 📊 Table configuration + +### Schéma SQL + +```sql +-- Table de configuration système (clé/valeur) +CREATE TABLE configuration ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + cle VARCHAR(100) UNIQUE NOT NULL, -- Clé unique (ex: 'smtp_host') + valeur TEXT, -- Valeur (peut être NULL) + type VARCHAR(50) NOT NULL, -- Type: 'string', 'number', 'boolean', 'json', 'encrypted' + categorie VARCHAR(50), -- Catégorie: 'email', 'app', 'security' + description TEXT, -- Description pour l'interface admin + modifie_le TIMESTAMPTZ DEFAULT now(), -- Date dernière modification + modifie_par UUID REFERENCES utilisateurs(id) -- Qui a modifié (traçabilité) +); + +-- Index pour performance +CREATE INDEX idx_configuration_cle ON configuration(cle); +CREATE INDEX idx_configuration_categorie ON configuration(categorie); +``` + +### Seed initial + +```sql +INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES +-- === Configuration Email (SMTP) === +('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'), +('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'), +('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'), +('smtp_auth_required', 'false', 'boolean', 'email', 'Authentification SMTP requise'), +('smtp_user', '', 'string', 'email', 'Utilisateur SMTP (si authentification requise)'), +('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'), +('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'), +('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'), + +-- === Configuration Application === +('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'), +('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'), +('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'), +('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'), + +-- === Configuration Sécurité === +('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'), +('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'), +('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'), +('bcrypt_rounds', '12', 'number', 'security', 'Nombre de rounds bcrypt pour le hachage des mots de passe'); +``` + +### Types de données + +| Type | Description | Exemple | +|------|-------------|---------| +| `string` | Chaîne de caractères | `"mail.example.com"` | +| `number` | Nombre entier ou décimal | `587` | +| `boolean` | Booléen | `true` / `false` | +| `json` | Objet JSON | `{"key": "value"}` | +| `encrypted` | Chaîne chiffrée AES-256 | `"a3f8b2..."` (hash) | + +--- + +## 🔧 Service Configuration + +### Responsabilités + +1. **Cache en mémoire** : Chargement au démarrage +2. **Lecture** : `get(key, defaultValue)` +3. **Écriture** : `set(key, value, userId)` +4. **Chiffrement** : AES-256 pour type `encrypted` +5. **Conversion de types** : string → number/boolean/json +6. **Test SMTP** : Validation connexion + +### Implémentation (TypeScript) + +```typescript +// backend/src/config/config.service.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Configuration } from './entities/configuration.entity'; +import * as crypto from 'crypto'; + +@Injectable() +export class ConfigService { + private cache: Map = new Map(); + private readonly ENCRYPTION_KEY = process.env.CONFIG_ENCRYPTION_KEY; + + constructor( + @InjectRepository(Configuration) + private configRepo: Repository, + ) { + this.loadCache(); + } + + // Chargement du cache au démarrage + async loadCache() { + const configs = await this.configRepo.find(); + configs.forEach(config => { + let value = config.valeur; + + // Déchiffrement si nécessaire + if (config.type === 'encrypted' && value) { + value = this.decrypt(value); + } + + // Conversion de type + value = this.convertType(value, config.type); + + this.cache.set(config.cle, value); + }); + } + + // Récupération d'une valeur + get(key: string, defaultValue?: any): any { + return this.cache.has(key) ? this.cache.get(key) : defaultValue; + } + + // Mise à jour d'une valeur + async set(key: string, value: any, userId: string): Promise { + const config = await this.configRepo.findOne({ where: { cle: key } }); + + if (!config) { + throw new Error(`Configuration key '${key}' not found`); + } + + let valueToStore = String(value); + + // Chiffrement si nécessaire + if (config.type === 'encrypted') { + valueToStore = this.encrypt(valueToStore); + } + + config.valeur = valueToStore; + config.modifie_par = userId; + config.modifie_le = new Date(); + + await this.configRepo.save(config); + + // Mise à jour du cache + this.cache.set(key, value); + } + + // Récupération de toutes les configs par catégorie + async getByCategory(category: string): Promise { + const configs = await this.configRepo.find({ where: { categorie: category } }); + + return configs.reduce((acc, config) => { + let value = config.valeur; + + if (config.type === 'encrypted') { + value = '***********'; // Masquer les mots de passe + } else { + value = this.convertType(value, config.type); + } + + acc[config.cle] = { + value, + description: config.description, + type: config.type, + }; + + return acc; + }, {}); + } + + // Test de connexion SMTP + async testSmtpConnection(): Promise { + const nodemailer = require('nodemailer'); + + const transporter = nodemailer.createTransport({ + host: this.get('smtp_host'), + port: this.get('smtp_port'), + secure: this.get('smtp_secure'), + auth: this.get('smtp_auth_required') ? { + user: this.get('smtp_user'), + pass: this.get('smtp_password'), + } : undefined, + }); + + try { + await transporter.verify(); + return true; + } catch (error) { + console.error('SMTP test failed:', error); + return false; + } + } + + // Utilitaires + private convertType(value: string, type: string): any { + switch (type) { + case 'number': return Number(value); + case 'boolean': return value === 'true'; + case 'json': return JSON.parse(value); + default: return value; + } + } + + private encrypt(text: string): string { + const cipher = crypto.createCipher('aes-256-cbc', this.ENCRYPTION_KEY); + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return encrypted; + } + + private decrypt(text: string): string { + const decipher = crypto.createDecipher('aes-256-cbc', this.ENCRYPTION_KEY); + let decrypted = decipher.update(text, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } +} +``` + +--- + +## 🔄 Workflow Setup Initial + +### Diagramme de séquence + +```mermaid +sequenceDiagram + participant SA as Super Admin + participant App as Application + participant Guard as SetupGuard + participant API as ConfigAPI + participant DB as PostgreSQL + participant SMTP as Serveur SMTP + + SA->>App: Première connexion + App->>Guard: Vérifier setup_completed + Guard->>DB: SELECT valeur FROM configuration
WHERE cle='setup_completed' + DB-->>Guard: 'false' + + Guard-->>App: Redirection forcée vers
/admin/setup + + SA->>SA: Remplit formulaire config
(SMTP, app, sécurité) + + SA->>App: Clic "Tester la connexion SMTP" + App->>API: POST /api/v1/configuration/test-smtp + API->>SMTP: Test connexion + + alt Test SMTP OK + SMTP-->>API: ✅ Connexion réussie + API->>SA: Envoi email de test + API-->>App: ✅ Test réussi + App-->>SA: Message: "Email de test envoyé" + else Test SMTP KO + SMTP-->>API: ❌ Erreur connexion + API-->>App: ❌ Erreur détaillée + App-->>SA: Message: "Erreur: vérifiez les paramètres" + end + + SA->>App: Clic "Sauvegarder" + App->>API: PATCH /api/v1/configuration/bulk
{smtp_host, smtp_port, ...} + + API->>DB: BEGIN TRANSACTION + API->>DB: UPDATE configuration SET valeur=...
FOR EACH key + API->>DB: UPDATE configuration
SET valeur='true'
WHERE cle='setup_completed' + API->>DB: COMMIT + + API->>API: Recharger cache ConfigService + + API-->>App: ✅ Configuration sauvegardée + App-->>SA: Redirection vers /admin/dashboard + + SA->>App: Accès complet à l'application +``` + +### Étapes détaillées + +#### 1. Détection configuration incomplète + +**Guard** : `SetupGuard` (NestJS) + +```typescript +@Injectable() +export class SetupGuard implements CanActivate { + constructor(private configService: ConfigService) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const setupCompleted = this.configService.get('setup_completed', false); + + // Exemptions + const exemptedRoutes = ['/auth/login', '/admin/setup', '/api/v1/configuration']; + if (exemptedRoutes.some(route => request.url.includes(route))) { + return true; + } + + // Si setup non complété, bloquer + if (!setupCompleted) { + throw new HttpException( + 'Configuration initiale requise', + HttpStatus.TEMPORARY_REDIRECT, + { location: '/admin/setup' } + ); + } + + return true; + } +} +``` + +#### 2. Formulaire Setup (Frontend) + +**3 onglets** : + +##### Onglet 1 : Configuration Email 📧 + +| Champ | Type | Valeur par défaut | Obligatoire | +|-------|------|-------------------|-------------| +| Serveur SMTP | Text | `localhost` | ✅ | +| Port SMTP | Number | `25` | ✅ | +| Sécurité | Select | `Aucune` / `STARTTLS` / `SSL/TLS` | ✅ | +| Authentification requise | Checkbox | `false` | - | +| Utilisateur SMTP | Text | - | Si auth | +| Mot de passe SMTP | Password | - | Si auth | +| Nom expéditeur | Text | `P'titsPas` | ✅ | +| Email expéditeur | Email | `no-reply@ptits-pas.fr` | ✅ | + +**Bouton** : "🧪 Tester la connexion SMTP" + +##### Onglet 2 : Personnalisation 🎨 + +| Champ | Type | Valeur par défaut | Obligatoire | +|-------|------|-------------------|-------------| +| Nom de l'application | Text | `P'titsPas` | ✅ | +| URL de l'application | URL | `https://app.ptits-pas.fr` | ✅ | +| Logo | File (PNG/JPG) | Logo par défaut | ❌ | + +##### Onglet 3 : Paramètres avancés ⚙️ + +| Champ | Type | Valeur par défaut | Obligatoire | +|-------|------|-------------------|-------------| +| Durée validité token MDP (jours) | Number | `7` | ✅ | +| Durée session JWT (heures) | Number | `24` | ✅ | +| Taille max upload (MB) | Number | `5` | ✅ | + +**Bouton** : "💾 Sauvegarder et terminer la configuration" + +--- + +## 🔌 APIs Configuration + +### Endpoint 1 : Récupérer config par catégorie + +```http +GET /api/v1/configuration/:category +Authorization: Bearer +``` + +**Paramètres** : +- `category` : `email` | `app` | `security` + +**Réponse 200** : +```json +{ + "smtp_host": { + "value": "localhost", + "description": "Serveur SMTP", + "type": "string" + }, + "smtp_port": { + "value": 25, + "description": "Port SMTP", + "type": "number" + }, + "smtp_password": { + "value": "***********", + "description": "Mot de passe SMTP", + "type": "encrypted" + } +} +``` + +--- + +### Endpoint 2 : Mise à jour multiple + +```http +PATCH /api/v1/configuration/bulk +Authorization: Bearer +Content-Type: application/json +``` + +**Body** : +```json +{ + "smtp_host": "mail.mairie-bezons.fr", + "smtp_port": 587, + "smtp_secure": false, + "smtp_auth_required": true, + "smtp_user": "noreply@mairie-bezons.fr", + "smtp_password": "SecretPassword123", + "email_from_name": "P'titsPas - Mairie de Bezons", + "email_from_address": "noreply@mairie-bezons.fr", + "app_name": "P'titsPas Bezons", + "app_url": "https://ptitspas.mairie-bezons.fr" +} +``` + +**Réponse 200** : +```json +{ + "message": "Configuration mise à jour avec succès", + "updated": 10 +} +``` + +--- + +### Endpoint 3 : Test connexion SMTP + +```http +POST /api/v1/configuration/test-smtp +Authorization: Bearer +Content-Type: application/json +``` + +**Body** : +```json +{ + "smtp_host": "mail.mairie-bezons.fr", + "smtp_port": 587, + "smtp_secure": false, + "smtp_auth_required": true, + "smtp_user": "noreply@mairie-bezons.fr", + "smtp_password": "SecretPassword123", + "test_email": "admin@mairie-bezons.fr" +} +``` + +**Réponse 200** : +```json +{ + "success": true, + "message": "Connexion SMTP réussie. Email de test envoyé à admin@mairie-bezons.fr" +} +``` + +**Réponse 400** : +```json +{ + "success": false, + "message": "Erreur de connexion SMTP", + "error": "ECONNREFUSED: Connection refused" +} +``` + +--- + +## 💻 Interface Admin + +### Écran Setup Initial + +``` +┌─────────────────────────────────────────────────────────┐ +│ 🚀 Configuration Initiale - P'titsPas │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ Bienvenue ! Configurez votre installation P'titsPas │ +│ │ +│ [ 📧 Email ] [ 🎨 Personnalisation ] [ ⚙️ Avancé ] │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 📧 Configuration Email (SMTP) │ +│ │ +│ Serveur SMTP * │ +│ [_____________________________________________] │ +│ Ex: mail.mairie-bezons.fr, smtp.gmail.com │ +│ │ +│ Port SMTP * │ +│ [_____] 25 (standard), 465 (SSL), 587 (STARTTLS) │ +│ │ +│ Sécurité * │ +│ [ ▼ Aucune ] STARTTLS SSL/TLS │ +│ │ +│ ☐ Authentification requise │ +│ │ +│ Utilisateur SMTP │ +│ [_____________________________________________] │ +│ │ +│ Mot de passe SMTP │ +│ [_____________________________________________] │ +│ │ +│ Nom de l'expéditeur * │ +│ [_____________________________________________] │ +│ Ex: P'titsPas - Mairie de Bezons │ +│ │ +│ Email expéditeur * │ +│ [_____________________________________________] │ +│ Ex: noreply@mairie-bezons.fr │ +│ │ +│ [ 🧪 Tester la connexion SMTP ] │ +│ │ +│ ───────────────────────────────────────────────── │ +│ │ +│ [ ← Précédent ] [ Suivant → ] │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### Écran Paramètres (accès permanent) + +Identique au Setup Initial, mais accessible depuis le menu admin : +- Menu Admin → Paramètres → Configuration Système + +--- + +## 📋 Exemples de configuration + +### Configuration 1 : Mairie (serveur local) + +```json +{ + "smtp_host": "mail.mairie-bezons.fr", + "smtp_port": 25, + "smtp_secure": false, + "smtp_auth_required": false, + "email_from_name": "P'titsPas - Mairie de Bezons", + "email_from_address": "noreply@mairie-bezons.fr", + "app_name": "P'titsPas Bezons", + "app_url": "https://ptitspas.mairie-bezons.fr" +} +``` + +### Configuration 2 : Gmail (pour tests) + +```json +{ + "smtp_host": "smtp.gmail.com", + "smtp_port": 587, + "smtp_secure": false, + "smtp_auth_required": true, + "smtp_user": "contact@ptits-pas.fr", + "smtp_password": "abcd efgh ijkl mnop", + "email_from_name": "P'titsPas", + "email_from_address": "contact@ptits-pas.fr", + "app_name": "P'titsPas", + "app_url": "https://app.ptits-pas.fr" +} +``` + +**Note** : Pour Gmail, utiliser un "Mot de passe d'application" (App Password) + +### Configuration 3 : Office 365 + +```json +{ + "smtp_host": "smtp.office365.com", + "smtp_port": 587, + "smtp_secure": false, + "smtp_auth_required": true, + "smtp_user": "noreply@collectivite.fr", + "smtp_password": "MotDePasseSecurise123", + "email_from_name": "P'titsPas - Collectivité", + "email_from_address": "noreply@collectivite.fr", + "app_name": "P'titsPas", + "app_url": "https://ptitspas.collectivite.fr" +} +``` + +--- + +## 🔒 Sécurité + +### Chiffrement des mots de passe + +**Algorithme** : AES-256-CBC +**Clé** : Variable d'environnement `CONFIG_ENCRYPTION_KEY` (32 caractères) + +**Génération de la clé** : +```bash +# Linux/Mac +openssl rand -hex 32 + +# Node.js +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +**Fichier `.env`** : +```env +CONFIG_ENCRYPTION_KEY=a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2 +``` + +### Variables d'environnement critiques + +**Fichier `.env`** (à créer lors de l'installation) : + +```env +# Base de données +DATABASE_URL=postgresql://app_user:password@ptitspas-postgres:5432/ptitpas_db + +# JWT +JWT_SECRET=VotreSecretJWTTresLongEtAleatoire123456789 +JWT_EXPIRY=24h + +# Configuration +CONFIG_ENCRYPTION_KEY=VotreCleDeChiffrementAES256TresLongue32Caracteres + +# Application +NODE_ENV=production +PORT=3000 +``` + +--- + +## 📚 Références + +### Documentation interne +- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md) +- [02_ARCHITECTURE.md](./02_ARCHITECTURE.md) +- [03_DEPLOYMENT.md](./03_DEPLOYMENT.md) +- [10_DATABASE.md](./10_DATABASE.md) +- [11_API.md](./11_API.md) + +### Documentation externe +- [NestJS Configuration](https://docs.nestjs.com/techniques/configuration) +- [Nodemailer SMTP](https://nodemailer.com/smtp/) +- [Node.js Crypto](https://nodejs.org/api/crypto.html) + +--- + +**Dernière mise à jour** : 25 Novembre 2025 +**Version** : 1.0 +**Statut** : ✅ Document validé + diff --git a/docs/22_DOCUMENTS-LEGAUX.md b/docs/22_DOCUMENTS-LEGAUX.md new file mode 100644 index 0000000..4bd5bc5 --- /dev/null +++ b/docs/22_DOCUMENTS-LEGAUX.md @@ -0,0 +1,698 @@ +# 📄 Documentation Technique - Gestion Documents Légaux (CGU/Privacy) + +**Version** : 1.0 +**Date** : 25 Novembre 2025 +**Auteur** : Équipe PtitsPas +**Référence** : RGPD & Conformité juridique + +--- + +## 📖 Table des matières + +1. [Vue d'ensemble](#vue-densemble) +2. [Architecture](#architecture) +3. [Tables BDD](#tables-bdd) +4. [Service Documents Légaux](#service-documents-légaux) +5. [Workflow Upload & Activation](#workflow-upload--activation) +6. [Workflow Acceptation Utilisateur](#workflow-acceptation-utilisateur) +7. [APIs](#apis) +8. [Interface Admin](#interface-admin) +9. [Conformité RGPD](#conformité-rgpd) + +--- + +## 🎯 Vue d'ensemble + +### Problématique + +Chaque collectivité déployant P'titsPas on-premise doit pouvoir : +1. ✅ **Personnaliser** les CGU et la Politique de confidentialité +2. ✅ **Versionner** les documents (traçabilité juridique) +3. ✅ **Tracer** qui a accepté quelle version (RGPD) +4. ✅ **Prouver** l'acceptation (IP, User-Agent, horodatage) +5. ✅ **Empêcher** le retour en arrière (sécurité juridique) + +### Solution + +- **Documents génériques v1** fournis par défaut (rédigés avec juriste) +- **Upload de nouvelles versions** par l'admin (PDF uniquement) +- **Versioning automatique** (incrémentation sans retour arrière) +- **Activation manuelle** (prévisualisation avant mise en prod) +- **Traçabilité complète** (hash SHA-256, IP, User-Agent) + +--- + +## 🏗️ Architecture + +### Flux de données + +``` +┌─────────────────────────────────────────────────────────┐ +│ Workflow Documents │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 1. UPLOAD (Admin) │ +│ Admin ──▶ API ──▶ File System ──▶ BDD │ +│ /documents/legaux/ │ +│ cgu_v4_.pdf │ +│ │ +│ 2. ACTIVATION (Admin) │ +│ Admin ──▶ API ──▶ BDD (actif=true) │ +│ │ +│ 3. ACCEPTATION (Utilisateur) │ +│ User ──▶ Frontend ──▶ API ──▶ BDD │ +│ (inscription) (trace IP/UA) │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### Composants + +1. **Table `documents_legaux`** : Stockage versions + métadonnées +2. **Table `acceptations_documents`** : Traçabilité acceptations +3. **Service `DocumentsLegauxService`** : Upload, versioning, activation +4. **API REST** : CRUD documents +5. **Interface Admin** : Upload + activation +6. **Interface Inscription** : Affichage + acceptation + +--- + +## 📊 Tables BDD + +### Table 1 : `documents_legaux` + +```sql +-- Table pour gérer les versions des documents légaux +CREATE TABLE documents_legaux ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy' + version INTEGER NOT NULL, -- Numéro de version (auto-incrémenté) + fichier_nom VARCHAR(255) NOT NULL, -- Nom original du fichier + fichier_path VARCHAR(500) NOT NULL, -- Chemin de stockage + fichier_hash VARCHAR(64) NOT NULL, -- Hash SHA-256 pour intégrité + actif BOOLEAN DEFAULT false, -- Version actuellement active + televerse_par UUID REFERENCES utilisateurs(id), -- Qui a uploadé + televerse_le TIMESTAMPTZ DEFAULT now(), -- Date d'upload + active_le TIMESTAMPTZ, -- Date d'activation + UNIQUE(type, version) -- Pas de doublon version +); + +-- Index pour performance +CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif); +CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC); +``` + +**Contraintes** : +- ✅ Un seul document `actif=true` par type à la fois +- ✅ Versioning auto-incrémenté (pas de gaps) +- ✅ Hash SHA-256 pour vérifier l'intégrité du fichier + +--- + +### Table 2 : `acceptations_documents` + +```sql +-- Table de traçabilité des acceptations (RGPD) +CREATE TABLE acceptations_documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE, + id_document UUID REFERENCES documents_legaux(id), + type_document VARCHAR(50) NOT NULL, -- 'cgu' ou 'privacy' + version_document INTEGER NOT NULL, -- Version acceptée + accepte_le TIMESTAMPTZ DEFAULT now(), -- Date d'acceptation + ip_address INET, -- IP de l'utilisateur (RGPD) + user_agent TEXT -- Navigateur (preuve) +); + +CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur); +CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document); +``` + +**Données capturées** : +- ✅ **Qui** : `id_utilisateur` +- ✅ **Quoi** : `type_document`, `version_document` +- ✅ **Quand** : `accepte_le` +- ✅ **Où** : `ip_address` +- ✅ **Comment** : `user_agent` + +--- + +### Modification table `utilisateurs` + +```sql +-- Ajouter colonnes pour référence rapide (optionnel) +ALTER TABLE utilisateurs + ADD COLUMN cgu_version_acceptee INTEGER, + ADD COLUMN cgu_acceptee_le TIMESTAMPTZ, + ADD COLUMN privacy_version_acceptee INTEGER, + ADD COLUMN privacy_acceptee_le TIMESTAMPTZ; +``` + +**Note** : Ces colonnes sont **redondantes** avec `acceptations_documents`, mais permettent un accès rapide sans JOIN. + +--- + +### Seed initial + +```sql +-- Documents génériques v1 (fournis par défaut) +INSERT INTO documents_legaux (type, version, fichier_nom, fichier_path, fichier_hash, actif, televerse_le, active_le) VALUES +('cgu', 1, 'cgu_v1_default.pdf', '/documents/legaux/cgu_v1_default.pdf', 'a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2', true, now(), now()), +('privacy', 1, 'privacy_v1_default.pdf', '/documents/legaux/privacy_v1_default.pdf', 'b4f9c3d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4', true, now(), now()); +``` + +**Fichiers à fournir** : +- `/documents/legaux/cgu_v1_default.pdf` (rédigé avec juriste) +- `/documents/legaux/privacy_v1_default.pdf` (conforme RGPD) + +--- + +## 🔧 Service Documents Légaux + +### Responsabilités + +1. **Récupérer documents actifs** : `getDocumentsActifs()` +2. **Uploader nouvelle version** : `uploadNouvelleVersion(type, file, userId)` +3. **Activer une version** : `activerVersion(documentId)` +4. **Lister versions** : `listerVersions(type)` +5. **Télécharger document** : `telechargerDocument(documentId)` + +### Implémentation (TypeScript) + +```typescript +// backend/src/documents-legaux/documents-legaux.service.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DocumentLegal } from './entities/document-legal.entity'; +import * as crypto from 'crypto'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +@Injectable() +export class DocumentsLegauxService { + private readonly UPLOAD_DIR = '/app/documents/legaux'; + + constructor( + @InjectRepository(DocumentLegal) + private docRepo: Repository, + ) {} + + // Récupérer les documents actifs + async getDocumentsActifs(): Promise<{ cgu: DocumentLegal; privacy: DocumentLegal }> { + const cgu = await this.docRepo.findOne({ + where: { type: 'cgu', actif: true }, + }); + + const privacy = await this.docRepo.findOne({ + where: { type: 'privacy', actif: true }, + }); + + if (!cgu || !privacy) { + throw new Error('Documents légaux manquants'); + } + + return { cgu, privacy }; + } + + // Uploader une nouvelle version + async uploadNouvelleVersion( + type: 'cgu' | 'privacy', + file: Express.Multer.File, + userId: string, + ): Promise { + // 1. Calculer la prochaine version + const lastDoc = await this.docRepo.findOne({ + where: { type }, + order: { version: 'DESC' }, + }); + const nouvelleVersion = (lastDoc?.version || 0) + 1; + + // 2. Calculer le hash du fichier + const fileBuffer = file.buffer; + const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + + // 3. Générer le nom de fichier unique + const timestamp = Date.now(); + const fileName = `${type}_v${nouvelleVersion}_${timestamp}.pdf`; + const filePath = path.join(this.UPLOAD_DIR, fileName); + + // 4. Sauvegarder le fichier + await fs.mkdir(this.UPLOAD_DIR, { recursive: true }); + await fs.writeFile(filePath, fileBuffer); + + // 5. Créer l'entrée en BDD + const document = this.docRepo.create({ + type, + version: nouvelleVersion, + fichier_nom: file.originalname, + fichier_path: filePath, + fichier_hash: hash, + actif: false, // Pas actif par défaut + televerse_par: userId, + televerse_le: new Date(), + }); + + return await this.docRepo.save(document); + } + + // Activer une version + async activerVersion(documentId: string): Promise { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new Error('Document non trouvé'); + } + + // Transaction : désactiver l'ancienne version, activer la nouvelle + await this.docRepo.manager.transaction(async (manager) => { + // Désactiver toutes les versions de ce type + await manager.update( + DocumentLegal, + { type: document.type, actif: true }, + { actif: false }, + ); + + // Activer la nouvelle version + await manager.update( + DocumentLegal, + { id: documentId }, + { actif: true, active_le: new Date() }, + ); + }); + } + + // Lister toutes les versions (pour l'admin) + async listerVersions(type: 'cgu' | 'privacy'): Promise { + return await this.docRepo.find({ + where: { type }, + order: { version: 'DESC' }, + relations: ['televerse_par'], + }); + } + + // Télécharger un document (stream) + async telechargerDocument(documentId: string): Promise<{ stream: Buffer; filename: string }> { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new Error('Document non trouvé'); + } + + const fileBuffer = await fs.readFile(document.fichier_path); + + return { + stream: fileBuffer, + filename: document.fichier_nom, + }; + } + + // Vérifier l'intégrité d'un document + async verifierIntegrite(documentId: string): Promise { + const document = await this.docRepo.findOne({ where: { id: documentId } }); + + if (!document) { + throw new Error('Document non trouvé'); + } + + const fileBuffer = await fs.readFile(document.fichier_path); + const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + + return hash === document.fichier_hash; + } +} +``` + +--- + +## 🔄 Workflow Upload & Activation + +### Diagramme de séquence + +```mermaid +sequenceDiagram + participant A as Admin + participant API as Backend API + participant FS as File System + participant DB as PostgreSQL + + A->>API: POST /api/v1/documents-legaux
{type: 'cgu', file: PDF} + + API->>API: Validation fichier
(PDF, max 10MB) + API->>API: Calcul hash SHA-256 + + API->>DB: SELECT MAX(version)
WHERE type='cgu' + DB-->>API: version = 3 + + API->>API: Nouvelle version = 4 + + API->>FS: Enregistrer fichier
/documents/legaux/cgu_v4_.pdf + FS-->>API: ✅ Fichier sauvegardé + + API->>DB: INSERT INTO documents_legaux
(type, version=4, actif=false) + DB-->>API: ✅ Document créé + + API-->>A: 201 Created
{id, version: 4, actif: false} + + A->>A: Prévisualisation PDF + A->>API: PATCH /api/v1/documents-legaux/{id}/activer + + API->>DB: BEGIN TRANSACTION + API->>DB: UPDATE documents_legaux
SET actif=false WHERE type='cgu' + API->>DB: UPDATE documents_legaux
SET actif=true, active_le=now()
WHERE id={id} + API->>DB: COMMIT + + API-->>A: ✅ CGU v4 activées +``` + +--- + +## 📥 Workflow Acceptation Utilisateur + +### Diagramme de séquence + +```mermaid +sequenceDiagram + participant U as Utilisateur + participant App as Frontend + participant API as Backend + participant DB as PostgreSQL + + U->>App: Inscription (étape CGU) + + App->>API: GET /api/v1/documents-legaux/actifs + API->>DB: SELECT * FROM documents_legaux
WHERE actif=true + DB-->>API: {cgu: v4, privacy: v2} + API-->>App: {cgu: {version: 4, url: '...'}, privacy: {...}} + + App->>App: Afficher liens PDF
"CGU v4" et "Privacy v2" + + U->>U: Lit les documents + U->>U: Coche "J'accepte" + + App->>API: POST /api/v1/auth/register
{..., cgu_version: 4, privacy_version: 2, ip, user_agent} + + API->>DB: BEGIN TRANSACTION + + API->>DB: INSERT INTO utilisateurs
(..., cgu_version_acceptee=4, privacy_version_acceptee=2) + DB-->>API: id_utilisateur + + API->>DB: INSERT INTO acceptations_documents
(id_utilisateur, type='cgu', version=4, ip, user_agent) + API->>DB: INSERT INTO acceptations_documents
(id_utilisateur, type='privacy', version=2, ip, user_agent) + + API->>DB: COMMIT + + API-->>App: ✅ Inscription réussie +``` + +--- + +## 🔌 APIs + +### API 1 : Récupérer documents actifs (Public) + +```http +GET /api/v1/documents-legaux/actifs +``` + +**Réponse 200** : +```json +{ + "cgu": { + "id": "uuid-cgu-v4", + "type": "cgu", + "version": 4, + "url": "/api/v1/documents-legaux/uuid-cgu-v4/download", + "active_le": "2025-11-20T14:30:00Z" + }, + "privacy": { + "id": "uuid-privacy-v2", + "type": "privacy", + "version": 2, + "url": "/api/v1/documents-legaux/uuid-privacy-v2/download", + "active_le": "2025-10-15T09:15:00Z" + } +} +``` + +--- + +### API 2 : Lister versions (Admin) + +```http +GET /api/v1/documents-legaux/:type/versions +Authorization: Bearer +``` + +**Paramètres** : +- `type` : `cgu` | `privacy` + +**Réponse 200** : +```json +[ + { + "id": "uuid-cgu-v4", + "version": 4, + "fichier_nom": "CGU_Mairie_Bezons_2025.pdf", + "actif": true, + "televerse_par": { + "id": "uuid-admin", + "prenom": "Lucas", + "nom": "MOREAU" + }, + "televerse_le": "2025-11-20T14:00:00Z", + "active_le": "2025-11-20T14:30:00Z" + }, + { + "id": "uuid-cgu-v3", + "version": 3, + "fichier_nom": "CGU_v3.pdf", + "actif": false, + "televerse_par": { + "id": "uuid-admin", + "prenom": "Admin", + "nom": "Système" + }, + "televerse_le": "2025-10-15T09:00:00Z", + "active_le": "2025-10-15T09:15:00Z" + } +] +``` + +--- + +### API 3 : Upload nouvelle version (Admin) + +```http +POST /api/v1/documents-legaux +Authorization: Bearer +Content-Type: multipart/form-data +``` + +**Body** : +``` +type: cgu +file: +``` + +**Réponse 201** : +```json +{ + "id": "uuid-nouveau-doc", + "type": "cgu", + "version": 5, + "fichier_nom": "CGU_Mairie_Bezons_2025_v2.pdf", + "actif": false, + "televerse_le": "2025-11-25T10:00:00Z" +} +``` + +**Erreurs** : +- `400 Bad Request` : Fichier non PDF ou trop volumineux (>10MB) +- `401 Unauthorized` : Token manquant ou invalide +- `403 Forbidden` : Rôle insuffisant (pas super_admin) + +--- + +### API 4 : Activer une version (Admin) + +```http +PATCH /api/v1/documents-legaux/:id/activer +Authorization: Bearer +``` + +**Réponse 200** : +```json +{ + "message": "Document activé avec succès", + "documentId": "uuid-nouveau-doc", + "type": "cgu", + "version": 5 +} +``` + +--- + +### API 5 : Télécharger document (Public) + +```http +GET /api/v1/documents-legaux/:id/download +``` + +**Réponse 200** : +``` +Content-Type: application/pdf +Content-Disposition: attachment; filename="CGU_v5.pdf" + + +``` + +--- + +### API 6 : Historique acceptations utilisateur (Admin) + +```http +GET /api/v1/users/:userId/acceptations +Authorization: Bearer +``` + +**Réponse 200** : +```json +[ + { + "type_document": "cgu", + "version_document": 4, + "accepte_le": "2025-11-20T15:30:00Z", + "ip_address": "192.168.1.100", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + }, + { + "type_document": "privacy", + "version_document": 2, + "accepte_le": "2025-11-20T15:30:00Z", + "ip_address": "192.168.1.100", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + } +] +``` + +--- + +## 💻 Interface Admin + +### Écran Gestion Documents Légaux + +``` +┌─────────────────────────────────────────────────────────┐ +│ 📄 Gestion des Documents Légaux │ +├─────────────────────────────────────────────────────────┤ +│ [ CGU ] [ Politique de confidentialité ] │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ 📋 Conditions Générales d'Utilisation (CGU) │ +│ │ +│ Version active : v4 │ +│ Activée le : 20/11/2025 14:30 │ +│ Téléversée par : Lucas MOREAU │ +│ │ +│ [ 📥 Télécharger ] [ 👁️ Prévisualiser ] │ +│ │ +│ ───────────────────────────────────────────────── │ +│ │ +│ 📤 Uploader une nouvelle version │ +│ │ +│ ⚠️ Attention : L'upload d'une nouvelle version │ +│ créera la version v5. Vous pourrez la prévisualiser│ +│ avant de l'activer. │ +│ │ +│ [ Choisir un fichier PDF ] (max 10MB) │ +│ │ +│ [ 📤 Uploader ] │ +│ │ +│ ───────────────────────────────────────────────── │ +│ │ +│ 📜 Historique des versions │ +│ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ ✅ v4 (Active) │ │ +│ │ Activée le : 20/11/2025 14:30 │ │ +│ │ Par : Lucas MOREAU │ │ +│ │ Hash : a3f8b2c4...e0f1a2 ✓ │ │ +│ │ [ 📥 Télécharger ] [ 👁️ Voir ] │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ v3 (Inactive) │ │ +│ │ Activée le : 15/10/2025 09:15 │ │ +│ │ Par : Admin Système │ │ +│ │ Hash : b4f9c3d6...f2a3b4 ✓ │ │ +│ │ [ 📥 Télécharger ] [ 👁️ Voir ] [🔄 Réactiver]│ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 🔒 Conformité RGPD + +### Données capturées + +| Donnée | Justification RGPD | Durée conservation | +|--------|-------------------|-------------------| +| `id_utilisateur` | Traçabilité acceptation | Durée du compte | +| `version_document` | Preuve version acceptée | Durée du compte | +| `accepte_le` | Horodatage légal | Durée du compte | +| `ip_address` | Preuve origine acceptation | 1 an (recommandé) | +| `user_agent` | Preuve navigateur/appareil | 1 an (recommandé) | + +### Droits utilisateur + +#### Droit d'accès (Article 15) +L'utilisateur peut demander : +- Quelles versions il a acceptées +- Quand il les a acceptées +- Depuis quelle IP + +**API** : `GET /api/v1/users/me/acceptations` + +#### Droit à l'oubli (Article 17) +Lors de la suppression du compte : +- Suppression des données personnelles +- Conservation des acceptations anonymisées (obligation légale) + +**Implémentation** : +```sql +-- Anonymisation (pas suppression totale) +UPDATE acceptations_documents +SET ip_address = NULL, + user_agent = NULL +WHERE id_utilisateur = ''; + +-- Puis suppression utilisateur +DELETE FROM utilisateurs WHERE id = ''; +``` + +--- + +## 📚 Références + +### Documentation interne +- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md) +- [10_DATABASE.md](./10_DATABASE.md) +- [11_API.md](./11_API.md) +- [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md) + +### Documentation externe +- [RGPD - Article 7 (Consentement)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre2#Article7) +- [RGPD - Article 15 (Droit d'accès)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre3#Article15) +- [RGPD - Article 17 (Droit à l'oubli)](https://www.cnil.fr/fr/reglement-europeen-protection-donnees/chapitre3#Article17) + +--- + +**Dernière mise à jour** : 25 Novembre 2025 +**Version** : 1.0 +**Statut** : ✅ Document validé + + + diff --git a/docs/23_LISTE-TICKETS.md b/docs/23_LISTE-TICKETS.md new file mode 100644 index 0000000..6bb9a32 --- /dev/null +++ b/docs/23_LISTE-TICKETS.md @@ -0,0 +1,1144 @@ +# 🎫 Liste Complète des Tickets - Projet P'titsPas + +**Version** : 1.0 +**Date** : 25 Novembre 2025 +**Auteur** : Équipe PtitsPas +**Estimation totale** : ~173h + +--- + +## 📊 Vue d'ensemble + +### Répartition par priorité + +| Priorité | Nombre | Estimation | Description | +|----------|--------|------------|-------------| +| **P0** | 7 tickets | ~5h | Amendements BDD (BLOQUANT) | +| **P1** | 7 tickets | ~22h | Configuration système (BLOQUANT) | +| **P2** | 18 tickets | ~50h | Backend métier | +| **P3** | 17 tickets | ~52h | Frontend | +| **P4** | 4 tickets | ~24h | Tests & Documentation | +| **CRITIQUES** | 6 tickets | ~13h | Upload, Logs, Infra, CDC | +| **JURIDIQUE** | 1 ticket | ~8h | Rédaction CGU/Privacy | +| **TOTAL** | **61 tickets** | **~173h** | | + +--- + +## 🔴 PRIORITÉ 0 : Amendements Base de Données (BLOQUANT) + +### Ticket #1 : [BDD] Ajout champs manquants conformité CDC +**Estimation** : 1h +**Labels** : `bdd`, `p0-bloquant`, `cdc` + +**Description** : +Ajouter les champs manquants dans la base de données pour être conforme au Cahier des Charges v1.3. + +**Tâches** : +- [ ] Ajouter `ville_naissance` VARCHAR(150) dans `utilisateurs` +- [ ] Ajouter `pays_naissance` VARCHAR(100) dans `utilisateurs` +- [ ] Ajouter `date_acceptation_cgu` TIMESTAMPTZ dans `utilisateurs` +- [ ] Rendre `nir_chiffre` NOT NULL dans `assistantes_maternelles` +- [ ] Ajouter `date_obtention_agrement` DATE dans `assistantes_maternelles` +- [ ] Créer migration Prisma +- [ ] Tester migration sur BDD de dev + +--- + +### Ticket #2 : [BDD] Ajout table/champ présentation dossier parent +**Estimation** : 30min +**Labels** : `bdd`, `p0-bloquant`, `cdc` + +**Description** : +Ajouter un champ pour stocker la présentation du dossier parent (étape 4 de l'inscription). + +**Tâches** : +- [ ] Ajouter `presentation_dossier` TEXT dans table `parents` +- [ ] Créer migration Prisma +- [ ] Tester migration + +--- + +### Ticket #3 : [BDD] Ajout gestion tokens création mot de passe ✅ +**Estimation** : 30min +**Labels** : `bdd`, `p0-bloquant`, `security` +**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28) + +**Description** : +Ajouter les champs nécessaires pour gérer les tokens de création de mot de passe (workflow sans MDP lors inscription). + +**Tâches** : +- [x] Ajouter `password_reset_token` UUID dans `utilisateurs` +- [x] Ajouter `password_reset_expires` TIMESTAMPTZ dans `utilisateurs` +- [x] Créer migration Prisma +- [x] Tester migration + +--- + +### Ticket #4 : [BDD] Ajout champ genre obligatoire enfants ✅ +**Estimation** : 30min +**Labels** : `bdd`, `p0-bloquant`, `cdc` +**Statut** : ✅ TERMINÉ (Fermé le 2025-11-28) + +**Description** : +Ajouter le champ `genre` obligatoire (H/F) dans la table `enfants`. + +**Tâches** : +- [x] Ajouter `genre` ENUM('H', 'F') NOT NULL dans `enfants` +- [x] Créer migration Prisma +- [x] Tester migration + +--- + +### Ticket #5 : [BDD] Supprimer champs obsolètes +**Estimation** : 30min +**Labels** : `bdd`, `p0-bloquant`, `cleanup` + +**Description** : +Supprimer les champs obsolètes identifiés lors de l'audit. + +**Tâches** : +- [ ] Supprimer `mobile` dans `utilisateurs` (remplacé par `telephone`) +- [ ] Supprimer `telephone_fixe` dans `utilisateurs` +- [ ] Supprimer `annee_experience` dans `assistantes_maternelles` +- [ ] Supprimer `specialite` dans `assistantes_maternelles` +- [ ] Créer migration Prisma +- [ ] Tester migration + +--- + +### Ticket #6 : [BDD] Table configuration système +**Estimation** : 1h +**Labels** : `bdd`, `p0-bloquant`, `on-premise` + +**Description** : +Créer la table `configuration` pour stocker les paramètres système (SMTP, app, sécurité). + +**Tâches** : +- [ ] Créer table `configuration` (clé/valeur/type/catégorie) +- [ ] Créer index sur `cle` et `categorie` +- [ ] Seed valeurs par défaut (SMTP localhost, ports, etc.) +- [ ] Créer migration Prisma +- [ ] Tester migration + +**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md) + +--- + +### Ticket #7 : [BDD] Tables documents légaux & acceptations ✅ +**Estimation** : 2h +**Labels** : `bdd`, `p0-bloquant`, `rgpd`, `juridique` +**Statut** : ✅ TERMINÉ (Fermé le 2025-11-30 - Ticket #68 sur Gitea) + +**Description** : +Créer les tables pour gérer les versions des documents légaux (CGU/Privacy) et tracer les acceptations utilisateurs. + +**Tâches** : +- [ ] Créer table `documents_legaux` (versioning + hash SHA-256) +- [ ] Créer table `acceptations_documents` (traçabilité RGPD) +- [ ] Modifier table `utilisateurs` (colonnes version acceptée) +- [ ] Créer index +- [ ] Seed documents génériques v1 +- [ ] Créer migration Prisma +- [ ] Tester migration + +**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md) + +--- + +## 🟠 PRIORITÉ 1 : Configuration Système (BLOQUANT) + +### Ticket #8 : [Backend] Service Configuration +**Estimation** : 4h +**Labels** : `backend`, `p1-bloquant`, `on-premise` + +**Description** : +Créer le service de configuration avec cache en mémoire et chiffrement AES-256. + +**Tâches** : +- [ ] Créer `ConfigService` avec cache Map +- [ ] Implémenter `get(key, defaultValue)` +- [ ] Implémenter `set(key, value, userId)` +- [ ] Implémenter `getByCategory(category)` +- [ ] Implémenter chiffrement/déchiffrement AES-256 +- [ ] Implémenter conversion de types (string/number/boolean/json) +- [ ] Implémenter `loadCache()` au démarrage +- [ ] Tests unitaires (mock repository) + +**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md#service-configuration) + +--- + +### Ticket #9 : [Backend] API Configuration +**Estimation** : 3h +**Labels** : `backend`, `p1-bloquant`, `on-premise` + +**Description** : +Créer les endpoints REST pour gérer la configuration système. + +**Tâches** : +- [ ] `GET /api/v1/configuration/:category` (super_admin only) +- [ ] `PATCH /api/v1/configuration/bulk` (mise à jour multiple) +- [ ] `POST /api/v1/configuration/test-smtp` (test connexion + envoi email) +- [ ] Guards (super_admin only) +- [ ] Validation DTO +- [ ] Tests unitaires + +**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md#apis-configuration) + +--- + +### Ticket #10 : [Backend] Guard Configuration Initiale +**Estimation** : 2h +**Labels** : `backend`, `p1-bloquant`, `on-premise` + +**Description** : +Créer un Guard/Middleware qui détecte si la configuration initiale est incomplète et force la redirection. + +**Tâches** : +- [ ] Créer `SetupGuard` +- [ ] Vérifier `setup_completed` dans ConfigService +- [ ] Redirection forcée vers `/admin/setup` si false +- [ ] Exemption pour routes publiques (login, register) +- [ ] Exemption pour route `/admin/setup` +- [ ] Tests unitaires + +**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md#workflow-setup-initial) + +--- + +### Ticket #11 : [Backend] Adaptation MailService pour config dynamique +**Estimation** : 3h +**Labels** : `backend`, `p1-bloquant`, `on-premise`, `email` + +**Description** : +Adapter le service email pour utiliser la configuration dynamique depuis la BDD au lieu de variables d'environnement hardcodées. + +**Tâches** : +- [ ] Remplacer `mail.ptits-pas.fr` hardcodé par `ConfigService.get('smtp_host')` +- [ ] Lecture config SMTP depuis BDD (host, port, secure, auth, user, pass) +- [ ] Lecture expéditeur depuis BDD (from_name, from_address) +- [ ] Recréation du transport Nodemailer dynamique +- [ ] Tests unitaires (mock ConfigService) + +**⚠️ IMPORTANT** : Ce ticket corrige tous les hardcoding de `mail.ptits-pas.fr` dans le code. + +--- + +### Ticket #12 : [Frontend] Écran Configuration Initiale (Setup Wizard) +**Estimation** : 6h +**Labels** : `frontend`, `p1-bloquant`, `on-premise` + +**Description** : +Créer l'écran de configuration initiale (Setup Wizard) accessible à la première connexion du super admin. + +**Tâches** : +- [ ] Page `/admin/setup` (accessible uniquement si config incomplète) +- [ ] Formulaire multi-onglets (Email / Application / Avancé) +- [ ] Onglet 1 : Configuration Email (SMTP, auth, expéditeur) +- [ ] Onglet 2 : Personnalisation (nom app, URL, logo) +- [ ] Onglet 3 : Paramètres avancés (durées tokens, upload max) +- [ ] Bouton "Tester la connexion SMTP" (appel API + feedback) +- [ ] Validation côté client +- [ ] Sauvegarde (appel API `PATCH /configuration/bulk`) +- [ ] Message succès + redirection dashboard + +**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md#interface-admin) + +--- + +### Ticket #13 : [Frontend] Écran Paramètres (accès permanent) +**Estimation** : 2h +**Labels** : `frontend`, `p1-bloquant`, `on-premise` + +**Description** : +Créer l'écran de paramètres accessible depuis le menu admin (même interface que Setup Wizard). + +**Tâches** : +- [ ] Page `/admin/parametres` (accessible depuis menu admin) +- [ ] Même interface que Setup Wizard +- [ ] Affichage valeurs actuelles +- [ ] Modification et sauvegarde + +--- + +### Ticket #14 : [Doc] Documentation configuration on-premise +**Estimation** : 2h +**Labels** : `documentation`, `p1-bloquant`, `on-premise` + +**Description** : +Rédiger la documentation pour aider les collectivités à configurer l'application. + +**Tâches** : +- [ ] Guide d'installation pour collectivités +- [ ] Liste des paramètres SMTP courants (Office 365, Gmail, serveurs locaux) +- [ ] Exemples de configuration +- [ ] Troubleshooting SMTP +- [ ] Variables `.env` requises +- [ ] Génération secrets (JWT, encryption key) + +**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md) + +--- + +## 🟢 PRIORITÉ 2 : Backend - Authentification & Gestion Comptes + +### Ticket #15 : [Backend] API Création gestionnaire +**Estimation** : 3h +**Labels** : `backend`, `p2`, `auth` + +**Description** : +Créer l'endpoint pour permettre au super admin de créer des gestionnaires. + +**Tâches** : +- [ ] Endpoint `POST /api/v1/gestionnaires` +- [ ] Validation DTO +- [ ] Hash bcrypt +- [ ] Flag `changement_mdp_obligatoire = TRUE` +- [ ] Guards (super_admin only) +- [ ] Email de notification (utiliser MailService avec config dynamique) +- [ ] Tests unitaires + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-2--création-dun-gestionnaire) + +--- + +### Ticket #16 : [Backend] API Inscription Parent (étape 1 - Parent 1) +**Estimation** : 4h +**Labels** : `backend`, `p2`, `auth`, `cdc` + +**Description** : +Créer l'endpoint d'inscription Parent (étape 1/6 : informations Parent 1). + +**Tâches** : +- [ ] Endpoint `POST /api/v1/auth/register/parent` +- [ ] Validation DTO (sans mot de passe) +- [ ] Génération token création MDP (UUID) +- [ ] Durée token : Lire depuis `ConfigService.get('password_reset_token_expiry_days')` +- [ ] Création utilisateur + entité parent +- [ ] Statut `en_attente` +- [ ] Tests unitaires + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-3--inscription-dun-parent) + +--- + +### Ticket #17 : [Backend] API Inscription Parent (étape 2 - Parent 2) +**Estimation** : 2h +**Labels** : `backend`, `p2`, `auth`, `cdc` + +**Description** : +Ajouter la gestion du co-parent (Parent 2) dans l'endpoint d'inscription. + +**Tâches** : +- [ ] Ajout gestion co-parent dans endpoint +- [ ] Relation `id_co_parent` dans table `parents` +- [ ] Génération token pour Parent 2 +- [ ] Tests unitaires + +--- + +### Ticket #18 : [Backend] API Inscription Parent - REFONTE (Workflow complet 6 étapes) ✅ +**Estimation** : 4h +**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload` +**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01) + +**Description** : +Refonte complète de l'API d'inscription parent pour gérer le workflow complet en 6 étapes dans une seule transaction. + +**Tâches** : +- [ ] Endpoint `POST /api/v1/enfants` +- [ ] Upload photo (Multer) +- [ ] Taille max : Lire depuis `ConfigService.get('max_upload_size_mb')` +- [ ] Validation genre obligatoire (H/F) +- [ ] Gestion enfant à naître vs né +- [ ] Rattachement aux 2 parents (si Parent 2 existe) +- [ ] Tests unitaires + +--- + +### Ticket #19 : [Backend] API Inscription Parent (étape 2 - Parent 2) ✅ +**Estimation** : 2h +**Labels** : `backend`, `p2`, `auth`, `cdc` +**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01) + +**Description** : +Gestion du co-parent (Parent 2) dans l'endpoint d'inscription (intégré dans la refonte #18). + +**Tâches** : +- [ ] Enregistrement présentation dossier +- [ ] Enregistrement acceptation CGU avec horodatage +- [ ] Endpoint récapitulatif +- [ ] Tests unitaires + +--- + +### Ticket #20 : [Backend] API Inscription Parent (étape 3 - Enfants) ✅ +**Estimation** : 4h +**Labels** : `backend`, `p2`, `auth`, `cdc`, `upload` +**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01) + +**Description** : +Gestion des enfants dans l'endpoint d'inscription (intégré dans la refonte #18). + +**Tâches** : +- [ ] Endpoint `POST /api/v1/auth/register/am` +- [ ] Upload photo (Multer) +- [ ] Taille max : Lire depuis ConfigService +- [ ] Consentement photo avec horodatage +- [ ] Génération token création MDP +- [ ] Tests unitaires + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-3bis--inscription-dune-assistante-maternelle) + +--- + +### Ticket #21 : [Backend] API Inscription Parent (étape 4-6 - Finalisation) ✅ +**Estimation** : 3h +**Labels** : `backend`, `p2`, `auth`, `cdc` +**Statut** : ✅ TERMINÉ (Fermé le 2025-12-01) + +**Description** : +Finalisation de l'inscription parent (présentation, CGU, récapitulatif - intégré dans la refonte #18). + +**Tâches** : +- [ ] Validation NIR (15 chiffres obligatoire) +- [ ] Date/lieu de naissance +- [ ] Numéro agrément + date obtention +- [ ] Capacité accueil +- [ ] Tests unitaires + +--- + +### Ticket #22 : [Backend] API Création mot de passe +**Estimation** : 3h +**Labels** : `backend`, `p2`, `auth`, `security` + +**Description** : +Créer les endpoints pour permettre aux utilisateurs de créer leur mot de passe via le lien reçu par email. + +**Tâches** : +- [ ] Endpoint `GET /api/v1/auth/verify-token?token=` +- [ ] Endpoint `POST /api/v1/auth/create-password` +- [ ] Validation token (existe, non expiré, non utilisé) +- [ ] Hash bcrypt + suppression token +- [ ] Activation compte (`statut = actif`) +- [ ] Tests unitaires + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-7--création-du-mot-de-passe) + +--- + +### Ticket #23 : [Backend] API Liste comptes en attente +**Estimation** : 2h +**Labels** : `backend`, `p2`, `gestionnaire` + +**Description** : +Créer les endpoints pour lister les comptes en attente de validation. + +**Tâches** : +- [ ] Endpoint `GET /api/v1/parents?statut=en_attente` +- [ ] Endpoint `GET /api/v1/assistantes-maternelles?statut=en_attente` +- [ ] Guards (gestionnaire/admin only) +- [ ] Filtres + pagination +- [ ] Tests unitaires + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-4--consultation-des-demandes-par-le-gestionnaire) + +--- + +### Ticket #24 : [Backend] API Validation/Refus comptes +**Estimation** : 3h +**Labels** : `backend`, `p2`, `gestionnaire` + +**Description** : +Créer les endpoints pour valider ou refuser les comptes en attente. + +**Tâches** : +- [ ] Endpoint `PATCH /api/v1/users/{id}/valider` +- [ ] Endpoint `PATCH /api/v1/users/{id}/suspendre` +- [ ] Création entrée table `validations` +- [ ] Guards (gestionnaire/admin only) +- [ ] Tests unitaires + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-5--validation-dun-compte) + +--- + +### Ticket #25 : [Backend] Service Email - Installation Nodemailer +**Estimation** : 2h +**Labels** : `backend`, `p2`, `email` + +**Description** : +Installer et configurer Nodemailer pour l'envoi d'emails. + +**Tâches** : +- [ ] Installation Nodemailer + types +- [ ] Configuration de base +- [ ] Service `MailService` avec méthodes de base +- [ ] Tests unitaires (mock SMTP) + +--- + +### Ticket #26 : [Backend] Templates Email - Validation +**Estimation** : 3h +**Labels** : `backend`, `p2`, `email` + +**Description** : +Créer les templates d'emails pour la validation des comptes (avec lien création MDP). + +**Tâches** : +- [ ] Template Parent 1 (création MDP) +- [ ] URL app : Lire depuis `ConfigService.get('app_url')` +- [ ] Nom app : Lire depuis `ConfigService.get('app_name')` +- [ ] Expéditeur : Lire depuis ConfigService +- [ ] Template Parent 2 (co-parent création MDP) +- [ ] Template AM (création MDP) +- [ ] Intégration Handlebars +- [ ] Tests d'envoi + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-6--réception-de-la-notification) + +--- + +### Ticket #27 : [Backend] Templates Email - Refus +**Estimation** : 1h +**Labels** : `backend`, `p2`, `email` + +**Description** : +Créer le template d'email pour le refus de compte. + +**Tâches** : +- [ ] Template refus générique +- [ ] Variables dynamiques : Nom app, URL, expéditeur depuis ConfigService +- [ ] Intégration dans endpoint suspendre +- [ ] Tests d'envoi + +--- + +### Ticket #28 : [Backend] Connexion - Vérification statut +**Estimation** : 2h +**Labels** : `backend`, `p2`, `auth` + +**Description** : +Modifier l'endpoint de connexion pour bloquer les comptes en attente ou suspendus. + +**Tâches** : +- [ ] Modifier endpoint `POST /api/v1/auth/login` +- [ ] Bloquer si `statut = en_attente` +- [ ] Bloquer si `statut = suspendu` +- [ ] Messages d'erreur explicites +- [ ] Tests unitaires + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-8--connexion-de-lutilisateur-validé) + +--- + +### Ticket #29 : [Backend] Changement MDP obligatoire première connexion +**Estimation** : 2h +**Labels** : `backend`, `p2`, `auth`, `security` + +**Description** : +Implémenter le changement de mot de passe obligatoire pour les gestionnaires/admins à la première connexion. + +**Tâches** : +- [ ] Endpoint `POST /api/v1/auth/change-password-required` +- [ ] Vérification flag `changement_mdp_obligatoire` +- [ ] Mise à jour flag après changement +- [ ] Tests unitaires + +--- + +### Ticket #30 : [Backend] Service Documents Légaux +**Estimation** : 4h +**Labels** : `backend`, `p2`, `juridique`, `rgpd` + +**Description** : +Créer le service de gestion des documents légaux (CGU/Privacy) avec versioning. + +**Tâches** : +- [ ] `DocumentsLegauxService` complet +- [ ] Upload avec versioning auto-incrémenté +- [ ] Calcul hash SHA-256 +- [ ] Activation/désactivation versions +- [ ] Méthode `getDocumentsActifs()` +- [ ] Méthode `uploadNouvelleVersion()` +- [ ] Méthode `activerVersion()` +- [ ] Méthode `listerVersions()` +- [ ] Tests unitaires + +**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md#service-documents-légaux) + +--- + +### Ticket #31 : [Backend] API Documents Légaux +**Estimation** : 3h +**Labels** : `backend`, `p2`, `juridique`, `rgpd` + +**Description** : +Créer les endpoints REST pour gérer les documents légaux. + +**Tâches** : +- [ ] `GET /api/v1/documents-legaux/actifs` (public) +- [ ] `GET /api/v1/documents-legaux/:type/versions` (admin) +- [ ] `POST /api/v1/documents-legaux` (upload, admin only) +- [ ] `PATCH /api/v1/documents-legaux/:id/activer` (admin only) +- [ ] `GET /api/v1/documents-legaux/:id/download` (public) +- [ ] Guards + validation +- [ ] Tests unitaires + +**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md#apis) + +--- + +### Ticket #32 : [Backend] Traçabilité acceptations documents +**Estimation** : 2h +**Labels** : `backend`, `p2`, `rgpd` + +**Description** : +Enregistrer les acceptations de documents légaux lors de l'inscription (traçabilité RGPD). + +**Tâches** : +- [ ] Enregistrement dans `acceptations_documents` lors inscription +- [ ] Capture IP + User-Agent +- [ ] API `GET /api/v1/users/:id/acceptations` (admin) +- [ ] Tests unitaires + +**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md#workflow-acceptation-utilisateur) + +--- + +## 🟢 PRIORITÉ 3 : Frontend - Interfaces + +### Ticket #33 : [Frontend] Écran Création Gestionnaire +**Estimation** : 3h +**Labels** : `frontend`, `p3`, `auth` + +**Description** : +Créer l'écran de création de gestionnaire (super admin uniquement). + +**Tâches** : +- [ ] Formulaire (nom, prénom, email, MDP) +- [ ] Validation côté client +- [ ] Appel API `POST /gestionnaires` +- [ ] Messages succès/erreur + +--- + +### Ticket #34 : [Réservé - Non utilisé] + +--- + +### Ticket #35 : [Réservé - Non utilisé] + +--- + +### Ticket #36 : [Frontend] Inscription Parent - Étape 1 (Parent 1) ✅ +**Estimation** : 3h +**Labels** : `frontend`, `p3`, `auth`, `cdc` +**Statut** : ✅ TERMINÉ (PR #73 mergée le 2025-12-01) + +**Description** : +Créer le formulaire d'inscription parent - étape 1/6 (informations Parent 1). + +**Tâches** : +- [x] Formulaire identité Parent 1 +- [x] Validation côté client +- [x] Pas de champ mot de passe +- [x] Navigation vers étape 2 +- [x] Améliorations visuelles (labels 22px, champs 20px, espacement 32px) +- [x] Correction indicateur étape 1/6 + +--- + +### Ticket #37 : [Frontend] Inscription Parent - Étape 2 (Parent 2) +**Estimation** : 3h +**Labels** : `frontend`, `p3`, `auth`, `cdc` + +**Description** : +Créer le formulaire d'inscription parent - étape 2/6 (informations Parent 2 optionnel). + +**Tâches** : +- [ ] Question "Ajouter un second parent ?" +- [ ] Formulaire identité Parent 2 (conditionnel) +- [ ] Checkbox "Même adresse" +- [ ] Navigation vers étape 3 +- [ ] Pas de champ mot de passe +- [ ] Améliorations visuelles (mêmes que Step1) +- [ ] Correction indicateur étape 2/6 + +--- + +### Ticket #38 : [Frontend] Inscription Parent - Étape 3 (Enfants) +**Estimation** : 4h +**Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload` + +**Description** : +Créer le formulaire d'inscription parent - étape 3/6 (informations enfants). + +**Tâches** : +- [ ] Question "Enfant déjà né ?" +- [ ] Formulaire enfant (prénom, date, genre H/F obligatoire) +- [ ] Upload photo (si né) +- [ ] Validation taille (5MB) +- [ ] Bouton "Ajouter un autre enfant" +- [ ] Navigation vers étape 4 + +--- + +### Ticket #39 : [Frontend] Inscription Parent - Étapes 4-6 (Finalisation) +**Estimation** : 4h +**Labels** : `frontend`, `p3`, `auth`, `cdc` + +**Description** : +Créer les étapes finales de l'inscription parent (présentation, CGU, récapitulatif). + +**Tâches** : +- [ ] Étape 4 : Textarea présentation +- [ ] Étape 5 : Checkbox CGU + liens PDF +- [ ] Étape 6 : Récapitulatif complet +- [ ] Bouton "Modifier" / "Valider" +- [ ] Appel API final +- [ ] Message confirmation + +--- + +### Ticket #40 : [Frontend] Inscription AM - Panneau 1 (Identité) +**Estimation** : 3h +**Labels** : `frontend`, `p3`, `auth`, `cdc`, `upload` + +**Description** : +Créer le formulaire d'inscription AM - panneau 1/5 (identité). + +**Tâches** : +- [ ] Formulaire identité +- [ ] Upload photo +- [ ] Checkbox consentement photo +- [ ] Pas de champ mot de passe +- [ ] Navigation vers panneau 2 + +--- + +### Ticket #41 : [Frontend] Inscription AM - Panneau 2 (Infos pro) +**Estimation** : 3h +**Labels** : `frontend`, `p3`, `auth`, `cdc` + +**Description** : +Créer le formulaire d'inscription AM - panneau 2/5 (informations professionnelles). + +**Tâches** : +- [ ] Formulaire infos pro +- [ ] Champ NIR (15 chiffres, validation) +- [ ] Date/lieu naissance +- [ ] Agrément + date obtention +- [ ] Navigation vers présentation + +--- + +### Ticket #42 : [Frontend] Inscription AM - Finalisation +**Estimation** : 3h +**Labels** : `frontend`, `p3`, `auth`, `cdc` + +**Description** : +Créer les étapes finales de l'inscription AM (présentation, CGU, récapitulatif). + +**Tâches** : +- [ ] Textarea présentation +- [ ] Checkbox CGU + liens PDF +- [ ] Récapitulatif (NIR masqué) +- [ ] Appel API final +- [ ] Message confirmation + +--- + +### Ticket #43 : [Frontend] Écran Création Mot de Passe +**Estimation** : 3h +**Labels** : `frontend`, `p3`, `auth` + +**Description** : +Créer l'écran de création de mot de passe (lien reçu par email). + +**Tâches** : +- [ ] Page `/create-password?token=` +- [ ] Vérification token (appel API) +- [ ] Formulaire MDP + confirmation +- [ ] Validation côté client (8 car, 1 maj, 1 chiffre) +- [ ] Appel API création MDP +- [ ] Redirection vers login + +--- + +### Ticket #44 : [Frontend] Dashboard Gestionnaire - Structure +**Estimation** : 2h +**Labels** : `frontend`, `p3`, `gestionnaire` + +**Description** : +Créer la structure du dashboard gestionnaire avec 2 onglets. + +**Tâches** : +- [ ] Layout avec 2 onglets (Parents / AM) +- [ ] Navigation entre onglets +- [ ] État vide ("Aucune demande") + +--- + +### Ticket #45 : [Frontend] Dashboard Gestionnaire - Liste Parents +**Estimation** : 4h +**Labels** : `frontend`, `p3`, `gestionnaire` + +**Description** : +Créer la liste des parents en attente de validation. + +**Tâches** : +- [ ] Appel API `GET /parents?statut=en_attente` +- [ ] Affichage liste avec cartes +- [ ] Boutons Valider/Refuser +- [ ] Modale confirmation +- [ ] Rafraîchissement après action + +--- + +### Ticket #46 : [Frontend] Dashboard Gestionnaire - Liste AM +**Estimation** : 4h +**Labels** : `frontend`, `p3`, `gestionnaire` + +**Description** : +Créer la liste des assistantes maternelles en attente de validation. + +**Tâches** : +- [ ] Appel API `GET /assistantes-maternelles?statut=en_attente` +- [ ] Affichage liste avec cartes +- [ ] NIR masqué +- [ ] Boutons Valider/Refuser +- [ ] Modale confirmation +- [ ] Rafraîchissement après action + +--- + +### Ticket #47 : [Frontend] Écran Changement MDP Obligatoire +**Estimation** : 2h +**Labels** : `frontend`, `p3`, `auth`, `security` + +**Description** : +Créer l'écran de changement de mot de passe obligatoire (première connexion gestionnaire/admin). + +**Tâches** : +- [ ] Détection flag `changement_mdp_obligatoire` +- [ ] Modale bloquante après login +- [ ] Formulaire MDP actuel + nouveau MDP +- [ ] Appel API +- [ ] Redirection vers dashboard + +--- + +### Ticket #48 : [Frontend] Gestion Erreurs & Messages +**Estimation** : 2h +**Labels** : `frontend`, `p3`, `ux` + +**Description** : +Créer un système de gestion des erreurs et messages utilisateur. + +**Tâches** : +- [ ] Intercepteur HTTP pour erreurs API +- [ ] Snackbars/Toasts pour succès/erreurs +- [ ] Messages explicites (compte en attente, refusé, etc.) + +--- + +### Ticket #49 : [Frontend] Écran Gestion Documents Légaux (Admin) +**Estimation** : 5h +**Labels** : `frontend`, `p3`, `juridique`, `admin` + +**Description** : +Créer l'écran de gestion des documents légaux (CGU/Privacy) pour l'admin. + +**Tâches** : +- [ ] Onglets CGU / Privacy +- [ ] Affichage version active +- [ ] Upload nouvelle version (PDF uniquement) +- [ ] Prévisualisation PDF +- [ ] Activation version +- [ ] Historique versions + +**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md#interface-admin) + +--- + +### Ticket #50 : [Frontend] Affichage dynamique CGU lors inscription +**Estimation** : 2h +**Labels** : `frontend`, `p3`, `juridique` + +**Description** : +Afficher dynamiquement les CGU/Privacy lors de l'inscription (avec numéro de version). + +**Tâches** : +- [ ] Appel API `GET /documents-legaux/actifs` +- [ ] Liens vers PDF avec numéro de version +- [ ] Envoi version acceptée lors inscription + +--- + +### Ticket #51 : [Frontend] Écran Logs Admin (optionnel v1.1) +**Estimation** : 4h +**Labels** : `frontend`, `p3`, `admin`, `logs` + +**Description** : +Créer l'écran de consultation des logs système (optionnel pour v1.1). + +**Tâches** : +- [ ] Appel API logs +- [ ] Filtres (date, niveau, utilisateur) +- [ ] Pagination +- [ ] Export CSV + +--- + +## 🔵 PRIORITÉ 4 : Tests & Documentation + +### Ticket #52 : [Tests] Tests unitaires Backend +**Estimation** : 8h +**Labels** : `tests`, `p4`, `backend` + +**Description** : +Créer les tests unitaires pour tous les services et controllers backend. + +**Tâches** : +- [ ] Tests services (auth, user, mail, config, documents) +- [ ] Tests controllers +- [ ] Couverture > 80% +- [ ] Mocks (repository, ConfigService, MailService) + +--- + +### Ticket #50 : [Tests] Tests intégration Backend +**Estimation** : 5h +**Labels** : `tests`, `p4`, `backend` + +**Description** : +Créer les tests d'intégration pour les workflows complets. + +**Tâches** : +- [ ] Workflow complet Parent +- [ ] Workflow complet AM +- [ ] Workflow refus +- [ ] Workflow configuration initiale + +--- + +### Ticket #51 : [Tests] Tests E2E Frontend +**Estimation** : 8h +**Labels** : `tests`, `p4`, `frontend` + +**Description** : +Créer les tests end-to-end pour les parcours utilisateurs. + +**Tâches** : +- [ ] Workflow configuration initiale +- [ ] Workflow création gestionnaire +- [ ] Workflow inscription parent +- [ ] Workflow inscription AM +- [ ] Workflow dashboard gestionnaire + +--- + +### Ticket #52 : [Doc] Documentation API OpenAPI/Swagger +**Estimation** : 3h +**Labels** : `documentation`, `p4`, `api` + +**Description** : +Générer et documenter l'API avec Swagger/OpenAPI. + +**Tâches** : +- [ ] Génération Swagger depuis NestJS +- [ ] Documentation tous les endpoints +- [ ] Exemples de requêtes/réponses +- [ ] Schémas DTO + +--- + +## ⚠️ CRITIQUES : Upload, Emails, Infra, Doc + +### Ticket #53 : [Backend] Service Upload & Stockage fichiers +**Estimation** : 3h +**Labels** : `backend`, `critique`, `upload` + +**Description** : +Créer le service de gestion des uploads de fichiers (photos). + +**Tâches** : +- [ ] Multer pour upload +- [ ] Validation (type MIME, taille max depuis config) +- [ ] Stockage dans `/uploads/photos/` +- [ ] Nettoyage fichiers orphelins +- [ ] Tests unitaires + +--- + +### Ticket #54 : [Backend] API Téléchargement photos sécurisé +**Estimation** : 2h +**Labels** : `backend`, `critique`, `upload`, `security` + +**Description** : +Créer l'endpoint sécurisé pour télécharger les photos. + +**Tâches** : +- [ ] Endpoint `GET /api/v1/files/:id` +- [ ] Vérification droits (parent voit ses enfants, AM voit son profil, gestionnaire voit tout) +- [ ] Stream fichier +- [ ] Tests unitaires + +--- + +### Ticket #55 : [Backend] Service Logging (Winston) +**Estimation** : 3h +**Labels** : `backend`, `critique`, `monitoring` + +**Description** : +Mettre en place un système de logs centralisé avec Winston pour faciliter le debugging et le monitoring. + +**Tâches** : +- [ ] Installation Winston + transports +- [ ] Configuration (console + fichier avec rotation daily) +- [ ] Logs applicatifs (info, warn, error) +- [ ] Logs emails (succès/échec SMTP avec détails) +- [ ] Logs connexions (qui se connecte quand, IP) +- [ ] Logs validations (qui valide quoi, timestamp) +- [ ] Rotation des logs (daily, max 30 jours) +- [ ] Tests + +--- + +### Ticket #56 : [Frontend] Écran Logs Admin (optionnel v1.1) +**Estimation** : 4h +**Labels** : `frontend`, `p3`, `monitoring`, `admin` + +**Description** : +Créer un écran pour consulter les logs depuis l'interface admin (optionnel Phase 1.1). + +**Tâches** : +- [ ] Page `/admin/logs` (super_admin only) +- [ ] Filtres (date, niveau, type) +- [ ] Pagination +- [ ] Recherche +- [ ] Export CSV + +**Note** : Peut être fait en Phase 1.1 si le temps le permet + +--- + +### Ticket #57 : [Infra] Volume Docker pour uploads +**Estimation** : 30min +**Labels** : `infra`, `critique`, `docker` + +**Description** : +Ajouter un volume Docker pour persister les fichiers uploadés. + +**Tâches** : +- [ ] Ajout volume `/uploads` dans `docker-compose.yml` +- [ ] Persistance des fichiers uploadés +- [ ] Test redémarrage conteneur + +--- + +### Ticket #58 : [Infra] Volume Docker pour documents légaux +**Estimation** : 30min +**Labels** : `infra`, `critique`, `docker` + +**Description** : +Ajouter un volume Docker pour persister les documents légaux (CGU/Privacy). + +**Tâches** : +- [ ] Ajout volume `/documents/legaux` dans `docker-compose.yml` +- [ ] Persistance des PDF +- [ ] Test redémarrage conteneur + +--- + +### Ticket #59 : [Doc] Guide installation & configuration +**Estimation** : 3h +**Labels** : `documentation`, `critique`, `on-premise` + +**Description** : +Rédiger le guide complet d'installation et de configuration pour les collectivités. + +**Tâches** : +- [ ] Variables `.env` requises +- [ ] Génération secrets (JWT, encryption) +- [ ] Configuration SMTP courante +- [ ] Troubleshooting +- [ ] Commandes Docker Compose +- [ ] Backup/Restauration + +--- + +## 📚 JURIDIQUE & CDC + +### Ticket #60 : [Doc] Amendement CDC v1.4 - Suppression SMS +**Estimation** : 30min +**Labels** : `documentation`, `cdc` + +**Description** : +Amender le Cahier des Charges pour supprimer la mention des notifications SMS (pas de plus-value identifiée). + +**Tâches** : +- [ ] Modifier CDC Section 3.1.6 (Notifications parent) +- [ ] Modifier CDC Section 3.2.5 (Notifications AM) +- [ ] Remplacer "Email OU SMS" par "Email" +- [ ] Supprimer toute mention de SMS +- [ ] Incrémenter version → v1.4 +- [ ] Ajouter changelog : "Suppression notifications SMS (Email uniquement)" + +--- + +### Ticket #61 : [Doc] Rédaction CGU/Privacy génériques v1 +**Estimation** : 8h +**Labels** : `documentation`, `juridique`, `rgpd` + +**Description** : +Rédiger les documents légaux génériques (CGU et Politique de confidentialité) conformes RGPD. + +**Tâches** : +- [ ] Rédaction CGU génériques (avec aide juriste) +- [ ] Rédaction Privacy génériques (RGPD compliant) +- [ ] Conversion en PDF +- [ ] Placement dans `/documents/legaux/` +- [ ] Calcul hash SHA-256 + +**⚠️ IMPORTANT** : Nécessite l'intervention d'un juriste. + +--- + +## 📊 Résumé final + +**Total** : 61 tickets +**Estimation** : ~173h de développement + +### Par priorité +- **P0 (Bloquant BDD)** : 7 tickets (~5h) +- **P1 (Bloquant Config)** : 7 tickets (~22h) +- **P2 (Backend)** : 18 tickets (~50h) +- **P3 (Frontend)** : 17 tickets (~52h) ← +1 ticket logs admin +- **P4 (Tests/Doc)** : 4 tickets (~24h) +- **Critiques** : 6 tickets (~13h) ← -2 email, +1 logs, +1 CDC +- **Juridique** : 1 ticket (~8h) + +### Par domaine +- **BDD** : 7 tickets +- **Backend** : 23 tickets ← +1 logs +- **Frontend** : 17 tickets ← +1 logs admin +- **Tests** : 3 tickets +- **Documentation** : 5 tickets ← +1 amendement CDC +- **Infra** : 2 tickets +- **Juridique** : 1 ticket + +### Modifications par rapport à la version initiale +- ❌ **Supprimé** : Tickets "Renvoyer email validation" (backend + frontend) - Pas prioritaire +- ✅ **Ajouté** : Ticket #55 "Service Logging Winston" - Monitoring essentiel +- ✅ **Ajouté** : Ticket #56 "Écran Logs Admin" - Optionnel Phase 1.1 +- ✅ **Ajouté** : Ticket #60 "Amendement CDC v1.4 - Suppression SMS" - Simplification + +--- + +**Dernière mise à jour** : 25 Novembre 2025 +**Version** : 1.0 +**Statut** : ✅ Prêt pour création dans Gitea + diff --git a/docs/24_DECISIONS-PROJET.md b/docs/24_DECISIONS-PROJET.md new file mode 100644 index 0000000..21b6f33 --- /dev/null +++ b/docs/24_DECISIONS-PROJET.md @@ -0,0 +1,568 @@ +# 📋 Décisions Projet - P'titsPas + +**Version** : 1.0 +**Date** : 25 Novembre 2025 +**Auteur** : Équipe PtitsPas + +--- + +## 🎯 Objectif de ce document + +Ce document trace toutes les **décisions importantes** prises lors de la conception du projet P'titsPas. Il sert de référence pour comprendre les choix techniques et fonctionnels. + +--- + +## 📊 Décisions Architecture + +### 1. Mono-repo vs Multi-repo + +**Décision** : ✅ **Mono-repo** + +**Justification** : +- Code cohérent dans un seul dépôt Git +- Commits atomiques (frontend + backend + BDD en même temps) +- CI/CD simplifié (un seul webhook) +- Facilite le travail collaboratif + +**Structure** : +``` +ptitspas-app/ +├── frontend/ # Application Flutter +├── backend/ # API NestJS +├── database/ # Migrations SQL/Prisma +├── api-contracts/ # Contrats OpenAPI + Prisma +├── docs/ # Documentation +└── docker-compose.yml # Orchestration +``` + +--- + +### 2. Déploiement On-Premise + +**Décision** : ✅ **Application on-premise avec configuration dynamique** + +**Justification** : +- Chaque collectivité a son infrastructure (SMTP, domaines, ports) +- Autonomie totale (pas de dépendance à notre infra) +- Conformité aux politiques IT locales +- Sécurité (données restent dans le SI de la collectivité) + +**Solution technique** : +- Table `configuration` en BDD (clé/valeur) +- Setup Wizard à la première connexion +- Configuration SMTP dynamique +- Personnalisation (nom app, logo, URL) + +**Référence** : [21_CONFIGURATION-SYSTEME.md](./21_CONFIGURATION-SYSTEME.md) + +--- + +### 3. Gestion des fichiers uploadés + +**Décision** : ✅ **Système de fichiers local avec volume Docker** + +**Justification** : +- Simplicité (pas de S3/MinIO pour on-premise) +- Performance (accès direct au disque) +- Coût (pas de service externe) + +**Solution technique** : +- Stockage dans `/uploads/photos/` +- Volume Docker persistant +- Service Upload avec Multer +- Validation taille (depuis config) + +**Alternatives rejetées** : +- ❌ Base de données (BYTEA) : Mauvaise performance +- ❌ S3/MinIO : Overkill pour on-premise + +--- + +### 4. Documents légaux (CGU/Privacy) + +**Décision** : ✅ **Versioning automatique avec traçabilité RGPD** + +**Justification** : +- Conformité juridique (preuve d'acceptation) +- Flexibilité (chaque collectivité adapte ses CGU) +- Traçabilité (hash SHA-256, IP, User-Agent) +- Sécurité juridique (pas de retour en arrière) + +**Solution technique** : +- Table `documents_legaux` (versioning auto-incrémenté) +- Table `acceptations_documents` (traçabilité RGPD) +- Upload PDF par l'admin +- Activation manuelle après prévisualisation +- Documents génériques v1 fournis par défaut + +**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md) + +--- + +## 🔧 Décisions Fonctionnelles + +### 5. Workflow inscription sans mot de passe + +**Décision** : ✅ **Pas de mot de passe lors de l'inscription, lien email après validation** + +**Justification** : +- Simplifie l'inscription (moins de friction) +- Adapté aux parents séparés/divorcés (communication difficile) +- Sécurité (token UUID avec expiration) +- Validation gestionnaire avant activation + +**Workflow** : +1. Parent/AM s'inscrit (sans MDP) +2. Gestionnaire valide +3. Email envoyé avec lien création MDP (valable 7 jours) +4. Utilisateur crée son MDP +5. Compte activé + +**Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md) + +--- + +### 6. Genre enfant obligatoire (H/F) + +**Décision** : ✅ **Genre obligatoire (H/F uniquement)** + +**Justification** : +- Contexte métier : Enfants à garder (pas de genre neutre) +- Conformité CDC +- Simplicité + +**Implémentation** : +- Champ `genre` ENUM('H', 'F') NOT NULL dans table `enfants` + +--- + +### 7. Suppression champs téléphone + +**Décision** : ✅ **Un seul champ "Téléphone" (mobile privilégié)** + +**Justification** : +- Simplification (plus besoin de "Mobile" et "Téléphone fixe") +- Réalité terrain : Tout le monde a un mobile +- Note dans l'interface : "(mobile privilégié)" + +**Implémentation** : +- Supprimer `mobile` et `telephone_fixe` +- Garder uniquement `telephone` + +--- + +### 8. NIR obligatoire pour Assistantes Maternelles + +**Décision** : ✅ **NIR (Numéro de Sécurité sociale) obligatoire** + +**Justification** : +- Conformité CDC +- Nécessaire pour génération automatique du contrat +- Stockage sécurisé (chiffrement recommandé) + +**Implémentation** : +- Champ `nir_chiffre` VARCHAR(15) NOT NULL dans `assistantes_maternelles` +- Validation : 15 chiffres exactement +- Affichage masqué dans l'interface (XXX XX XX XX XXX 123) + +--- + +### 9. Suppression notifications SMS + +**Décision** : ✅ **Supprimer les notifications SMS, garder uniquement Email** + +**Justification** : +- Pas de plus-value identifiée +- Coût supplémentaire (service SMS) +- Complexité (intégration Twilio/OVH) +- Email suffit largement + +**Action** : +- Amender le CDC v1.4 +- Remplacer "Email OU SMS" par "Email" +- Supprimer toute mention de SMS + +**Référence** : Ticket #57 + +--- + +## 🚫 Décisions Phase 2 (reportées) + +### 10. Gestion erreurs SMTP (renvoyer email) + +**Décision** : ⏸️ **Phase 2 ou cas par cas** + +**Justification** : +- Pas prioritaire pour MVP +- Downtime SMTP rare (< 24h) +- Recréation dossier acceptable en phase de test +- Peut être ajouté plus tard si besoin terrain + +--- + +### 11. Sauvegarde & Restauration automatique + +**Décision** : ⏸️ **Phase finale (quand tout fonctionne)** + +**Justification** : +- Pas bloquant pour développement +- Nécessite application stable +- Script `pg_dump` simple à ajouter +- Documentation pour admins sys + +**À faire** : +- Script `backup.sh` avec cron +- Guide sauvegarde/restauration +- Test restauration + +--- + +### 12. RGPD avancé (droit à l'oubli, export données) + +**Décision** : ⏸️ **Phase 2** + +**Justification** : +- Conformité de base assurée (consentement, traçabilité) +- Droit à l'oubli : Peu de demandes en phase test +- Export données : Peut être fait manuellement en phase test + +**À faire** : +- API suppression compte (soft delete) +- API export données personnelles (JSON) +- Anonymisation comptes inactifs + +--- + +### 13. Statistiques dashboard + +**Décision** : ⏸️ **Phase 2 (besoin d'utilisateurs d'abord)** + +**Justification** : +- Pas de données = pas de statistiques +- Fonctionnalité importante pour vente +- Nécessite application en production + +**À faire** : +- API statistiques (comptes, enfants, AM, validations) +- Widgets dashboard (graphiques) +- Export rapports + +--- + +### 14. Migration de données + +**Décision** : ❌ **Pas de migration prévue** + +**Justification** : +- Pas de système existant à migrer +- Application nouvelle +- Import CSV peut être ajouté cas par cas si besoin + +--- + +### 15. Documentation utilisateur + +**Décision** : ⏸️ **Phase 2 (formation présentiel prioritaire)** + +**Justification** : +- Premiers utilisateurs : Formation directe par l'équipe +- Documentation nécessaire pour scalabilité +- Vidéos tutoriels utiles mais pas bloquant + +**À faire** : +- Guide utilisateur gestionnaire +- Guide utilisateur parent/AM +- FAQ +- Vidéos tutoriels + +--- + +## 🔐 Décisions Sécurité + +### 16. Chiffrement mots de passe configuration + +**Décision** : ✅ **AES-256-CBC pour mots de passe SMTP** + +**Justification** : +- Sécurité (mots de passe SMTP en clair = risque) +- Conformité sécurité +- Déchiffrement uniquement en mémoire + +**Implémentation** : +- Type `encrypted` dans table `configuration` +- Clé de chiffrement dans `.env` (32 caractères) +- Service ConfigService gère chiffrement/déchiffrement + +--- + +### 17. Hash mots de passe utilisateurs + +**Décision** : ✅ **Bcrypt avec 12 rounds** + +**Justification** : +- Standard industrie +- Résistant aux attaques brute-force +- Configurable (rounds dans config) + +--- + +### 18. Tokens création mot de passe + +**Décision** : ✅ **UUID avec expiration 7 jours** + +**Justification** : +- Sécurité (UUID impossible à deviner) +- Expiration (limite fenêtre d'attaque) +- Durée configurable (dans table `configuration`) + +--- + +### 19. JWT sessions + +**Décision** : ✅ **JWT avec expiration 24h** + +**Justification** : +- Stateless (pas de session en BDD) +- Performance (pas de lookup BDD à chaque requête) +- Durée configurable + +--- + +## 📊 Décisions Base de Données + +### 20. PostgreSQL vs autres SGBD + +**Décision** : ✅ **PostgreSQL 17** + +**Justification** : +- Open-source +- Robuste et performant +- Support JSON (flexibilité) +- Support UUID natif +- Communauté active + +--- + +### 21. ORM : Prisma vs TypeORM + +**Décision** : ✅ **Prisma** (mais TypeORM déjà en place) + +**Justification Prisma** : +- Type-safety +- Migrations claires +- Génération client automatique + +**Note** : Le projet YNOV utilise TypeORM. À évaluer si migration vers Prisma nécessaire. + +--- + +### 22. Migrations versionnées + +**Décision** : ✅ **Migrations SQL versionnées** + +**Justification** : +- Traçabilité des changements schéma +- Rollback possible +- Déploiement automatisé + +**Implémentation** : +- Fichiers `XX_nom_migration.sql` +- Exécution via `prisma migrate deploy` +- User `app_admin` pour DDL +- User `app_user` pour DML (runtime) + +--- + +## 🧪 Décisions Tests + +### 23. Stratégie de tests + +**Décision** : ✅ **Tests unitaires + intégration + E2E** + +**Justification** : +- Qualité code +- Confiance déploiement +- Détection régression + +**Couverture cible** : +- Tests unitaires : > 80% +- Tests intégration : Workflows complets +- Tests E2E : Parcours utilisateurs critiques + +--- + +## 📝 Décisions Documentation + +### 24. Structure documentation + +**Décision** : ✅ **Documentation centralisée dans `/docs/` avec numérotation** + +**Justification** : +- Lisibilité (ordre logique) +- Maintenance (tout au même endroit) +- Versioning (Git) + +**Structure** : +``` +docs/ +├── 00_INDEX.md +├── 01_CAHIER-DES-CHARGES.md +├── 02_ARCHITECTURE.md +├── 03_DEPLOYMENT.md +├── 10_DATABASE.md +├── 11_API.md +├── 20_WORKFLOW-CREATION-COMPTE.md +├── 21_CONFIGURATION-SYSTEME.md +├── 22_DOCUMENTS-LEGAUX.md +├── 23_LISTE-TICKETS.md +├── 24_DECISIONS-PROJET.md (ce document) +├── 90_AUDIT.md +└── test-data/ +``` + +--- + +## 🔄 Décisions Workflow + +### 25. Gitea + Webhook + Déploiement automatique + +**Décision** : ✅ **Déploiement automatique sur push master** + +**Justification** : +- Productivité (pas de déploiement manuel) +- Fiabilité (script testé) +- Rapidité (feedback immédiat) + +**Workflow** : +1. Push sur `master` +2. Webhook Gitea déclenché +3. Script `deploy-ptitspas.sh` exécuté +4. Git pull + Migrations + Docker rebuild + Restart + +--- + +### 26. Branches Git + +**Décision** : ✅ **Stratégie simple : master + feature branches** + +**Justification** : +- Simplicité (petite équipe) +- Flexibilité + +**Branches** : +- `master` : Production +- `archive/*` : Archives (ex: maquette initiale) +- `migration/*` : Migrations (ex: intégration YNOV) +- `feature/*` : Nouvelles fonctionnalités + +--- + +## 🎫 Décisions Ticketing + +### 27. Taille des tickets + +**Décision** : ✅ **Tickets relativement petits (2-6h)** + +**Justification** : +- Granularité (suivi précis) +- Motivation (tickets terminables rapidement) +- Revue de code facilitée + +--- + +### 28. Organisation tickets + +**Décision** : ✅ **1 ticket = 1 fonctionnalité complète (Front + Back + BDD si nécessaire)** + +**Justification** : +- Cohérence (toute la feature développée ensemble) +- Testable (end-to-end immédiatement) +- Atomique (livraison de valeur fonctionnelle) + +**Alternative rejetée** : +- ❌ 1 ticket Front + 1 ticket Back + 1 ticket BDD = Dépendances complexes + +--- + +## 🚀 Décisions Déploiement + +### 29. Docker Compose vs Kubernetes + +**Décision** : ✅ **Docker Compose** + +**Justification** : +- Simplicité (on-premise petite/moyenne collectivité) +- Pas de besoin de scalabilité horizontale +- Maintenance facile + +**Alternative rejetée** : +- ❌ Kubernetes : Overkill pour on-premise mono-instance + +--- + +### 30. Traefik comme reverse proxy + +**Décision** : ✅ **Traefik** + +**Justification** : +- Configuration automatique (labels Docker) +- SSL Let's Encrypt automatique +- Dashboard intégré + +--- + +## 📊 Logs & Monitoring + +### 31. Système de logs + +**Décision** : ✅ **Winston avec rotation quotidienne** + +**Justification** : +- Standard NestJS +- Rotation automatique (pas de disque plein) +- Niveaux de log (info, warn, error) +- Transports multiples (console + fichier) + +**Logs à capturer** : +- Logs applicatifs (info, warn, error) +- Logs emails (succès/échec SMTP) +- Logs connexions (qui se connecte quand) +- Logs validations (qui valide quoi) + +--- + +## 📋 Résumé des décisions critiques + +| # | Décision | Statut | Impact | +|---|----------|--------|--------| +| 1 | Mono-repo | ✅ Phase 1 | Architecture | +| 2 | On-premise avec config dynamique | ✅ Phase 1 | Déploiement | +| 3 | Système de fichiers pour uploads | ✅ Phase 1 | Stockage | +| 4 | Versioning documents légaux | ✅ Phase 1 | RGPD | +| 5 | Inscription sans MDP | ✅ Phase 1 | UX | +| 6 | Genre enfant obligatoire (H/F) | ✅ Phase 1 | Métier | +| 7 | Un seul champ téléphone | ✅ Phase 1 | Simplification | +| 8 | NIR obligatoire AM | ✅ Phase 1 | Conformité | +| 9 | Suppression SMS | ✅ Phase 1 | Simplification | +| 10 | Renvoyer email | ⏸️ Phase 2 | Nice-to-have | +| 11 | Sauvegarde auto | ⏸️ Phase finale | Ops | +| 12 | RGPD avancé | ⏸️ Phase 2 | Conformité | +| 13 | Statistiques | ⏸️ Phase 2 | Business | +| 14 | Migration données | ❌ Rejeté | N/A | +| 15 | Doc utilisateur | ⏸️ Phase 2 | Formation | +| 31 | Logs Winston | ✅ Phase 1 | Monitoring | + +--- + +## 🔄 Historique des modifications + +| Date | Version | Modifications | +|------|---------|---------------| +| 25/11/2025 | 1.0 | Création du document - Toutes les décisions initiales | + +--- + +**Dernière mise à jour** : 25 Novembre 2025 +**Version** : 1.0 +**Statut** : ✅ Document validé + diff --git a/docs/25_PHASE-2-BACKLOG.md b/docs/25_PHASE-2-BACKLOG.md new file mode 100644 index 0000000..4ff8a6e --- /dev/null +++ b/docs/25_PHASE-2-BACKLOG.md @@ -0,0 +1,433 @@ +# 📋 Backlog Phase 2 - P'titsPas + +**Version** : 1.0 +**Date** : 25 Novembre 2025 +**Auteur** : Équipe PtitsPas + +--- + +## 🎯 Objectif de ce document + +Ce document liste toutes les **fonctionnalités reportées en Phase 2**. Ces fonctionnalités ne sont pas bloquantes pour le MVP (Phase 1), mais apportent de la valeur ajoutée pour la production intensive. + +--- + +## 📊 Vue d'ensemble + +| Catégorie | Nombre de tickets | Estimation | +|-----------|-------------------|------------| +| **RGPD avancé** | 3 tickets | ~8h | +| **Monitoring avancé** | 2 tickets | ~6h | +| **Statistiques & Reporting** | 4 tickets | ~16h | +| **Sauvegarde & Restauration** | 2 tickets | ~6h | +| **Documentation utilisateur** | 3 tickets | ~12h | +| **Améliorations UX** | 3 tickets | ~10h | +| **TOTAL** | **17 tickets** | **~58h** | + +--- + +## 🔒 RGPD avancé + +### Ticket P2-01 : [Backend] API Suppression compte (soft delete) +**Estimation** : 3h +**Labels** : `backend`, `phase-2`, `rgpd` + +**Description** : +Implémenter le droit à l'oubli (RGPD Article 17) avec suppression logique des comptes. + +**Tâches** : +- [ ] Endpoint `DELETE /api/v1/users/:id` (soft delete) +- [ ] Ajout champ `supprime_le` TIMESTAMPTZ dans `utilisateurs` +- [ ] Anonymisation données personnelles (email, téléphone, adresse) +- [ ] Conservation données légales (acceptations CGU) +- [ ] Guards (super_admin only) +- [ ] Tests unitaires + +**Justification report Phase 2** : +- Peu de demandes en phase test +- Suppression manuelle possible en attendant + +--- + +### Ticket P2-02 : [Backend] API Export données personnelles (RGPD) +**Estimation** : 3h +**Labels** : `backend`, `phase-2`, `rgpd` + +**Description** : +Implémenter le droit à la portabilité (RGPD Article 20) avec export JSON des données personnelles. + +**Tâches** : +- [ ] Endpoint `GET /api/v1/users/:id/export` (JSON) +- [ ] Export données utilisateur + enfants + acceptations +- [ ] Format JSON structuré +- [ ] Guards (utilisateur lui-même ou admin) +- [ ] Tests unitaires + +**Justification report Phase 2** : +- Export manuel SQL possible en phase test +- Peu de demandes attendues + +--- + +### Ticket P2-03 : [Backend] Cron anonymisation comptes inactifs +**Estimation** : 2h +**Labels** : `backend`, `phase-2`, `rgpd`, `cron` + +**Description** : +Anonymiser automatiquement les comptes inactifs après X mois (configurable). + +**Tâches** : +- [ ] Cron job quotidien +- [ ] Détection comptes inactifs (dernière connexion > X mois) +- [ ] Anonymisation automatique +- [ ] Notification admin +- [ ] Configuration durée inactivité (table `configuration`) +- [ ] Tests + +**Justification report Phase 2** : +- Pas de comptes inactifs en phase test +- Peut être fait manuellement + +--- + +## 📊 Monitoring avancé + +### Ticket P2-04 : [Backend] API Métriques système +**Estimation** : 3h +**Labels** : `backend`, `phase-2`, `monitoring` + +**Description** : +Exposer des métriques système (CPU, RAM, disque, BDD) pour monitoring. + +**Tâches** : +- [ ] Endpoint `GET /api/v1/metrics` (super_admin only) +- [ ] Métriques système (CPU, RAM, disque) +- [ ] Métriques BDD (connexions, taille, requêtes lentes) +- [ ] Métriques application (requêtes/s, temps réponse) +- [ ] Format Prometheus (optionnel) +- [ ] Tests + +**Justification report Phase 2** : +- Pas critique pour MVP +- Logs suffisent pour debugging initial + +--- + +### Ticket P2-05 : [Frontend] Dashboard Monitoring +**Estimation** : 3h +**Labels** : `frontend`, `phase-2`, `monitoring`, `admin` + +**Description** : +Créer un dashboard de monitoring pour le super admin. + +**Tâches** : +- [ ] Page `/admin/monitoring` (super_admin only) +- [ ] Graphiques temps réel (CPU, RAM, disque) +- [ ] Alertes (seuils configurables) +- [ ] Historique métriques (7 jours) + +**Justification report Phase 2** : +- Pas critique pour MVP +- Monitoring serveur possible via outils système + +--- + +## 📈 Statistiques & Reporting + +### Ticket P2-06 : [Backend] API Statistiques dashboard +**Estimation** : 4h +**Labels** : `backend`, `phase-2`, `statistiques` + +**Description** : +Créer les endpoints pour récupérer les statistiques de l'application. + +**Tâches** : +- [ ] Endpoint `GET /api/v1/stats/overview` +- [ ] Statistiques globales (comptes, enfants, AM, validations) +- [ ] Statistiques temporelles (inscriptions par mois, validations par semaine) +- [ ] Statistiques géographiques (par ville) +- [ ] Cache (rafraîchissement toutes les heures) +- [ ] Tests + +**Justification report Phase 2** : +- Pas de données = pas de statistiques +- Nécessite application en production + +--- + +### Ticket P2-07 : [Frontend] Widgets statistiques dashboard +**Estimation** : 4h +**Labels** : `frontend`, `phase-2`, `statistiques`, `gestionnaire` + +**Description** : +Afficher les statistiques dans le dashboard gestionnaire. + +**Tâches** : +- [ ] Widget "Comptes en attente" (nombre) +- [ ] Widget "Comptes validés ce mois" (graphique) +- [ ] Widget "Enfants inscrits" (nombre) +- [ ] Widget "AM disponibles" (nombre) +- [ ] Graphique évolution inscriptions (6 derniers mois) + +**Justification report Phase 2** : +- Besoin d'utilisateurs réels pour avoir des données +- Fonctionnalité importante pour vente, mais pas bloquante pour MVP + +--- + +### Ticket P2-08 : [Backend] API Export rapports (CSV/PDF) +**Estimation** : 4h +**Labels** : `backend`, `phase-2`, `reporting` + +**Description** : +Permettre l'export de rapports pour les gestionnaires. + +**Tâches** : +- [ ] Endpoint `GET /api/v1/reports/users` (CSV) +- [ ] Endpoint `GET /api/v1/reports/validations` (CSV) +- [ ] Endpoint `GET /api/v1/reports/summary` (PDF) +- [ ] Génération PDF (librairie PDFKit) +- [ ] Filtres (date, statut, rôle) +- [ ] Tests + +**Justification report Phase 2** : +- Export manuel SQL possible en phase test +- Nécessite données réelles + +--- + +### Ticket P2-09 : [Frontend] Écran Rapports +**Estimation** : 4h +**Labels** : `frontend`, `phase-2`, `reporting`, `gestionnaire` + +**Description** : +Créer un écran de génération de rapports pour les gestionnaires. + +**Tâches** : +- [ ] Page `/admin/rapports` (gestionnaire/admin) +- [ ] Sélection type de rapport (utilisateurs, validations, synthèse) +- [ ] Filtres (date, statut, rôle) +- [ ] Prévisualisation +- [ ] Téléchargement (CSV/PDF) + +**Justification report Phase 2** : +- Pas prioritaire pour MVP +- Nécessite données réelles + +--- + +## 💾 Sauvegarde & Restauration + +### Ticket P2-10 : [Infra] Script backup PostgreSQL automatique +**Estimation** : 3h +**Labels** : `infra`, `phase-2`, `backup` + +**Description** : +Créer un script de sauvegarde automatique de la base de données. + +**Tâches** : +- [ ] Script `backup.sh` avec `pg_dump` +- [ ] Compression (gzip) +- [ ] Rotation (garder 30 derniers jours) +- [ ] Cron job quotidien (3h du matin) +- [ ] Notification email en cas d'échec +- [ ] Tests restauration + +**Justification report Phase 2** : +- Pas bloquant pour développement +- Backup manuel possible en phase test +- À faire avant mise en production + +--- + +### Ticket P2-11 : [Doc] Guide sauvegarde & restauration +**Estimation** : 3h +**Labels** : `documentation`, `phase-2`, `backup` + +**Description** : +Documenter les procédures de sauvegarde et restauration pour les admins sys. + +**Tâches** : +- [ ] Procédure sauvegarde manuelle +- [ ] Procédure restauration +- [ ] Configuration cron +- [ ] Troubleshooting +- [ ] Exemples de commandes +- [ ] Checklist pré-restauration + +**Justification report Phase 2** : +- Documentation technique, pas bloquante pour MVP +- À faire avant mise en production + +--- + +## 📚 Documentation utilisateur + +### Ticket P2-12 : [Doc] Guide utilisateur Gestionnaire +**Estimation** : 4h +**Labels** : `documentation`, `phase-2`, `formation` + +**Description** : +Rédiger le guide utilisateur pour les gestionnaires. + +**Tâches** : +- [ ] Connexion et première utilisation +- [ ] Consultation des demandes +- [ ] Validation/Refus de comptes +- [ ] Gestion des paramètres +- [ ] FAQ gestionnaire +- [ ] Captures d'écran + +**Justification report Phase 2** : +- Formation présentiel prioritaire pour premiers utilisateurs +- Documentation nécessaire pour scalabilité + +--- + +### Ticket P2-13 : [Doc] Guide utilisateur Parent/AM +**Estimation** : 4h +**Labels** : `documentation`, `phase-2`, `formation` + +**Description** : +Rédiger le guide utilisateur pour les parents et assistantes maternelles. + +**Tâches** : +- [ ] Inscription étape par étape +- [ ] Création du mot de passe +- [ ] Connexion +- [ ] Utilisation de l'application +- [ ] FAQ parent/AM +- [ ] Captures d'écran + +**Justification report Phase 2** : +- Formation présentiel prioritaire +- Documentation nécessaire pour scalabilité + +--- + +### Ticket P2-14 : [Doc] Vidéos tutoriels +**Estimation** : 4h +**Labels** : `documentation`, `phase-2`, `formation`, `video` + +**Description** : +Créer des vidéos tutoriels pour les utilisateurs. + +**Tâches** : +- [ ] Vidéo "Inscription parent" (3-5 min) +- [ ] Vidéo "Inscription AM" (3-5 min) +- [ ] Vidéo "Validation comptes gestionnaire" (3-5 min) +- [ ] Vidéo "Configuration initiale admin" (5-7 min) +- [ ] Hébergement (YouTube privé ou serveur) + +**Justification report Phase 2** : +- Pas bloquant pour MVP +- Utile pour scalabilité et autonomie utilisateurs + +--- + +## 🎨 Améliorations UX + +### Ticket P2-15 : [Frontend] Mode sombre +**Estimation** : 3h +**Labels** : `frontend`, `phase-2`, `ux` + +**Description** : +Ajouter un mode sombre à l'application. + +**Tâches** : +- [ ] Thème sombre (couleurs, contrastes) +- [ ] Toggle mode clair/sombre +- [ ] Sauvegarde préférence utilisateur +- [ ] Adaptation tous les écrans + +**Justification report Phase 2** : +- Nice-to-have, pas bloquant +- Améliore confort utilisateur + +--- + +### Ticket P2-16 : [Frontend] Notifications push (optionnel) +**Estimation** : 4h +**Labels** : `frontend`, `phase-2`, `ux`, `notifications` + +**Description** : +Ajouter des notifications push pour les événements importants. + +**Tâches** : +- [ ] Service Worker (PWA) +- [ ] Notification "Compte validé" +- [ ] Notification "Nouveau message" (si messagerie) +- [ ] Gestion permissions +- [ ] Paramètres notifications + +**Justification report Phase 2** : +- Nice-to-have, pas bloquant +- Nécessite PWA ou app mobile + +--- + +### Ticket P2-17 : [Frontend] Accessibilité (WCAG 2.1) +**Estimation** : 3h +**Labels** : `frontend`, `phase-2`, `ux`, `a11y` + +**Description** : +Améliorer l'accessibilité de l'application (conformité WCAG 2.1). + +**Tâches** : +- [ ] Audit accessibilité (Lighthouse, axe) +- [ ] Correction contrastes +- [ ] Attributs ARIA +- [ ] Navigation clavier +- [ ] Lecteur d'écran (test) + +**Justification report Phase 2** : +- Amélioration continue +- Pas bloquant pour MVP +- Important pour collectivités (obligation légale) + +--- + +## 📋 Priorisation Phase 2 + +### Priorité Haute (à faire en premier) +1. **Sauvegarde & Restauration** (Tickets P2-10, P2-11) → Avant mise en production +2. **Statistiques** (Tickets P2-06, P2-07) → Argument de vente +3. **Documentation utilisateur** (Tickets P2-12, P2-13) → Scalabilité + +### Priorité Moyenne +4. **RGPD avancé** (Tickets P2-01, P2-02, P2-03) → Conformité +5. **Reporting** (Tickets P2-08, P2-09) → Utile pour gestionnaires + +### Priorité Basse +6. **Monitoring avancé** (Tickets P2-04, P2-05) → Nice-to-have +7. **Améliorations UX** (Tickets P2-15, P2-16, P2-17) → Confort + +--- + +## 🚀 Critères de passage en Phase 2 + +La Phase 2 peut commencer quand : +- ✅ Phase 1 terminée (61 tickets) +- ✅ Application déployée en production (au moins 1 collectivité) +- ✅ Utilisateurs réels (au moins 10 comptes validés) +- ✅ Feedback terrain collecté +- ✅ Bugs critiques corrigés + +--- + +## 📊 Estimation globale Phase 2 + +**Total** : 17 tickets +**Estimation** : ~58h de développement + +**Planning suggéré** : +- Sprint 1 (2 semaines) : Sauvegarde + Statistiques (26h) +- Sprint 2 (2 semaines) : Documentation + RGPD (24h) +- Sprint 3 (1 semaine) : Reporting + Améliorations UX (8h) + +--- + +**Dernière mise à jour** : 25 Novembre 2025 +**Version** : 1.0 +**Statut** : ✅ Backlog Phase 2 défini + diff --git a/docs/90_AUDIT.md b/docs/90_AUDIT.md new file mode 100644 index 0000000..3b06086 --- /dev/null +++ b/docs/90_AUDIT.md @@ -0,0 +1,98 @@ +# 🕵️ Rapport d'Audit Technique & Fonctionnel - Projet P'titsPas (Ynov) + +**Date** : 23 Novembre 2025 +**Auditeur** : Assistant IA (Deploy Infra V2) +**Objet** : État des lieux avant reprise des développements pour finalisation. + +--- + +## 🚨 Synthèse Exécutive + +Le projet dispose d'une **base technique saine** (NestJS, PostgreSQL, Docker) et d'une structure de code propre. Cependant, **plusieurs fonctionnalités critiques du Cahier des Charges (CDC) sont manquantes ou incomplètes**, rendant l'application inutilisable en l'état pour le workflow principal (Inscription -> Validation -> Utilisation). + +**Score d'avancement estimé : 25%** +(Backend: 60%, Frontend: 10% [Coquille vide], Infra: 80%) + +--- + +## 1. 🏗️ Infrastructure & Docker + +### ✅ Points Positifs +* **Stack propre** : Architecture micro-services (Backend, Frontend, Database, PgAdmin). +* **Configuration Traefik** : Les labels et le routing (`ynov.ptits-pas.fr`) sont corrects. +* **Images optimisées** : Utilisation de builds multi-stage. + +### ⚠️ Problèmes & Correctifs +1. **Processus Zombies (Critique)** : + * **Constat** : Plus de 9000 processus zombies détectés. + * **Cause** : Le conteneur Backend lance `node` en PID 1 sans gestionnaire de signaux (`init`). + * **Action** : Ajouter `tini` ou `dumb-init` dans le `Dockerfile` Backend. +2. **Base de Données (Seed)** : + * **Constat** : Le script d'initialisation (`01_init.sql`) insère un administrateur avec le mot de passe `"admin123"` **en clair**. + * **Impact** : Impossible de se connecter (le Backend attend un hash Bcrypt). + * **Action** : Remplacer l'insertion SQL par un vrai hash ou un script de seed NestJS. + +--- + +## 2. 🔙 Backend (NestJS) + +### ✅ Points Positifs +* **Architecture** : Modulaire (Auth, User, Parents, Enfants...). +* **Qualité Code** : Typage fort, DTOs validés (`class-validator`), Entités TypeORM claires. + +### ❌ Écarts Fonctionnels Majeurs +| Fonctionnalité | État | Problème Identifié | +| :--- | :---: | :--- | +| **Envoi d'E-mails** | 🔴 **ABSENT** | Aucune librairie mail (Nodemailer) installée. Aucune logique d'envoi. | +| **Inscription** | 🟠 **Incomplet** | L'utilisateur est créé, mais sa fiche métier (`Parents` ou `AssistanteMaternelle`) **n'est pas générée**. Elle n'est créée qu'à la validation, ce qui empêche le remplissage du profil en amont. | +| **Validation Compte** | 🟠 **Partiel** | Le changement de statut fonctionne, mais **aucune notification mail** n'est envoyée (requis par CDC). | +| **Gestionnaire** | ✅ **OK** | La route de création par SuperAdmin existe et est sécurisée. | + +--- + +## 3. 📱 Frontend (Flutter) + +### ✅ Points Positifs +* Structure du projet cohérente. +* Authentification (Login) implémentée. + +### ❌ Écarts Fonctionnels Majeurs +| Écran / Fonction | État | Problème Identifié | +| :--- | :---: | :--- | +| **Création Gestionnaire** | 🔴 **VIDE** | Le fichier `gestionnaires_create.dart` contient uniquement un texte placeholder. **Aucun formulaire.** | +| **Dashboard Admin** | 🔴 **FAKE** | L'écran existe mais affiche des données simulées "en dur" (Mock). **Non connecté à l'API.** Impossible de valider les comptes. | +| **Dashboard Parent** | 🔴 **FAKE** | L'écran existe, architecture propre (Service/Controller), mais toutes les données (Enfants, Contrats) sont mockées (`TODO: API Call`). | +| **Dashboard AssMat** | 🔴 **INEXISTANT** | Aucun écran trouvé pour l'accueil des Assistantes Maternelles. | +| **Contact Support** | 🔴 **Fake** | Bouton présent mais code vide (`// TODO`). | +| **Signalement Bug** | 🔴 **Fake** | Idem, code non implémenté. | + +--- + +## 4. 📝 Plan d'Action Prioritaire (Roadmap de Réparation) + +Pour rendre l'application fonctionnelle selon le "Workflow attendu", voici les étapes techniques à réaliser dans l'ordre : + +### Étape 1 : Réparation Socle (Infra & Data) +1. **Fixer le Dockerfile Backend** (Tini pour les zombies). +2. **Fixer le mot de passe Admin** : Créer un script pour mettre à jour le hash du mot de passe `admin123` en base de données. + +### Étape 2 : Implémentation Backend (Mail & Workflow) +3. **Installer `nodemailer`** et configurer le service SMTP (via le serveur mail Host). +4. **Corriger `register`** : Créer l'entité `Parent`/`AssMat` dès l'inscription (vide mais existante). +5. **Corriger `validateUser`** : Ajouter l'envoi de mail de notification (Validé/Refusé). + +### Étape 3 : Implémentation Frontend (Écrans Manquants) +6. **Développer l'écran "Créer Gestionnaire"** : Formulaire (Nom, Prénom, Email, MDP) + Appel API. +7. **Vérifier/Finir le Dashboard Gestionnaire** : Liste des utilisateurs "En attente" + Boutons Valider/Refuser. + +### Étape 4 : Connexion (Workflow Complet) +8. Tester le cycle complet : + * Login SuperAdmin -> Création Gestionnaire. + * Login Gestionnaire -> Vue vide. + * Inscription Parent -> Apparition dans la liste Gestionnaire. + * Validation par Gestionnaire -> Mail reçu par Parent. + * Login Parent -> Accès autorisé. + +--- +*Ce document sert de référence pour la suite des travaux.* + diff --git a/docs/99_REGLES-CODAGE.md b/docs/99_REGLES-CODAGE.md new file mode 100644 index 0000000..04e10af --- /dev/null +++ b/docs/99_REGLES-CODAGE.md @@ -0,0 +1,344 @@ +# 📐 Règles de Codage - Projet P'titsPas + +**Version** : 1.0 +**Date** : 1er Décembre 2025 +**Statut** : ✅ Actif + +--- + +## 🌍 Langue du Code + +### Principe Général +**Tout le code doit être écrit en FRANÇAIS**, sauf les termes techniques qui restent en **ANGLAIS**. + +--- + +## ✅ Ce qui doit être en FRANÇAIS + +### 1. Noms de variables +```typescript +// ✅ BON +const utilisateurConnecte = await this.trouverUtilisateur(id); +const enfantsEnregistres = []; +const tokenCreationMotDePasse = crypto.randomUUID(); + +// ❌ MAUVAIS +const loggedUser = await this.findUser(id); +const savedChildren = []; +const passwordCreationToken = crypto.randomUUID(); +``` + +### 2. Noms de fonctions/méthodes +```typescript +// ✅ BON +async inscrireParentComplet(dto: DtoInscriptionParentComplet) { } +async creerGestionnaire(dto: DtoCreationGestionnaire) { } +async validerCompte(idUtilisateur: string) { } + +// ❌ MAUVAIS +async registerParentComplete(dto: RegisterParentCompleteDto) { } +async createManager(dto: CreateManagerDto) { } +async validateAccount(userId: string) { } +``` + +### 3. Noms de classes/interfaces/types +```typescript +// ✅ BON +export class DtoInscriptionParentComplet { } +export class ServiceAuthentification { } +export interface OptionsConfiguration { } +export type StatutUtilisateur = 'actif' | 'en_attente' | 'suspendu'; + +// ❌ MAUVAIS +export class RegisterParentCompleteDto { } +export class AuthService { } +export interface ConfigOptions { } +export type UserStatus = 'active' | 'pending' | 'suspended'; +``` + +### 4. Noms de fichiers +```typescript +// ✅ BON +inscription-parent-complet.dto.ts +service-authentification.ts +entite-utilisateurs.ts +controleur-configuration.ts + +// ❌ MAUVAIS +register-parent-complete.dto.ts +auth.service.ts +users.entity.ts +config.controller.ts +``` + +### 5. Propriétés d'entités/DTOs +```typescript +// ✅ BON +export class Enfants { + @Column({ name: 'prenom' }) + prenom: string; + + @Column({ name: 'date_naissance' }) + dateNaissance: Date; + + @Column({ name: 'consentement_photo' }) + consentementPhoto: boolean; +} + +// ❌ MAUVAIS +export class Children { + @Column({ name: 'first_name' }) + firstName: string; + + @Column({ name: 'birth_date' }) + birthDate: Date; + + @Column({ name: 'consent_photo' }) + consentPhoto: boolean; +} +``` + +### 6. Commentaires +```typescript +// ✅ BON +// Créer Parent 1 + Parent 2 (si existe) + entités parents +// Vérifier que l'email n'existe pas déjà +// Transaction : Créer utilisateur + entité métier + +// ❌ MAUVAIS +// Create Parent 1 + Parent 2 (if exists) + parent entities +// Check if email already exists +// Transaction: Create user + business entity +``` + +### 7. Messages d'erreur/succès +```typescript +// ✅ BON +throw new ConflictException('Un compte avec cet email existe déjà'); +return { message: 'Inscription réussie. Votre dossier est en attente de validation.' }; + +// ❌ MAUVAIS +throw new ConflictException('An account with this email already exists'); +return { message: 'Registration successful. Your application is pending validation.' }; +``` + +### 8. Logs +```typescript +// ✅ BON +this.logger.log('📦 Chargement de 16 configurations en cache'); +this.logger.error('Erreur lors de la création du parent'); + +// ❌ MAUVAIS +this.logger.log('📦 Loading 16 configurations in cache'); +this.logger.error('Error creating parent'); +``` + +--- + +## ✅ Ce qui RESTE en ANGLAIS (Termes Techniques) + +### 1. Patterns de conception +- `singleton` +- `factory` +- `repository` +- `observer` +- `decorator` + +### 2. Architecture/Framework +- `backend` / `frontend` +- `controller` +- `service` +- `middleware` +- `guard` +- `interceptor` +- `pipe` +- `filter` +- `module` +- `provider` + +### 3. Concepts techniques +- `entity` (TypeORM) +- `DTO` (Data Transfer Object) +- `API` / `endpoint` +- `token` (JWT) +- `hash` (bcrypt) +- `cache` +- `query` +- `transaction` +- `migration` +- `seed` + +### 4. Bibliothèques/Technologies +- `NestJS` +- `TypeORM` +- `PostgreSQL` +- `Docker` +- `Git` +- `JWT` +- `bcrypt` +- `Multer` +- `Nodemailer` + +### 5. Mots-clés TypeScript/JavaScript +- `async` / `await` +- `const` / `let` / `var` +- `function` +- `class` +- `interface` +- `type` +- `enum` +- `import` / `export` +- `return` +- `throw` + +--- + +## 📋 Exemples Complets + +### Exemple 1 : Service d'authentification + +```typescript +// ✅ BON +@Injectable() +export class ServiceAuthentification { + constructor( + private readonly serviceUtilisateurs: ServiceUtilisateurs, + private readonly serviceJwt: JwtService, + @InjectRepository(Utilisateurs) + private readonly depotUtilisateurs: Repository, + ) {} + + async inscrireParentComplet(dto: DtoInscriptionParentComplet) { + // Vérifier que l'email n'existe pas + const existe = await this.serviceUtilisateurs.trouverParEmail(dto.email); + if (existe) { + throw new ConflictException('Un compte avec cet email existe déjà'); + } + + // Générer le token de création de mot de passe + const tokenCreationMdp = crypto.randomUUID(); + const dateExpiration = new Date(); + dateExpiration.setDate(dateExpiration.getDate() + 7); + + // Transaction : Créer parent + enfants + const resultat = await this.depotUtilisateurs.manager.transaction(async (manager) => { + const parent1 = new Utilisateurs(); + parent1.email = dto.email; + parent1.prenom = dto.prenom; + parent1.nom = dto.nom; + parent1.tokenCreationMdp = tokenCreationMdp; + + const parentEnregistre = await manager.save(Utilisateurs, parent1); + + return { parent: parentEnregistre, token: tokenCreationMdp }; + }); + + return { + message: 'Inscription réussie. Votre dossier est en attente de validation.', + idParent: resultat.parent.id, + statut: 'en_attente', + }; + } +} +``` + +### Exemple 2 : Entité Enfants + +```typescript +// ✅ BON +@Entity('enfants') +export class Enfants { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'prenom', length: 100 }) + prenom: string; + + @Column({ name: 'nom', length: 100 }) + nom: string; + + @Column({ + type: 'enum', + enum: TypeGenre, + name: 'genre' + }) + genre: TypeGenre; + + @Column({ type: 'date', name: 'date_naissance', nullable: true }) + dateNaissance?: Date; + + @Column({ type: 'date', name: 'date_prevue_naissance', nullable: true }) + datePrevueNaissance?: Date; + + @Column({ name: 'photo_url', type: 'text', nullable: true }) + photoUrl?: string; + + @Column({ name: 'consentement_photo', type: 'boolean', default: false }) + consentementPhoto: boolean; + + @Column({ name: 'est_multiple', type: 'boolean', default: false }) + estMultiple: boolean; + + @Column({ + type: 'enum', + enum: StatutEnfantType, + name: 'statut' + }) + statut: StatutEnfantType; +} +``` + +--- + +## 🔄 Migration Progressive + +### Stratégie +1. ✅ **Nouveau code** : Appliquer la règle immédiatement +2. ⏳ **Code existant** : Migrer progressivement lors des modifications +3. ❌ **Ne PAS refactoriser** tout le code d'un coup + +### Priorités de migration +1. **Haute priorité** : Nouveaux fichiers, nouvelles fonctionnalités +2. **Moyenne priorité** : Fichiers modifiés fréquemment +3. **Basse priorité** : Code stable non modifié + +### Exemple de migration progressive +```typescript +// Avant (ancien code - OK pour l'instant) +export class Children { } + +// Après modification (nouveau code - appliquer la règle) +export class Enfants { } +``` + +--- + +## 🚫 Exceptions + +### Cas où l'anglais est toléré +1. **Noms de colonnes en BDD** : Si la BDD existe déjà (ex: `first_name` en BDD → `prenom` en TypeScript) +2. **APIs externes** : Noms imposés par des bibliothèques tierces +3. **Standards** : `id`, `uuid`, `url`, `email`, `password` (termes universels) + +--- + +## ✅ Checklist Avant Commit + +- [ ] Noms de variables en français +- [ ] Noms de fonctions/méthodes en français +- [ ] Noms de classes/interfaces en français +- [ ] Noms de fichiers en français +- [ ] Propriétés d'entités/DTOs en français +- [ ] Commentaires en français +- [ ] Messages d'erreur/succès en français +- [ ] Termes techniques restent en anglais +- [ ] Pas de `console.log` (utiliser `this.logger`) +- [ ] Pas de code commenté +- [ ] Types TypeScript corrects (pas de `any`) +- [ ] Imports propres (pas d'imports inutilisés) + +--- + +**Dernière mise à jour** : 1er Décembre 2025 +**Auteur** : Équipe P'titsPas + diff --git a/docs/ARCHITECTURE_TECHNIQUE.md b/docs/ARCHITECTURE_TECHNIQUE.md new file mode 100644 index 0000000..5fead37 --- /dev/null +++ b/docs/ARCHITECTURE_TECHNIQUE.md @@ -0,0 +1,592 @@ +# Architecture Technique - P'titsPas +## Guide d'Infrastructure et de Déploiement + +--- + +## Vue d'ensemble + +P'titsPas est une application de gestion de garde d'enfants pour les collectivités locales, basée sur une architecture **client-serveur moderne** : + +- **Frontend** : Application web Flutter (Single Page Application) +- **Backend** : API REST Node.js/Express avec TypeScript +- **Base de données** : PostgreSQL avec ORM Prisma +- **Architecture** : Séparation claire frontend/backend avec API REST + +--- + +## Prérequis Serveur + +### Environnement d'exécution + +#### Backend +- **Node.js** : Version 18+ (LTS recommandée : 18.19.0+) +- **npm** : Version 9+ +- **TypeScript** : Inclus dans les dépendances du projet + +#### Base de données +- **PostgreSQL** : Version 15+ +- **Extensions** : UUID (pour les clés primaires) + +#### Frontend +- **Serveur web statique** : nginx, Apache, ou similaire +- **Flutter Web** : Compilation en JavaScript (pas de prérequis runtime) + +### Ressources recommandées + +#### Environnement de développement +- **RAM** : 4GB minimum +- **CPU** : 2 vCPU +- **Storage** : 10GB + +#### Environnement de production +- **RAM** : 8GB recommandé (4GB minimum) +- **CPU** : 4 vCPU recommandé (2 vCPU minimum) +- **Storage** : 50GB minimum (base de données + logs + backups) +- **Réseau** : + - Port 3000 : API Backend (interne) + - Port 80/443 : Web (externe) + - Port 5432 : PostgreSQL (interne uniquement) + +--- + +## Stack Technique Détaillée + +### Backend (API) + +```json +{ + "runtime": "Node.js 18+", + "language": "TypeScript", + "framework": "Express.js 4.18+", + "orm": "Prisma 6.7+", + "database_client": "@prisma/client", + "security": [ + "helmet (sécurité headers)", + "cors (CORS policy)", + "bcrypt (hashage mots de passe)", + "jsonwebtoken (JWT auth)" + ], + "logging": "morgan", + "validation": "@nestjs/common" +} +``` + +### Frontend (Web) + +```json +{ + "framework": "Flutter 3.2.6+", + "language": "Dart 3.0+", + "compilation": "JavaScript (Flutter Web)", + "navigation": "go_router 13.2+", + "state_management": "provider 6.1+", + "ui_framework": "Material Design", + "fonts": "Google Fonts", + "http_client": "http 1.2+" +} +``` + +### Base de données + +```sql +-- Structure PostgreSQL +-- Tables principales : +-- - Parent (utilisateurs parents) +-- - Child (enfants) +-- - Contract (contrats de garde) +-- - Admin (administrateurs) +-- - Theme (thèmes interface) +-- - AppSettings (paramètres app) + +-- Types de données : +-- - UUID pour toutes les clés primaires +-- - Timestamps automatiques (createdAt, updatedAt) +-- - Enums pour les statuts +``` + +--- + +## Installation et Configuration + +### 1. Prérequis système + +```bash +# Ubuntu/Debian +sudo apt update +sudo apt install -y nodejs npm postgresql postgresql-contrib nginx + +# Vérification versions +node --version # >= 18.0.0 +npm --version # >= 9.0.0 +psql --version # >= 15.0 +``` + +### 2. Configuration base de données + +```sql +-- Se connecter en tant que postgres +sudo -u postgres psql + +-- Créer la base de données et l'utilisateur +CREATE DATABASE ptitspas; +CREATE USER ptitspas_user WITH PASSWORD 'secure_password_here'; +GRANT ALL PRIVILEGES ON DATABASE ptitspas TO ptitspas_user; +ALTER USER ptitspas_user CREATEDB; -- Pour les migrations + +-- Quitter +\q +``` + +### 3. Installation Backend + +```bash +cd backend + +# Installation des dépendances +npm install + +# Configuration environnement +cp .env.example .env +# Éditer .env avec vos paramètres + +# Génération du client Prisma et migrations +npx prisma generate +npx prisma migrate deploy + +# Initialisation admin (optionnel) +npm run init-admin +``` + +### 4. Installation Frontend + +```bash +cd frontend + +# Installation des dépendances Flutter +flutter pub get + +# Build pour production +flutter build web --release +``` + +--- + +## Variables d'Environnement + +### Backend (.env) + +```bash +# Base de données +DATABASE_URL="postgresql://ptitspas_user:secure_password_here@localhost:5432/ptitspas" + +# Sécurité +JWT_SECRET="your-super-secret-jwt-key-minimum-32-characters" +JWT_EXPIRES_IN="24h" + +# Serveur +PORT=3000 +NODE_ENV=production + +# Optionnel +CORS_ORIGIN="https://ptitspas.yourdomain.com" +``` + +--- + +## Déploiement + +### Option 1 : Déploiement classique (recommandé) + +#### Backend + +```bash +cd backend + +# Installation production +npm ci --only=production + +# Build TypeScript +npm run build + +# Démarrage (avec PM2 recommandé) +npm install -g pm2 +pm2 start dist/index.js --name "ptitspas-api" +pm2 startup +pm2 save +``` + +#### Frontend + +```bash +cd frontend + +# Build production +flutter build web --release + +# Copier vers serveur web +sudo cp -r build/web/* /var/www/ptitspas/ +sudo chown -R www-data:www-data /var/www/ptitspas/ +``` + +### Option 2 : Conteneurisation Docker + +#### Dockerfile Backend + +```dockerfile +FROM node:18-alpine + +# Créer répertoire app +WORKDIR /app + +# Copier package files +COPY package*.json ./ +COPY prisma ./prisma/ + +# Installer dépendances +RUN npm ci --only=production + +# Copier code source +COPY . . + +# Build +RUN npm run build + +# Générer client Prisma +RUN npx prisma generate + +# Exposer port +EXPOSE 3000 + +# Variables d'environnement +ENV NODE_ENV=production + +# Commande démarrage +CMD ["npm", "start"] +``` + +#### Dockerfile Frontend + +```dockerfile +FROM nginx:alpine + +# Copier build Flutter +COPY build/web /usr/share/nginx/html + +# Configuration nginx +COPY nginx.conf /etc/nginx/nginx.conf + +# Exposer port +EXPOSE 80 + +# Démarrage nginx +CMD ["nginx", "-g", "daemon off;"] +``` + +#### Docker Compose + +```yaml +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + environment: + POSTGRES_DB: ptitspas + POSTGRES_USER: ptitspas_user + POSTGRES_PASSWORD: secure_password_here + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + + backend: + build: ./backend + environment: + DATABASE_URL: postgresql://ptitspas_user:secure_password_here@postgres:5432/ptitspas + JWT_SECRET: your-super-secret-jwt-key + NODE_ENV: production + ports: + - "3000:3000" + depends_on: + - postgres + + frontend: + build: ./frontend + ports: + - "80:80" + depends_on: + - backend + +volumes: + postgres_data: +``` + +--- + +## Configuration Nginx + +### Configuration complète + +```nginx +server { + listen 80; + server_name ptitspas.yourdomain.com; + + # Redirection HTTPS + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name ptitspas.yourdomain.com; + + # Certificats SSL + ssl_certificate /etc/letsencrypt/live/ptitspas.yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ptitspas.yourdomain.com/privkey.pem; + + # Configuration SSL sécurisée + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + + # Frontend statique (Flutter Web) + location / { + root /var/www/ptitspas; + index index.html; + try_files $uri $uri/ /index.html; + + # Cache statique + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # API Backend + location /api/ { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Logs + access_log /var/log/nginx/ptitspas_access.log; + error_log /var/log/nginx/ptitspas_error.log; +} +``` + +--- + +## Sécurité + +### Obligatoire + +1. **HTTPS avec certificat SSL** + ```bash + # Installation Certbot + sudo apt install certbot python3-certbot-nginx + + # Génération certificat + sudo certbot --nginx -d ptitspas.yourdomain.com + + # Renouvellement automatique + sudo crontab -e + # Ajouter : 0 12 * * * /usr/bin/certbot renew --quiet + ``` + +2. **Firewall** + ```bash + # UFW (Ubuntu) + sudo ufw allow 22 # SSH + sudo ufw allow 80 # HTTP + sudo ufw allow 443 # HTTPS + sudo ufw enable + ``` + +3. **Base de données sécurisée** + ```bash + # PostgreSQL : accès local uniquement + sudo nano /etc/postgresql/15/main/postgresql.conf + # Commenter : #listen_addresses = 'localhost' + + sudo nano /etc/postgresql/15/main/pg_hba.conf + # Vérifier que seules les connexions locales sont autorisées + ``` + +4. **Backup automatique** + ```bash + # Script backup + #!/bin/bash + BACKUP_DIR="/var/backups/ptitspas" + DATE=$(date +%Y%m%d_%H%M%S) + + pg_dump -U ptitspas_user -h localhost ptitspas > $BACKUP_DIR/ptitspas_$DATE.sql + + # Nettoyer les backups > 30 jours + find $BACKUP_DIR -name "*.sql" -mtime +30 -delete + + # Crontab : tous les jours à 2h + # 0 2 * * * /path/to/backup_script.sh + ``` + +### Recommandé + +1. **Fail2Ban** (protection brute force) + ```bash + sudo apt install fail2ban + sudo systemctl enable fail2ban + ``` + +2. **Monitoring des logs** + ```bash + # Logrotate pour éviter les gros fichiers + sudo nano /etc/logrotate.d/ptitspas + ``` + +3. **Updates automatiques** + ```bash + sudo apt install unattended-upgrades + sudo dpkg-reconfigure unattended-upgrades + ``` + +--- + +## Commandes de Gestion + +### Démarrage des services + +```bash +# Backend (développement) +cd backend && npm run dev + +# Backend (production avec PM2) +pm2 start ptitspas-api +pm2 status + +# Base de données +sudo systemctl start postgresql +sudo systemctl status postgresql + +# Serveur web +sudo systemctl start nginx +sudo systemctl status nginx +``` + +### Maintenance + +```bash +# Migrations base de données +cd backend +npx prisma migrate deploy + +# Logs Backend +pm2 logs ptitspas-api + +# Logs Nginx +sudo tail -f /var/log/nginx/ptitspas_access.log +sudo tail -f /var/log/nginx/ptitspas_error.log + +# Restart services +pm2 restart ptitspas-api +sudo systemctl restart nginx +``` + +--- + +## Monitoring et Healthcheck + +### Endpoints de santé + +```bash +# API Health (à implémenter) +curl https://ptitspas.yourdomain.com/api/health + +# Base de données +psql -h localhost -U ptitspas_user -d ptitspas -c "SELECT 1;" + +# Frontend +curl -I https://ptitspas.yourdomain.com/ +``` + +### Logs à surveiller + +1. **Backend** : Via PM2 ou logs applicatifs +2. **PostgreSQL** : `/var/log/postgresql/postgresql-15-main.log` +3. **Nginx** : `/var/log/nginx/ptitspas_*.log` +4. **Système** : `/var/log/syslog` + +### Métriques importantes + +- **CPU/RAM** : Usage serveur +- **Espace disque** : Base de données et logs +- **Connexions DB** : Nombre de connexions actives +- **Temps de réponse** : API et frontend +- **Erreurs 5xx** : Erreurs serveur + +--- + +## Troubleshooting + +### Problèmes courants + +1. **Backend ne démarre pas** + ```bash + # Vérifier variables d'environnement + cd backend && cat .env + + # Vérifier connexion DB + npx prisma db pull + + # Logs détaillés + npm run dev + ``` + +2. **Frontend ne s'affiche pas** + ```bash + # Vérifier build + cd frontend && flutter build web + + # Vérifier nginx + sudo nginx -t + sudo systemctl reload nginx + ``` + +3. **Erreurs base de données** + ```bash + # Vérifier statut PostgreSQL + sudo systemctl status postgresql + + # Vérifier connexions + sudo -u postgres psql -c "SELECT * FROM pg_stat_activity;" + ``` + +--- + +## Évolutivité + +### Optimisations possibles + +1. **Cache Redis** : Pour les sessions et cache applicatif +2. **CDN** : Pour les assets statiques +3. **Load Balancer** : Pour haute disponibilité +4. **Clustering** : Multiple instances Node.js +5. **Database replication** : Master/Slave PostgreSQL + +### Monitoring avancé + +- **Prometheus + Grafana** : Métriques système et applicatif +- **ELK Stack** : Centralisation des logs +- **Uptime monitoring** : Surveillance externe + +Cette architecture est conçue pour être **scalable**, **maintenable** et **sécurisée** pour un environnement de production professionnel. \ No newline at end of file diff --git a/docs/BRIEFING-FRONTEND.md b/docs/BRIEFING-FRONTEND.md new file mode 100644 index 0000000..fde9ca1 --- /dev/null +++ b/docs/BRIEFING-FRONTEND.md @@ -0,0 +1,266 @@ +# 🎯 BRIEFING - Développement Frontend P'titsPas + +## Projet +Application de gestion de crèches/assistantes maternelles - Frontend Flutter Web + +--- + +## Accès Git + +```bash +# Cloner le repo +git clone https://git.ptits-pas.fr/jmartin/petitspas.git +cd petitspas/frontend + +# Identifiants Gitea (si demandé) +# User: jmartin +# Token: giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc + +# API Gitea pour consulter les tickets frontend +curl -H "Authorization: token giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq '.[] | select(.labels[].name == "frontend") | {number, title}' +``` + +--- + +## Backend API (déjà déployé) + +**URL de base** : `https://app.ptits-pas.fr/api/v1/` + +### Endpoints clés disponibles + +| Endpoint | Méthode | Description | +|----------|---------|-------------| +| `/configuration/setup/status` | GET | Vérifie si setup terminé (`setupCompleted: true/false`) | +| `/auth/login` | POST | Connexion (`{email, password}`) | +| `/auth/me` | GET | Profil utilisateur (inclut `changement_mdp_obligatoire`) | +| `/auth/change-password-required` | POST | Changement MDP obligatoire | +| `/auth/refresh` | POST | Rafraîchir les tokens | +| `/configuration` | GET | Liste toutes les configs (super_admin) | +| `/configuration/:category` | GET | Configs par catégorie (email, app, security) | +| `/configuration/bulk` | PATCH | Mise à jour multiple des configs | +| `/configuration/test-smtp` | POST | Test connexion SMTP | +| `/configuration/setup/complete` | POST | Marquer setup comme terminé | +| `/gestionnaires` | POST | Créer gestionnaire (super_admin only) | +| `/gestionnaires` | GET | Liste des gestionnaires | +| `/documents-legaux/actifs` | GET | CGU et Privacy actifs | + +### Compte test super_admin + +``` +Email: admin@ptits-pas.fr +MDP: 4dm1n1strateur +changement_mdp_obligatoire: true (première connexion) +``` + +--- + +## Tickets Frontend prioritaires + +### Workflow d'initialisation (P1 - BLOQUANT) + +| # | Ticket | Description | Effort | +|---|--------|-------------|--------| +| **#14** | Setup Wizard | Écran configuration initiale (SMTP, app) | 4-5h | +| **#47** | Changement MDP Obligatoire | Modale bloquante après login si flag=true | 1-2h | +| **#35** | Création Gestionnaire | Formulaire création gestionnaire | 2-3h | + +### Authentification (P3) + +| # | Ticket | Description | Effort | +|---|--------|-------------|--------| +| **#43** | Écran Création MDP | Page `/create-password?token=xxx` | 2h | +| **#48** | Gestion Erreurs & Messages | Snackbars, intercepteur HTTP | 2h | + +### Dashboard Gestionnaire (P3) + +| # | Ticket | Description | Effort | +|---|--------|-------------|--------| +| **#44** | Structure Dashboard | Layout avec onglets Parents/AM | 2h | +| **#45** | Liste Parents | Liste des parents en attente | 2h | +| **#46** | Liste AM | Liste des AM en attente | 2h | + +### Inscription (P3) + +| # | Ticket | Description | Effort | +|---|--------|-------------|--------| +| **#38** | Inscription Parent Étape 3 | Enfants | 3h | +| **#39** | Inscription Parent Étapes 4-6 | Finalisation | 4h | +| **#40-42** | Inscription AM | 3 panneaux | 6h | +| **#50** | Affichage CGU dynamique | Version lors inscription | 1h | + +### Admin (P3) + +| # | Ticket | Description | Effort | +|---|--------|-------------|--------| +| **#49** | Gestion Documents Légaux | Upload/activation CGU/Privacy | 4h | +| **#51** | Écran Logs Admin | Optionnel v1.1 | 4h | + +--- + +## Règles de codage + +### Langue + +- **Français** pour : commentaires, descriptions, noms métier +- **Anglais** pour : termes techniques (controller, service, widget, singleton, etc.) + +### Exemples + +```dart +// ✅ BON - Noms métier en français, technique en anglais +class EcranConnexion extends StatefulWidget { } +final utilisateurConnecte = authService.recupererUtilisateur(); +final gestionnaire = await gestionnaireService.creer(donnees); + +// Widget avec nom métier +class CarteEnfant extends StatelessWidget { } +class FormulaireInscriptionParent extends StatefulWidget { } + +// ❌ MAUVAIS - Tout en anglais +class LoginScreen extends StatefulWidget { } +final loggedInUser = authService.getUser(); +class ChildCard extends StatelessWidget { } +``` + +### Termes à garder en anglais +- Widget, StatefulWidget, StatelessWidget +- Controller, Service, Provider +- Singleton, Factory, Builder +- async, await, Future, Stream +- Navigator, Router, Route +- API, HTTP, JSON, DTO + +### Termes métier en français +- Utilisateur, Parent, Enfant, Gestionnaire, Administrateur +- AssistanteMaternelle (ou AM) +- Inscription, Connexion, Déconnexion +- Configuration, Paramètres +- Validation, Refus, EnAttente + +--- + +## Structure frontend existante + +``` +frontend/lib/ +├── config/ +│ └── env.dart # Configuration environnement +├── controllers/ +│ └── parent_dashboard_controller.dart +├── models/ +│ ├── user_registration_data.dart +│ ├── parent_user_registration_data.dart +│ └── am_user_registration_data.dart +├── navigation/ +│ └── app_router.dart # Routes de l'application +├── screens/ +│ ├── auth/ +│ │ ├── login_screen.dart ✅ Fait +│ │ ├── register_choice_screen.dart ✅ Fait +│ │ ├── parent_register_step1-5_screen.dart ✅ Fait +│ │ ├── am/ ✅ 4 étapes faites +│ │ └── parent/ ✅ 5 étapes faites +│ ├── administrateurs/ +│ │ ├── admin_dashboardScreen.dart +│ │ └── creation/ +│ │ └── gestionnaires_create.dart ⚠️ Placeholder +│ ├── home/ +│ │ ├── home_screen.dart +│ │ └── parent_screen/ +│ └── legal/ +├── services/ +│ ├── auth_service.dart +│ ├── bug_report_service.dart +│ ├── dashboardService.dart +│ ├── login_navigation_service.dart +│ └── api/ +│ ├── api_config.dart +│ └── tokenService.dart +├── utils/ +│ └── data_generator.dart +└── widgets/ + ├── admin/ + │ ├── dashboard_admin.dart + │ ├── gestionnaire_card.dart + │ ├── gestionnaire_management_widget.dart + │ └── parent_managmant_widget.dart + ├── dashbord_parent/ + └── ... +``` + +--- + +## Workflow Git + +```bash +# 1. Créer une branche feature +git checkout develop +git pull origin develop +git checkout -b feature/XX-nom-ticket + +# 2. Développer et commiter +git add . +git commit -m "feat(#XX): Description courte" + +# 3. Pousser et créer PR +git push -u origin feature/XX-nom-ticket + +# 4. Créer PR vers master via Gitea ou API +curl -X POST \ + -H "Authorization: token giteabu_1796c6aace0e2ef7e4fdb49cdc3bc1bf8ee31fbc" \ + -H "Content-Type: application/json" \ + -d '{"title":"feat(#XX): Titre","body":"Description\n\nCloses #XX","head":"feature/XX-nom-ticket","base":"master"}' \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls" +``` + +--- + +## Commandes Flutter + +```bash +# Installation des dépendances +cd frontend +flutter pub get + +# Lancer en mode développement +flutter run -d chrome +# ou +flutter run -d windows +# ou +flutter run -d macos + +# Build web +flutter build web + +# Analyser le code +flutter analyze +``` + +--- + +## Fichiers de configuration importants + +### `lib/config/env.dart` +```dart +class Env { + static const String apiUrl = 'https://app.ptits-pas.fr/api/v1'; +} +``` + +### `lib/services/api/api_config.dart` +Configuration des appels API avec intercepteurs. + +--- + +## Recommandation de démarrage + +1. **Ticket #47** - Modale changement MDP obligatoire (simple, rapide) +2. **Ticket #14** - Setup Wizard (écran configuration initiale) +3. **Ticket #35** - Formulaire création gestionnaire + +Ces 3 tickets complètent le **workflow d'initialisation** ! + +--- + +*Dernière mise à jour : 27 janvier 2026* diff --git a/docs/EVOLUTIONS_CDC.md b/docs/EVOLUTIONS_CDC.md index 8b7a74d..6496fc6 100644 --- a/docs/EVOLUTIONS_CDC.md +++ b/docs/EVOLUTIONS_CDC.md @@ -209,6 +209,7 @@ Pour chaque évolution identifiée, ce document suivra la structure suivante : - [x] Ajouter d'autres évolutions identifiées - [ ] Mettre à jour le CDC original - [ ] Valider les modifications avec les parties prenantes +- [ ] Modifier le texte de la checkbox de consentement photo (libellé actuel : 'J\'accepte l\'utilisation de ma photo.') sur l'écran d'inscription Nounou Étape 2 (`nanny_register_step2_screen.dart`). # Évolutions proposées au cahier des charges diff --git a/docs/test-data/README.md b/docs/test-data/README.md new file mode 100644 index 0000000..7908a9e --- /dev/null +++ b/docs/test-data/README.md @@ -0,0 +1,279 @@ +# 📊 Données de Test + +Ce dossier contient les jeux de données de test pour l'application P'titsPas. + +## 📁 Fichiers + +### `utilisateurs-test.csv` + +Fichier CSV contenant les utilisateurs de test pour valider le workflow de création de compte. + +**Format** : CSV avec en-tête +**Encodage** : UTF-8 +**Séparateur** : Virgule (`,`) + +--- + +## 👥 Utilisateurs de test + +### 1. Administrateur + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| BERNARD | Sophie | sophie.bernard@ptits-pas.fr | 01 39 98 45 67 | 06 78 12 34 56 | + +**Rôle** : `administrateur` +**Notes** : Responsable direction générale - Ancienneté 8 ans + +--- + +### 2. Gestionnaire + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| MOREAU | Lucas | lucas.moreau@ptits-pas.fr | 01 39 98 56 78 | 06 87 23 45 67 | + +**Rôle** : `gestionnaire` +**Notes** : Service gestion administrative - Ancienneté 3 ans + +--- + +### 3. Assistantes Maternelles (2) + +#### Marie DUBOIS + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| DUBOIS | Marie | marie.dubois@ptits-pas.fr | 01 39 98 67 89 | 06 96 34 56 78 | + +**Rôle** : `assistante_maternelle` +**Spécialité** : Bébés 0-18 mois +**Agrément** : 4 enfants +**Places disponibles** : 2 + +#### Fatima EL MANSOURI + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| EL MANSOURI | Fatima | fatima.elmansouri@ptits-pas.fr | 01 39 98 78 90 | 06 75 45 67 89 | + +**Rôle** : `assistante_maternelle` +**Spécialité** : 1-3 ans +**Agrément** : 3 enfants +**Places disponibles** : 1 + +--- + +### 4. Parents (5) + +#### Couple MARTIN (avec triplés) + +**Claire MARTIN** + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| MARTIN | Claire | claire.martin@ptits-pas.fr | 01 39 98 89 01 | 06 89 56 78 90 | + +**Profession** : Infirmière +**Situation** : Mariée - triplés + +**Thomas MARTIN** + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| MARTIN | Thomas | thomas.martin@ptits-pas.fr | 01 39 98 89 01 | 06 78 45 67 89 | + +**Profession** : Ingénieur +**Situation** : Marié - triplés + +**Enfants** : +- Emma MARTIN (née le 15/02/2023, 8 mois) +- Noah MARTIN (né le 15/02/2023, 8 mois) +- Léa MARTIN (née le 15/02/2023, 8 mois) + +**Notes** : Couple avec triplés - Besoin garde multiple + +--- + +#### Couple divorcé DURAND/ROUSSEAU + +**Amélie DURAND** + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| DURAND | Amélie | amelie.durand@ptits-pas.fr | 01 39 98 90 12 | 06 67 78 89 90 | + +**Profession** : Comptable +**Situation** : Divorcée + +**Julien ROUSSEAU** + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| ROUSSEAU | Julien | julien.rousseau@ptits-pas.fr | 01 39 98 01 23 | 06 56 67 78 89 | + +**Profession** : Commercial +**Situation** : Divorcé + +**Enfants** (en commun) : +- Chloé ROUSSEAU (née le 20/04/2022, 2 ans) +- Hugo ROUSSEAU (né le 10/03/2024, 6 mois) + +**Notes** : +- Amélie : Garde principale des enfants +- Julien : Garde alternée 1 weekend/2 + +--- + +#### Père célibataire + +**David LECOMTE** + +| Nom | Prénom | Email | Téléphone | Mobile | +|-----|--------|-------|-----------|--------| +| LECOMTE | David | david.lecomte@ptits-pas.fr | 01 39 98 12 34 | 06 45 56 67 78 | + +**Profession** : Développeur web +**Situation** : Père célibataire + +**Enfants** : +- Maxime LECOMTE (né le 15/04/2023, 1 an 5 mois) + +**Notes** : Garde complète - Contact urgence : grand-mère paternelle + +--- + +## 🧪 Utilisation pour les tests + +### Scénarios de test + +#### Scénario 1 : Création de gestionnaire + +```typescript +// Créer Lucas MOREAU en tant que gestionnaire +POST /api/v1/gestionnaires +{ + "email": "lucas.moreau@ptits-pas.fr", + "password": "Test1234!", + "prenom": "Lucas", + "nom": "MOREAU" +} +``` + +#### Scénario 2 : Inscription assistante maternelle + +```typescript +// Marie DUBOIS s'inscrit +POST /api/v1/auth/register +{ + "email": "marie.dubois@ptits-pas.fr", + "password": "Test1234!", + "prenom": "Marie", + "nom": "DUBOIS", + "telephone": "01 39 98 67 89", + "mobile": "06 96 34 56 78", + "role": "assistante_maternelle" +} +``` + +#### Scénario 3 : Inscription parent + +```typescript +// Claire MARTIN s'inscrit +POST /api/v1/auth/register +{ + "email": "claire.martin@ptits-pas.fr", + "password": "Test1234!", + "prenom": "Claire", + "nom": "MARTIN", + "telephone": "01 39 98 89 01", + "mobile": "06 89 56 78 90", + "role": "parent" +} +``` + +#### Scénario 4 : Validation par gestionnaire + +```typescript +// Lucas MOREAU valide Marie DUBOIS +PATCH /api/v1/users/{marie_id}/valider +Authorization: Bearer {lucas_token} +{ + "comment": "Agrément vérifié - Profil complet" +} +``` + +#### Scénario 5 : Cas complexe - Triplés + +Test du workflow complet avec le couple MARTIN ayant des triplés : +1. Claire et Thomas s'inscrivent séparément +2. Chacun déclare les 3 enfants +3. Validation par le gestionnaire +4. Recherche d'assistante maternelle avec capacité pour 3 enfants + +#### Scénario 6 : Cas complexe - Garde alternée + +Test du workflow avec le couple divorcé DURAND/ROUSSEAU : +1. Amélie et Julien s'inscrivent séparément +2. Chacun déclare les 2 enfants en commun +3. Gestion de la garde alternée +4. Coordination entre les 2 parents + +--- + +## 📧 Emails de test + +Tous les emails de test utilisent le domaine `@ptits-pas.fr`. + +**Pour les tests en local**, vous pouvez utiliser : +- [Mailtrap](https://mailtrap.io/) pour capturer les emails +- [MailHog](https://github.com/mailhog/MailHog) pour un serveur SMTP local +- [Thunderbird](https://www.thunderbird.net/) pour consulter les emails + +**Configuration Thunderbird** : +- Serveur IMAP : `mail.ptits-pas.fr` +- Port : 993 (SSL/TLS) +- Serveur SMTP : `mail.ptits-pas.fr` +- Port : 587 (STARTTLS) + +--- + +## 🔐 Mots de passe de test + +**Par défaut pour tous les utilisateurs de test** : `password` + +**Note** : Ce mot de passe simple est utilisé uniquement pour les tests. En production, les règles suivantes s'appliquent : +- Minimum 8 caractères +- Au moins 1 majuscule +- Au moins 1 chiffre +- Au moins 1 caractère spécial (recommandé) + +--- + +## 🗄️ Script d'import + +Un script d'import sera créé pour charger automatiquement ces données de test en base. + +**Fichier** : `backend/src/scripts/seed-test-data.ts` + +**Utilisation** : +```bash +cd backend +npm run seed:test +``` + +--- + +## ⚠️ Avertissement + +**Ces données sont uniquement pour les tests !** + +- Ne jamais utiliser en production +- Les emails sont fictifs mais utilisent le domaine réel `@ptits-pas.fr` +- Les numéros de téléphone sont fictifs +- Les adresses sont à Bezons (95870) mais peuvent être fictives + +--- + +**Dernière mise à jour** : 24 Novembre 2025 + diff --git a/docs/test-data/utilisateurs-test.csv b/docs/test-data/utilisateurs-test.csv new file mode 100644 index 0000000..c93a0ae --- /dev/null +++ b/docs/test-data/utilisateurs-test.csv @@ -0,0 +1,11 @@ +Type,Nom,Prenom,Email,Telephone,Mobile,Date_naissance,Adresse,Code_postal,Ville,Profession,Situation_familiale,Enfant_1_nom,Enfant_1_naissance,Enfant_1_age,Enfant_2_nom,Enfant_2_naissance,Enfant_2_age,Enfant_3_nom,Enfant_3_naissance,Enfant_3_age,Notes_particulieres +ADMINISTRATEUR,BERNARD,Sophie,sophie.bernard@ptits-pas.fr,01 39 98 45 67,06 78 12 34 56,15/03/1978,"12 Avenue Gabriel Péri",95870,Bezons,Responsable administrative,Mariée,,,,,,,,,,Responsable direction générale - Ancienneté 8 ans +GESTIONNAIRE,MOREAU,Lucas,lucas.moreau@ptits-pas.fr,01 39 98 56 78,06 87 23 45 67,22/09/1985,"8 Rue Jean Jaurès",95870,Bezons,Gestionnaire des placements,Célibataire,,,,,,,,,,Service gestion administrative - Ancienneté 3 ans +ASSISTANTE_MATERNELLE,DUBOIS,Marie,marie.dubois@ptits-pas.fr,01 39 98 67 89,06 96 34 56 78,08/06/1980,"25 Rue de la République",95870,Bezons,Assistante maternelle,Mariée,,,,,,,,,,Agrément 4 enfants - Spécialité bébés 0-18 mois - 2 places disponibles +ASSISTANTE_MATERNELLE,EL MANSOURI,Fatima,fatima.elmansouri@ptits-pas.fr,01 39 98 78 90,06 75 45 67 89,12/11/1975,"17 Boulevard Aristide Briand",95870,Bezons,Assistante maternelle,Mariée,,,,,,,,,,Agrément 3 enfants - Spécialité 1-3 ans - 1 place disponible +PARENT,MARTIN,Claire,claire.martin@ptits-pas.fr,01 39 98 89 01,06 89 56 78 90,03/04/1990,"5 Avenue du Général de Gaulle",95870,Bezons,Infirmière,Mariée - triplés,Emma MARTIN,15/02/2023,8 mois,Noah MARTIN,15/02/2023,8 mois,Léa MARTIN,15/02/2023,8 mois,Couple avec triplés - Besoin garde multiple +PARENT,MARTIN,Thomas,thomas.martin@ptits-pas.fr,01 39 98 89 01,06 78 45 67 89,18/07/1988,"5 Avenue du Général de Gaulle",95870,Bezons,Ingénieur,Marié - triplés,Emma MARTIN,15/02/2023,8 mois,Noah MARTIN,15/02/2023,8 mois,Léa MARTIN,15/02/2023,8 mois,Couple avec triplés - Besoin garde multiple +PARENT,DURAND,Amélie,amelie.durand@ptits-pas.fr,01 39 98 90 12,06 67 78 89 90,14/12/1987,"23 Rue Victor Hugo",95870,Bezons,Comptable,Divorcée,Chloé ROUSSEAU,20/04/2022,2 ans,Hugo ROUSSEAU,10/03/2024,6 mois,,,,"Garde principale des enfants - Ex-conjoint : Julien ROUSSEAU - Nom de jeune fille : DURAND" +PARENT,ROUSSEAU,Julien,julien.rousseau@ptits-pas.fr,01 39 98 01 23,06 56 67 78 89,29/08/1985,"14 Rue Pasteur",95870,Bezons,Commercial,Divorcé,Chloé ROUSSEAU,20/04/2022,2 ans,Hugo ROUSSEAU,10/03/2024,6 mois,,,,"Garde alternée 1 weekend/2 - Ex-conjointe : Amélie DURAND (née DURAND)" +PARENT,LECOMTE,David,david.lecomte@ptits-pas.fr,01 39 98 12 34,06 45 56 67 78,07/10/1992,"31 Rue Émile Zola",95870,Bezons,Développeur web,Père célibataire,Maxime LECOMTE,15/04/2023,1 an 5 mois,,,,,,"Garde complète - Contact urgence : grand-mère paternelle" + diff --git a/docs/test-data/utilisateurs-test.html b/docs/test-data/utilisateurs-test.html new file mode 100644 index 0000000..13231a0 --- /dev/null +++ b/docs/test-data/utilisateurs-test.html @@ -0,0 +1,359 @@ + + + + + + Fiches d'identification - Utilisateurs de test + + + +
+

🏡 Fiches d'identification - Utilisateurs de test

+

Système de gestion des assistantes maternelles - Bezons (95870)

+

Mot de passe universel : password

+
+ + +
+
+
Sophie BERNARD
+
ADMINISTRATEUR
+
+
+
+
Informations personnelles
+

Email : sophie.bernard@ptits-pas.fr

+

Téléphone : 01 39 98 45 67

+

Téléphone mobile : 06 78 12 34 56

+

Date de naissance : 15/03/1978

+
+
+
Adresse
+

12 Avenue Gabriel Péri
+ 95870 Bezons
+ France

+
+
+
Fonction
+

Poste : Responsable administrative

+

Service : Direction générale

+

Ancienneté : 8 ans

+
+
+
+ + +
+
+
Lucas MOREAU
+
GESTIONNAIRE
+
+
+
+
Informations personnelles
+

Email : lucas.moreau@ptits-pas.fr

+

Téléphone : 01 39 98 56 78

+

Téléphone mobile : 06 87 23 45 67

+

Date de naissance : 22/09/1985

+
+
+
Adresse
+

8 Rue Jean Jaurès
+ 95870 Bezons
+ France

+
+
+
Fonction
+

Poste : Gestionnaire des placements

+

Service : Gestion administrative

+

Ancienneté : 3 ans

+
+
+
+ + +
+
+
Marie DUBOIS
+
ASSISTANTE MATERNELLE
+
+
+
+
Informations personnelles
+

Email : marie.dubois@ptits-pas.fr

+

Téléphone : 01 39 98 67 89

+

Téléphone mobile : 06 96 34 56 78

+

Date de naissance : 08/06/1980

+
+
+
Adresse
+

25 Rue de la République
+ 95870 Bezons
+ France

+
+
+
Profession
+

Agrément : 4 enfants max

+

Expérience : 12 ans

+

Spécialité : Bébés 0-18 mois

+

Places disponibles : 2

+
+
+
+ + +
+
+
Fatima EL MANSOURI
+
ASSISTANTE MATERNELLE
+
+
+
+
Informations personnelles
+

Email : fatima.elmansouri@ptits-pas.fr

+

Téléphone : 01 39 98 78 90

+

Téléphone mobile : 06 75 45 67 89

+

Date de naissance : 12/11/1975

+
+
+
Adresse
+

17 Boulevard Aristide Briand
+ 95870 Bezons
+ France

+
+
+
Profession
+

Agrément : 3 enfants max

+

Expérience : 15 ans

+

Spécialité : Enfants 1-3 ans

+

Places disponibles : 1

+
+
+
+ + +
+
+
Claire & Thomas MARTIN
+
PARENTS - TRIPLÉS
+
+
+
+
Informations Claire
+

Email : claire.martin@ptits-pas.fr

+

Téléphone : 06 89 56 78 90

+

Date de naissance : 03/04/1990

+

Profession : Infirmière

+
+
+
Informations Thomas
+

Email : thomas.martin@ptits-pas.fr

+

Téléphone : 06 78 45 67 89

+

Date de naissance : 18/07/1988

+

Profession : Ingénieur

+
+
+
Adresse commune
+

5 Avenue du Général de Gaulle
+ 95870 Bezons
+ France
+ Tél. fixe : 01 39 98 89 01

+
+
+
+
👶 Leurs enfants (Triplés)
+
Emma MARTIN - Née le 15/02/2023 (8 mois)
+
Noah MARTIN - Né le 15/02/2023 (8 mois)
+
Léa MARTIN - Née le 15/02/2023 (8 mois)
+
+
+ + +
+
+
Amélie DURAND
+
PARENT - MÈRE
+
+
+
+
Informations personnelles
+

Email : amelie.durand@ptits-pas.fr

+

Téléphone : 06 67 78 89 90

+

Date de naissance : 14/12/1987

+

Profession : Comptable

+

Situation : Divorcée (nom de jeune fille : DURAND)

+
+
+
Adresse
+

23 Rue Victor Hugo
+ 95870 Bezons
+ France
+ Tél. fixe : 01 39 98 90 12

+
+
+
+
👶 Ses enfants
+
Chloé ROUSSEAU - Née le 20/04/2022 (2 ans)
+
Hugo ROUSSEAU - Né le 10/03/2024 (6 mois)
+
+
+ +
+
+
Julien ROUSSEAU
+
PARENT - PÈRE
+
+
+
+
Informations personnelles
+

Email : julien.rousseau@ptits-pas.fr

+

Téléphone : 06 56 67 78 89

+

Date de naissance : 29/08/1985

+

Profession : Commercial

+

Situation : Divorcé

+
+
+
Adresse
+

14 Rue Pasteur
+ 95870 Bezons
+ France
+ Tél. fixe : 01 39 98 01 23

+
+
+
+
👶 Ses enfants (garde alternée)
+
Chloé ROUSSEAU - Née le 20/04/2022 (2 ans) - 1 weekend/2
+
Hugo ROUSSEAU - Né le 10/03/2024 (6 mois) - 1 weekend/2
+

Ex-conjointe : Amélie DURAND (née DURAND)

+
+
+ + +
+
+
David LECOMTE
+
PARENT - PÈRE SEUL
+
+
+
+
Informations personnelles
+

Email : david.lecomte@ptits-pas.fr

+

Téléphone : 06 45 56 67 78

+

Date de naissance : 07/10/1992

+

Profession : Développeur web

+

Situation : Père célibataire

+
+
+
Adresse
+

31 Rue Émile Zola
+ 95870 Bezons
+ France
+ Tél. fixe : 01 39 98 12 34

+
+
+
Situation familiale
+

Garde : Complète

+

Contact d'urgence : Grand-mère paternelle

+

Besoin : Garde temps plein

+
+
+
+
👶 Son enfant
+
Maxime LECOMTE - Né le 15/04/2023 (1 an et 5 mois)
+
+
+ +
+ 🔐 Tous les comptes utilisent le mot de passe : password
+ 📧 Accès webmail : mail.ptits-pas.fr +
+ +
+

Fiches générées pour tests système - Données fictives -

+
+ + diff --git a/frontend/.github/workflows/flutter-check.yml b/frontend/.github/workflows/flutter-check.yml new file mode 100644 index 0000000..b7b9f8f --- /dev/null +++ b/frontend/.github/workflows/flutter-check.yml @@ -0,0 +1,34 @@ +name: Flutter Code Check + +on: + push: + branches: [main, dev, feature/*, hotfix] + pull_request: + branches: [main, dev] + +jobs: + flutter-check: + name: Analyse & Test Flutter + runs-on: ubuntu-latest + + steps: + - name: ⬇️ Checkout code + uses: actions/checkout@v3 + + - name: 💡 Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.19.0' # ou celle que tu utilises + channel: stable + + - name: 📦 Install dependencies + run: flutter pub get + + - name: 🔍 Dart Analyzer + run: flutter analyze + + # - name: 🧪 Run tests (if present) + # run: flutter test || echo "No tests found" + + - name: 🧱 Build (Flutter Web) + run: flutter build web --release diff --git a/frontend/CONTRIBUTING.md b/frontend/CONTRIBUTING.md new file mode 100644 index 0000000..f61eefb --- /dev/null +++ b/frontend/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# 🚀 Guide de contribution – Projet P'titsPas (Flutter) + +Bienvenue ! Ce guide explique comment collaborer efficacement sur ce projet Flutter. + +--- + +## 📌 Branches Git + +Le projet suit une stratégie de branches simple et efficace : + +| Branche | Rôle | +|---------------|---------------------------------------------| +| `main` | Production (version stable déployée) | +| `develop` | Intégration (version en cours de test) | +| `feature/XXX` | Développement d’une nouvelle fonctionnalité | +| `fix/XXX` | Correction de bug | +| `hotfix/XXX` | Patch urgent sur `main` | + +--- + +## ✅ Cycle de développement + +1. **Crée une branche à partir de `develop`** + ```bash + git checkout develop + git pull origin develop + git checkout -b feature/FRONT-XXX-nom-fonctionnalite + +2. Travaille localement + commit régulièrement + + Commits clairs et concis : + Nom de la branche: Fonctionnalité push + + ```bash + git commit -m "FRONT-021: ajout du widget zone enfants" + ``` + +3. Pousse ta branche + + Exemple + ```bash + git push origin feature/FRONT-XXX-nom + + +4. Ouvre une Pull Request vers develop + + Ps : **La PR vers develop est faite lorsque une fonctionnalité du ticket à été fait et testé ou lorsque tous le ticket est finis** + - Titre : [FRONT-021] Widget zone enfants + + - Description : ce que tu as fait, ce qu’il reste à tester + - Lie le ticket associé (ex: Fixes #21) + +5. Relecture & Merge + - Au moins 1 review nécessaire + - Pas de commit direct sur develop ou main + +6. Une fois merge, supprime la branche distante: + + PS: **La branche est supprimé que lorsque tout le ticket a été consommé** + ```bash + git push origin --delete feature/FRONT-XXX-nom + ``` + +🧼 Règles de bonne conduite + +- Une PR = une seule fonctionnalité ou correction + +- Code commenté si logique complexe + +- Garder les noms de variables/dossiers clairs et en anglais + +- Pas de code mort ou non utilisé + +- Tester les commandes du workflow(dans le .github) afin d'être sur de ne pas avoir des erreur dans le code et pour etre sur de passer les tests du Workflow \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..7aaab71 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,16 @@ +# Stage builder +FROM ghcr.io/cirruslabs/flutter:3.19.0 AS builder +WORKDIR /app +COPY pubspec.* ./ +RUN flutter pub get +COPY . . +RUN flutter build web --release + +# Stage production +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/build/web /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/README.md b/frontend/README.md index 9e6f97c..1151cd8 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -14,3 +14,56 @@ A few resources to get you started if this is your first Flutter project: For help getting started with Flutter development, view the [online documentation](https://docs.flutter.dev/), which offers tutorials, samples, guidance on mobile development, and a full API reference. + +### Workflow Git + +Le projet suit un **Git Flow simplifié** avec 3 branches principales : + +- `main` : version stable et déployée en production +- `develop` : version intégrée et testée avant passage en production +- `feature/*`, `fix/*`, `hotfix/*` : branches spécifiques au développement + +**Cycle standard :** +```bash +# Création d’une feature +git checkout develop +git checkout -b feature/FRONT-021-zone-enfants + +# Développement +git add . +git commit -m "FRONT-021: Widget zone enfants" +git push origin feature/FRONT-021-zone-enfants + +# Pull Request => vers develop +# Merge → suppression de la branche +Voir CONTRIBUTING.md pour les conventions détaillées. +``` +### Structure du projet Flutter + +Le projet suit une architecture modulaire MVC simplifiée compatible avec Provider (ou Riverpod léger). + +```plaintext +lib/ +├── main.dart # Point d’entrée +├── routes/ # go_router ou auto_route +├── models/ # Classes de données (User, Parent, Enfant, etc.) +├── services/ # Requêtes HTTP, AuthService, StorageService +├── utils/ # Helpers, validateurs, formatteurs +├── widgets/ # Composants UI réutilisables +├── screens/ # Pages par grande fonctionnalité +│ ├── auth/ # Connexion, inscription, mot de passe oublié +│ ├── registration/ # Création parent / assistante maternelle +│ ├── dashboard/ # Tableau de bord parent / AM / gestionnaire +│ ├── profile/ # Gestion des infos utilisateur +│ └── children/ # Fiches enfants +``` + +### Architecture choisie +🟩 Type : MVC Modulaire avec Provider (ou Riverpod léger) + +Avantages : + +- Simple à prendre en main +- Rapide à structurer +- Permet la séparation des features +- Adaptée à un projet Flutter Web PWA diff --git a/frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java deleted file mode 100644 index 2ecdf29..0000000 --- a/frontend/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.flutter.plugins; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import io.flutter.Log; - -import io.flutter.embedding.engine.FlutterEngine; - -/** - * Generated file. Do not edit. - * This file is generated by the Flutter tool based on the - * plugins that support the Android platform. - */ -@Keep -public final class GeneratedPluginRegistrant { - private static final String TAG = "GeneratedPluginRegistrant"; - public static void registerWith(@NonNull FlutterEngine flutterEngine) { - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin image_picker_android, io.flutter.plugins.imagepicker.ImagePickerPlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e); - } - try { - flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin()); - } catch (Exception e) { - Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e); - } - } -} diff --git a/frontend/android/local.properties b/frontend/android/local.properties index 189af7a..abce8fc 100644 --- a/frontend/android/local.properties +++ b/frontend/android/local.properties @@ -1 +1,2 @@ -flutter.sdk=C:\\Users\\marti\\dev\\flutter \ No newline at end of file +flutter.sdk=/home/deploy/snap/flutter/common/flutter +sdk.dir=C:\\Users\\myhan\\AppData\\Local\\Android\\Sdk \ No newline at end of file diff --git a/frontend/assets/images/icon.svg b/frontend/assets/images/icon.svg new file mode 100644 index 0000000..9973b2a --- /dev/null +++ b/frontend/assets/images/icon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/assets/images/icon_improved.svg b/frontend/assets/images/icon_improved.svg new file mode 100644 index 0000000..88e4d30 --- /dev/null +++ b/frontend/assets/images/icon_improved.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/lib/config/app_router.dart b/frontend/lib/config/app_router.dart new file mode 100644 index 0000000..d456ad6 --- /dev/null +++ b/frontend/lib/config/app_router.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +// Models +import '../models/user_registration_data.dart'; +import '../models/nanny_registration_data.dart'; +import '../models/am_registration_data.dart'; + +// Screens +import '../screens/auth/login_screen.dart'; +import '../screens/auth/register_choice_screen.dart'; +import '../screens/auth/parent_register_step1_screen.dart'; +import '../screens/auth/parent_register_step2_screen.dart'; +import '../screens/auth/parent_register_step3_screen.dart'; +import '../screens/auth/parent_register_step4_screen.dart'; +import '../screens/auth/parent_register_step5_screen.dart'; +import '../screens/auth/nanny_register_step1_screen.dart'; +import '../screens/auth/nanny_register_step2_screen.dart'; +import '../screens/auth/nanny_register_step3_screen.dart'; +import '../screens/auth/nanny_register_step4_screen.dart'; +import '../screens/auth/nanny_register_confirmation_screen.dart'; +import '../screens/auth/am_register_step1_screen.dart'; +import '../screens/auth/am_register_step2_screen.dart'; +import '../screens/auth/am_register_step3_screen.dart'; +import '../screens/auth/am_register_step4_screen.dart'; +import '../screens/home/home_screen.dart'; +import '../screens/unknown_screen.dart'; + +// --- Provider Instances --- +// It's generally better to provide these higher up the widget tree if possible, +// or ensure they are created only once. +// For ShellRoute, creating them here and passing via .value is common. + +final userRegistrationDataNotifier = UserRegistrationData(); +final nannyRegistrationDataNotifier = NannyRegistrationData(); +final amRegistrationDataNotifier = AmRegistrationData(); + +class AppRouter { + static final GoRouter router = GoRouter( + initialLocation: '/login', + errorBuilder: (context, state) => const UnknownScreen(), + debugLogDiagnostics: true, + routes: [ + GoRoute( + path: '/login', + builder: (BuildContext context, GoRouterState state) => const LoginScreen(), + ), + GoRoute( + path: '/register-choice', + builder: (BuildContext context, GoRouterState state) => const RegisterChoiceScreen(), + ), + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) => const HomeScreen(), + ), + + // --- Parent Registration Flow --- + ShellRoute( + builder: (context, state, child) { + return ChangeNotifierProvider.value( + value: userRegistrationDataNotifier, + child: child, + ); + }, + routes: [ + GoRoute( + path: '/parent-register-step1', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep1Screen(), + ), + GoRoute( + path: '/parent-register-step2', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep2Screen(), + ), + GoRoute( + path: '/parent-register-step3', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep3Screen(), + ), + GoRoute( + path: '/parent-register-step4', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep4Screen(), + ), + GoRoute( + path: '/parent-register-step5', + builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep5Screen(), + ), + GoRoute( + path: '/parent-register-confirmation', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterConfirmationScreen(), + ), + ], + ), + + // --- Nanny Registration Flow --- + ShellRoute( + builder: (context, state, child) { + return ChangeNotifierProvider.value( + value: nannyRegistrationDataNotifier, + child: child, + ); + }, + routes: [ + GoRoute( + path: '/nanny-register-step1', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep1Screen(), + ), + GoRoute( + path: '/nanny-register-step2', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep2Screen(), + ), + GoRoute( + path: '/nanny-register-step3', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep3Screen(), + ), + GoRoute( + path: '/nanny-register-step4', + builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep4Screen(), + ), + GoRoute( + path: '/nanny-register-confirmation', + builder: (BuildContext context, GoRouterState state) { + return const NannyRegisterConfirmationScreen(); + }, + ), + ], + ), + + // --- AM (Assistante Maternelle) Registration Flow --- + ShellRoute( + builder: (context, state, child) { + return ChangeNotifierProvider.value( + value: amRegistrationDataNotifier, + child: child, + ); + }, + routes: [ + GoRoute( + path: '/am-register-step1', + builder: (BuildContext context, GoRouterState state) => const AmRegisterStep1Screen(), + ), + GoRoute( + path: '/am-register-step2', + builder: (BuildContext context, GoRouterState state) => const AmRegisterStep2Screen(), + ), + GoRoute( + path: '/am-register-step3', + builder: (BuildContext context, GoRouterState state) => const AmRegisterStep3Screen(), + ), + GoRoute( + path: '/am-register-step4', + builder: (BuildContext context, GoRouterState state) => const AmRegisterStep4Screen(), + ), + ], + ), + ], + ); +} \ No newline at end of file diff --git a/frontend/lib/config/env.dart b/frontend/lib/config/env.dart new file mode 100644 index 0000000..e044e1a --- /dev/null +++ b/frontend/lib/config/env.dart @@ -0,0 +1,14 @@ +class Env { + // Base URL de l'API, surchargeable à la compilation via --dart-define=API_BASE_URL + static const String apiBaseUrl = String.fromEnvironment( + 'API_BASE_URL', + defaultValue: 'https://ynov.ptits-pas.fr', + ); + + // Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/') + static String apiV1(String path) => "${apiBaseUrl}/api/v1$path"; +} + + + + diff --git a/frontend/lib/controllers/parent_dashboard_controller.dart b/frontend/lib/controllers/parent_dashboard_controller.dart new file mode 100644 index 0000000..143d84a --- /dev/null +++ b/frontend/lib/controllers/parent_dashboard_controller.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/child_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/contract_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/event_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/notification_model.dart'; +import 'package:p_tits_pas/services/dashboardService.dart'; + +class ParentDashboardController extends ChangeNotifier { + final DashboardService _dashboardService; + + ParentDashboardController(this._dashboardService); + + // État des données + List _children = []; + String? _selectedChildId; + AssistantModel? _selectedAssistant; + List _upcomingEvents = []; + List _contracts = []; + List _conversations = []; + List _notifications = []; + + // État de chargement + bool _isLoading = false; + String? _error; + + // Getters + List get children => _children; + String? get selectedChildId => _selectedChildId; + ChildModel? get selectedChild => _children.where((c) => c.id == _selectedChildId).firstOrNull; + AssistantModel? get selectedAssistant => _selectedAssistant; + List get upcomingEvents => _upcomingEvents; + List get contracts => _contracts; + List get conversations => _conversations; + List get notifications => _notifications; + bool get isLoading => _isLoading; + String? get error => _error; + + // Initialisation du dashboard + Future initDashboard() async { + _isLoading = true; + _error = null; + notifyListeners(); + + try { + await Future.wait([ + _loadChildren(), + _loadUpcomingEvents(), + _loadContracts(), + _loadConversations(), + _loadNotifications(), + ]); + + // Sélectionner le premier enfant par défaut + if (_children.isNotEmpty && _selectedChildId == null) { + await selectChild(_children.first.id); + } + } catch (e) { + _error = 'Erreur lors du chargement du tableau de bord: $e'; + } finally { + _isLoading = false; + notifyListeners(); + } + } + + // Sélection d'un enfant + Future selectChild(String childId) async { + _selectedChildId = childId; + notifyListeners(); + + // Charger les données spécifiques à cet enfant + await _loadChildSpecificData(childId); + } + + // Afficher le modal d'ajout d'enfant + void showAddChildModal() { + // Logique pour ouvrir le modal d'ajout d'enfant + // Sera implémentée dans le ticket FRONT-09 + } + + // Méthodes privées de chargement des données + Future _loadChildren() async { + _children = await _dashboardService.getChildren(); + notifyListeners(); + } + + Future _loadChildSpecificData(String childId) async { + try { + // Charger l'assistante maternelle associée à cet enfant + _selectedAssistant = await _dashboardService.getAssistantForChild(childId); + + // Filtrer les événements et contrats pour cet enfant + _upcomingEvents = await _dashboardService.getEventsForChild(childId); + _contracts = await _dashboardService.getContractsForChild(childId); + + notifyListeners(); + } catch (e) { + _error = 'Erreur lors du chargement des données pour l\'enfant: $e'; + notifyListeners(); + } + } + + Future _loadUpcomingEvents() async { + _upcomingEvents = await _dashboardService.getUpcomingEvents(); + notifyListeners(); + } + + Future _loadContracts() async { + _contracts = await _dashboardService.getContracts(); + notifyListeners(); + } + + Future _loadConversations() async { + _conversations = await _dashboardService.getConversations(); + notifyListeners(); + } + + Future _loadNotifications() async { + _notifications = await _dashboardService.getNotifications(); + notifyListeners(); + } + + // Méthodes d'action + Future markNotificationAsRead(String notificationId) async { + try { + await _dashboardService.markNotificationAsRead(notificationId); + await _loadNotifications(); // Recharger les notifications + } catch (e) { + _error = 'Erreur lors du marquage de la notification: $e'; + notifyListeners(); + } + } + + Future refreshDashboard() async { + await initDashboard(); + } +} \ No newline at end of file diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 7cf07f5..4478ce1 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation // import 'package:provider/provider.dart'; // Supprimer Provider -import 'navigation/app_router.dart'; +import 'config/app_router.dart'; // <-- Importer le bon routeur (GoRouter) // import 'theme/app_theme.dart'; // Supprimer AppTheme // import 'theme/theme_provider.dart'; // Supprimer ThemeProvider @@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { // Pas besoin de Provider.of ici - return MaterialApp( + return MaterialApp.router( // <-- Utilisation de MaterialApp.router title: 'P\'titsPas', theme: ThemeData.light().copyWith( // Utiliser un thème simple par défaut textTheme: GoogleFonts.meriendaTextTheme( @@ -35,8 +35,7 @@ class MyApp extends StatelessWidget { // Locale('en', 'US'), // Anglais, si besoin ], locale: const Locale('fr', 'FR'), // Forcer la locale française par défaut - initialRoute: AppRouter.login, - onGenerateRoute: AppRouter.generateRoute, + routerConfig: AppRouter.router, // <-- Passer la configuration du GoRouter debugShowCheckedModeBanner: false, ); } diff --git a/frontend/lib/models/am_registration_data.dart b/frontend/lib/models/am_registration_data.dart new file mode 100644 index 0000000..b660210 --- /dev/null +++ b/frontend/lib/models/am_registration_data.dart @@ -0,0 +1,136 @@ +import 'package:flutter/foundation.dart'; + +class AmRegistrationData extends ChangeNotifier { + // Step 1: Identity Info + String firstName = ''; + String lastName = ''; + String streetAddress = ''; // Nouveau pour N° et Rue + String postalCode = ''; // Nouveau + String city = ''; // Nouveau + String phone = ''; + String email = ''; + String password = ''; + // String? photoPath; // Déplacé ou géré à l'étape 2 + // bool photoConsent = false; // Déplacé ou géré à l'étape 2 + + // Step 2: Professional Info + String? photoPath; // Ajouté pour l'étape 2 + bool photoConsent = false; // Ajouté pour l'étape 2 + DateTime? dateOfBirth; + String birthCity = ''; // Nouveau + String birthCountry = ''; // Nouveau + // String placeOfBirth = ''; // Remplacé par birthCity et birthCountry + String nir = ''; // Numéro de Sécurité Sociale + String agrementNumber = ''; // Numéro d'agrément + int? capacity; // Number of children the AM can look after + + // Step 3: Presentation & CGU + String presentationText = ''; + bool cguAccepted = false; + + // --- Methods to update data and notify listeners --- + + void updateIdentityInfo({ + String? firstName, + String? lastName, + String? streetAddress, // Modifié + String? postalCode, // Nouveau + String? city, // Nouveau + String? phone, + String? email, + String? password, + }) { + this.firstName = firstName ?? this.firstName; + this.lastName = lastName ?? this.lastName; + this.streetAddress = streetAddress ?? this.streetAddress; // Modifié + this.postalCode = postalCode ?? this.postalCode; // Nouveau + this.city = city ?? this.city; // Nouveau + this.phone = phone ?? this.phone; + this.email = email ?? this.email; + this.password = password ?? this.password; + // if (photoPath != null || this.photoPath != null) { // Supprimé de l'étape 1 + // this.photoPath = photoPath; + // } + // this.photoConsent = photoConsent ?? this.photoConsent; // Supprimé de l'étape 1 + notifyListeners(); + } + + void updateProfessionalInfo({ + String? photoPath, + bool? photoConsent, + DateTime? dateOfBirth, + String? birthCity, // Nouveau + String? birthCountry, // Nouveau + // String? placeOfBirth, // Remplacé + String? nir, + String? agrementNumber, + int? capacity, + }) { + // Allow setting photoPath to null explicitly + if (photoPath != null || this.photoPath != null) { + this.photoPath = photoPath; + } + this.photoConsent = photoConsent ?? this.photoConsent; + this.dateOfBirth = dateOfBirth ?? this.dateOfBirth; + this.birthCity = birthCity ?? this.birthCity; // Nouveau + this.birthCountry = birthCountry ?? this.birthCountry; // Nouveau + // this.placeOfBirth = placeOfBirth ?? this.placeOfBirth; // Remplacé + this.nir = nir ?? this.nir; + this.agrementNumber = agrementNumber ?? this.agrementNumber; + this.capacity = capacity ?? this.capacity; + notifyListeners(); + } + + void updatePresentationAndCgu({ + String? presentationText, + bool? cguAccepted, + }) { + this.presentationText = presentationText ?? this.presentationText; + this.cguAccepted = cguAccepted ?? this.cguAccepted; + notifyListeners(); + } + + // --- Getters for validation or display --- + bool get isStep1Complete => + firstName.isNotEmpty && + lastName.isNotEmpty && + streetAddress.isNotEmpty && // Modifié + postalCode.isNotEmpty && // Nouveau + city.isNotEmpty && // Nouveau + phone.isNotEmpty && + email.isNotEmpty; + // password n'est pas requis à l'inscription (défini après validation par lien email) + + bool get isStep2Complete => + // photoConsent is mandatory if a photo is system-required, otherwise optional. + // For now, let's assume if photoPath is present, consent should ideally be true. + // Or, make consent always mandatory if photo section exists. + // Based on new mockup, photo is present, so consent might be implicitly or explicitly needed. + (photoPath != null ? photoConsent == true : true) && // Ajuster selon la logique de consentement désirée + dateOfBirth != null && + birthCity.isNotEmpty && + birthCountry.isNotEmpty && + nir.isNotEmpty && // Basic check, could add validation + agrementNumber.isNotEmpty && + capacity != null && capacity! > 0; + + bool get isStep3Complete => + // presentationText is optional as per CDC (message au gestionnaire) + cguAccepted; + + bool get isRegistrationComplete => + isStep1Complete && isStep2Complete && isStep3Complete; + + @override + String toString() { + return 'AmRegistrationData(' + 'firstName: $firstName, lastName: $lastName, ' + 'streetAddress: $streetAddress, postalCode: $postalCode, city: $city, ' + 'phone: $phone, email: $email, ' + // 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié + 'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, ' + 'nir: $nir, agrementNumber: $agrementNumber, capacity: $capacity, ' + 'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, ' + 'presentationText: $presentationText, cguAccepted: $cguAccepted)'; + } +} diff --git a/frontend/lib/models/am_user_registration_data.dart b/frontend/lib/models/am_user_registration_data.dart new file mode 100644 index 0000000..7a04ff2 --- /dev/null +++ b/frontend/lib/models/am_user_registration_data.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +class ChildminderId { + String firstName; + String lastName; + String address; + String postalCode; + String city; + String phone; + String email; + String password; + File? profilePicture; + bool photoConsent; + + ChildminderId({ + this.firstName = '', + this.lastName = '', + this.address = '', + this.postalCode = '', + this.city = '', + this.phone = '', + this.email = '', + this.password = '', + this.profilePicture, + this.photoConsent = false, + }); +} + +class ChildminderProfessional { + String dateOfBirth; + String birthCity; + String birthCountry; + String socialSecurityNumber; // NIR + String agreementNumber; + int maxChildren; + + ChildminderProfessional({ + this.dateOfBirth = '', + this.birthCity = '', + this.birthCountry = '', + this.socialSecurityNumber = '', + this.agreementNumber = '', + this.maxChildren = 1, + }); +} + +class ChildminderRegistrationData { + ChildminderId identity; + ChildminderProfessional professional; + String presentationMessage; + bool cguAccepted; + bool isPhotoRequired; + + ChildminderRegistrationData({ + ChildminderId? identityData, + ChildminderProfessional? professionalData, + this.presentationMessage = '', + this.cguAccepted = false, + this.isPhotoRequired = false, + }) : identity = identityData ?? ChildminderId(), + professional = professionalData ?? ChildminderProfessional(); + + void updateIdentity(ChildminderId data) { + identity = data; + } + + void updateProfessional(ChildminderProfessional data) { + professional = data; + } + + void updatePresentation(String message) { + presentationMessage = message; + } + + void acceptCGU() { + cguAccepted = true; + } + + bool get isComplete { + return identity.firstName.isNotEmpty && + identity.lastName.isNotEmpty && + identity.address.isNotEmpty && + identity.postalCode.isNotEmpty && + identity.city.isNotEmpty && + identity.phone.isNotEmpty && + identity.email.isNotEmpty && + identity.password.isNotEmpty && + professional.dateOfBirth.isNotEmpty && + professional.birthCity.isNotEmpty && + professional.birthCountry.isNotEmpty && + professional.socialSecurityNumber.isNotEmpty && + professional.agreementNumber.isNotEmpty && + cguAccepted && + (!isPhotoRequired || (identity.profilePicture != null && identity.photoConsent)); + } +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/assistant_model.dart b/frontend/lib/models/m_dashbord/assistant_model.dart new file mode 100644 index 0000000..bc880c3 --- /dev/null +++ b/frontend/lib/models/m_dashbord/assistant_model.dart @@ -0,0 +1,51 @@ +class AssistantModel { + final String id; + final String firstName; + final String lastName; + final String? photoUrl; + final double hourlyRate; + final double dailyFees; + final AssistantStatus status; + final String? address; + final String? phone; + final String? email; + + AssistantModel({ + required this.id, + required this.firstName, + required this.lastName, + this.photoUrl, + required this.hourlyRate, + required this.dailyFees, + required this.status, + this.address, + this.phone, + this.email, + }); + + factory AssistantModel.fromJson(Map json) { + return AssistantModel( + id: json['id'], + firstName: json['firstName'], + lastName: json['lastName'], + photoUrl: json['photoUrl'], + hourlyRate: json['hourlyRate'].toDouble(), + dailyFees: json['dailyFees'].toDouble(), + status: AssistantStatus.values.byName(json['status']), + address: json['address'], + phone: json['phone'], + email: json['email'], + ); + } + + String get fullName => '$firstName $lastName'; + String get hourlyRateFormatted => '${hourlyRate.toStringAsFixed(2)} €/h'; + String get dailyFeesFormatted => '${dailyFees.toStringAsFixed(2)} €/jour'; +} + +enum AssistantStatus { + available, + busy, + onHoliday, + unavailable, +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/child_model.dart b/frontend/lib/models/m_dashbord/child_model.dart new file mode 100644 index 0000000..2732b1c --- /dev/null +++ b/frontend/lib/models/m_dashbord/child_model.dart @@ -0,0 +1,58 @@ +class ChildModel { + final String id; + final String firstName; + final String? lastName; + final String? photoUrl; + final DateTime birthDate; + final ChildStatus status; + final String? assistantId; + + ChildModel({ + required this.id, + required this.firstName, + this.lastName, + this.photoUrl, + required this.birthDate, + required this.status, + this.assistantId, + }); + + factory ChildModel.fromJson(Map json) { + return ChildModel( + id: json['id'], + firstName: json['firstName'], + lastName: json['lastName'], + photoUrl: json['photoUrl'], + birthDate: DateTime.parse(json['birthDate']), + status: ChildStatus.values.byName(json['status']), + assistantId: json['assistantId'], + ); + } + + Map toJson() { + return { + 'id': id, + 'firstName': firstName, + 'lastName': lastName, + 'photoUrl': photoUrl, + 'birthDate': birthDate.toIso8601String(), + 'status': status.name, + 'assistantId': assistantId, + }; + } + + String get fullName => lastName != null ? '$firstName $lastName' : firstName; + + int get ageInMonths { + final now = DateTime.now(); + return (now.year - birthDate.year) * 12 + (now.month - birthDate.month); + } +} + +enum ChildStatus { + withAssistant, // En garde chez l'assistante + available, // Disponible + onHoliday, // En vacances + sick, // Malade + searching, // Recherche d'assistante +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/contract_model.dart b/frontend/lib/models/m_dashbord/contract_model.dart new file mode 100644 index 0000000..9b65ce8 --- /dev/null +++ b/frontend/lib/models/m_dashbord/contract_model.dart @@ -0,0 +1,65 @@ +class ContractModel { + final String id; + final String childId; + final String assistantId; + final ContractStatus status; + final DateTime startDate; + final DateTime? endDate; + final double hourlyRate; + final Map? terms; + final DateTime createdAt; + final DateTime? signedAt; + + ContractModel({ + required this.id, + required this.childId, + required this.assistantId, + required this.status, + required this.startDate, + this.endDate, + required this.hourlyRate, + this.terms, + required this.createdAt, + this.signedAt, + }); + + factory ContractModel.fromJson(Map json) { + return ContractModel( + id: json['id'], + childId: json['childId'], + assistantId: json['assistantId'], + status: ContractStatus.values.byName(json['status']), + startDate: DateTime.parse(json['startDate']), + endDate: json['endDate'] != null ? DateTime.parse(json['endDate']) : null, + hourlyRate: json['hourlyRate'].toDouble(), + terms: json['terms'], + createdAt: DateTime.parse(json['createdAt']), + signedAt: json['signedAt'] != null ? DateTime.parse(json['signedAt']) : null, + ); + } + + bool get isActive => status == ContractStatus.active; + bool get needsSignature => status == ContractStatus.draft; + String get statusLabel { + switch (status) { + case ContractStatus.draft: + return 'Brouillon'; + case ContractStatus.pending: + return 'En attente de validation'; + case ContractStatus.active: + return 'En cours'; + case ContractStatus.ended: + return 'Terminé'; + case ContractStatus.cancelled: + return 'Annulé'; + } + } +} + +enum ContractStatus { + draft, + pending, + active, + ended, + cancelled, +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/conversation_model.dart b/frontend/lib/models/m_dashbord/conversation_model.dart new file mode 100644 index 0000000..059d57a --- /dev/null +++ b/frontend/lib/models/m_dashbord/conversation_model.dart @@ -0,0 +1,46 @@ +class ConversationModel { + final String id; + final String title; + final List participantIds; + final List messages; + final DateTime lastMessageAt; + final int unreadCount; + final String? childId; + + ConversationModel({ + required this.id, + required this.title, + required this.participantIds, + required this.messages, + required this.lastMessageAt, + this.unreadCount = 0, + this.childId, + }); + + MessageModel? get lastMessage => messages.isNotEmpty ? messages.last : null; + bool get hasUnreadMessages => unreadCount > 0; +} + +class MessageModel { + final String id; + final String content; + final String senderId; + final DateTime sentAt; + final bool isFromAI; + final MessageStatus status; + + MessageModel({ + required this.id, + required this.content, + required this.senderId, + required this.sentAt, + this.isFromAI = false, + required this.status, + }); +} + +enum MessageStatus { + sent, + delivered, + read, +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/event_model.dart b/frontend/lib/models/m_dashbord/event_model.dart new file mode 100644 index 0000000..39702ed --- /dev/null +++ b/frontend/lib/models/m_dashbord/event_model.dart @@ -0,0 +1,66 @@ +class EventModel { + final String id; + final String title; + final String? description; + final DateTime startDate; + final DateTime? endDate; + final EventType type; + final EventStatus status; + final String? childId; + final String? assistantId; + final String? createdBy; + + EventModel({ + required this.id, + required this.title, + this.description, + required this.startDate, + this.endDate, + required this.type, + required this.status, + this.childId, + this.assistantId, + this.createdBy, + }); + + factory EventModel.fromJson(Map json) { + return EventModel( + id: json['id'], + title: json['title'], + description: json['description'], + startDate: DateTime.parse(json['startDate']), + endDate: json['endDate'] != null ? DateTime.parse(json['endDate']) : null, + type: EventType.values.byName(json['type']), + status: EventStatus.values.byName(json['status']), + childId: json['childId'], + assistantId: json['assistantId'], + createdBy: json['createdBy'], + ); + } + + bool get isMultiDay => endDate != null && !isSameDay(startDate, endDate!); + bool get isPending => status == EventStatus.pending; + bool get needsConfirmation => isPending && createdBy != 'current_user'; + + static bool isSameDay(DateTime date1, DateTime date2) { + return date1.year == date2.year && + date1.month == date2.month && + date1.day == date2.day; + } +} + +enum EventType { + parentVacation, // Vacances parents + childAbsence, // Absence enfant + rpeActivity, // Activité RPE + assistantVacation, // Congés assistante maternelle + sickLeave, // Arrêt maladie + personalNote, // Note personnelle +} + +enum EventStatus { + confirmed, + pending, + refused, + cancelled, +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/notification_model.dart b/frontend/lib/models/m_dashbord/notification_model.dart new file mode 100644 index 0000000..09d1ecf --- /dev/null +++ b/frontend/lib/models/m_dashbord/notification_model.dart @@ -0,0 +1,42 @@ +class NotificationModel { + final String id; + final String title; + final String content; + final NotificationType type; + final DateTime createdAt; + final bool isRead; + final String? actionUrl; + final Map? metadata; + + NotificationModel({ + required this.id, + required this.title, + required this.content, + required this.type, + required this.createdAt, + this.isRead = false, + this.actionUrl, + this.metadata, + }); + + factory NotificationModel.fromJson(Map json) { + return NotificationModel( + id: json['id'], + title: json['title'], + content: json['content'], + type: NotificationType.values.byName(json['type']), + createdAt: DateTime.parse(json['createdAt']), + isRead: json['isRead'] ?? false, + actionUrl: json['actionUrl'], + metadata: json['metadata'], + ); + } +} + +enum NotificationType { + newEvent, // Nouvel événement + fileModified, // Dossier modifié + contractPending, // Contrat en attente + paymentPending, // Paiement en attente + unreadMessage, // Message non lu +} \ No newline at end of file diff --git a/frontend/lib/models/nanny_registration_data.dart b/frontend/lib/models/nanny_registration_data.dart new file mode 100644 index 0000000..2e92854 --- /dev/null +++ b/frontend/lib/models/nanny_registration_data.dart @@ -0,0 +1,136 @@ +import 'package:flutter/foundation.dart'; + +class NannyRegistrationData extends ChangeNotifier { + // Step 1: Identity Info + String firstName = ''; + String lastName = ''; + String streetAddress = ''; // Nouveau pour N° et Rue + String postalCode = ''; // Nouveau + String city = ''; // Nouveau + String phone = ''; + String email = ''; + String password = ''; + // String? photoPath; // Déplacé ou géré à l'étape 2 + // bool photoConsent = false; // Déplacé ou géré à l'étape 2 + + // Step 2: Professional Info + String? photoPath; // Ajouté pour l'étape 2 + bool photoConsent = false; // Ajouté pour l'étape 2 + DateTime? dateOfBirth; + String birthCity = ''; // Nouveau + String birthCountry = ''; // Nouveau + // String placeOfBirth = ''; // Remplacé par birthCity et birthCountry + String nir = ''; // Numéro de Sécurité Sociale + String agrementNumber = ''; // Numéro d'agrément + int? capacity; // Number of children the nanny can look after + + // Step 3: Presentation & CGU + String presentationText = ''; + bool cguAccepted = false; + + // --- Methods to update data and notify listeners --- + + void updateIdentityInfo({ + String? firstName, + String? lastName, + String? streetAddress, // Modifié + String? postalCode, // Nouveau + String? city, // Nouveau + String? phone, + String? email, + String? password, + }) { + this.firstName = firstName ?? this.firstName; + this.lastName = lastName ?? this.lastName; + this.streetAddress = streetAddress ?? this.streetAddress; // Modifié + this.postalCode = postalCode ?? this.postalCode; // Nouveau + this.city = city ?? this.city; // Nouveau + this.phone = phone ?? this.phone; + this.email = email ?? this.email; + this.password = password ?? this.password; + // if (photoPath != null || this.photoPath != null) { // Supprimé de l'étape 1 + // this.photoPath = photoPath; + // } + // this.photoConsent = photoConsent ?? this.photoConsent; // Supprimé de l'étape 1 + notifyListeners(); + } + + void updateProfessionalInfo({ + String? photoPath, + bool? photoConsent, + DateTime? dateOfBirth, + String? birthCity, // Nouveau + String? birthCountry, // Nouveau + // String? placeOfBirth, // Remplacé + String? nir, + String? agrementNumber, + int? capacity, + }) { + // Allow setting photoPath to null explicitly + if (photoPath != null || this.photoPath != null) { + this.photoPath = photoPath; + } + this.photoConsent = photoConsent ?? this.photoConsent; + this.dateOfBirth = dateOfBirth ?? this.dateOfBirth; + this.birthCity = birthCity ?? this.birthCity; // Nouveau + this.birthCountry = birthCountry ?? this.birthCountry; // Nouveau + // this.placeOfBirth = placeOfBirth ?? this.placeOfBirth; // Remplacé + this.nir = nir ?? this.nir; + this.agrementNumber = agrementNumber ?? this.agrementNumber; + this.capacity = capacity ?? this.capacity; + notifyListeners(); + } + + void updatePresentationAndCgu({ + String? presentationText, + bool? cguAccepted, + }) { + this.presentationText = presentationText ?? this.presentationText; + this.cguAccepted = cguAccepted ?? this.cguAccepted; + notifyListeners(); + } + + // --- Getters for validation or display --- + bool get isStep1Complete => + firstName.isNotEmpty && + lastName.isNotEmpty && + streetAddress.isNotEmpty && // Modifié + postalCode.isNotEmpty && // Nouveau + city.isNotEmpty && // Nouveau + phone.isNotEmpty && + email.isNotEmpty && + password.isNotEmpty; + + bool get isStep2Complete => + // photoConsent is mandatory if a photo is system-required, otherwise optional. + // For now, let's assume if photoPath is present, consent should ideally be true. + // Or, make consent always mandatory if photo section exists. + // Based on new mockup, photo is present, so consent might be implicitly or explicitly needed. + (photoPath != null ? photoConsent == true : true) && // Ajuster selon la logique de consentement désirée + dateOfBirth != null && + birthCity.isNotEmpty && + birthCountry.isNotEmpty && + nir.isNotEmpty && // Basic check, could add validation + agrementNumber.isNotEmpty && + capacity != null && capacity! > 0; + + bool get isStep3Complete => + // presentationText is optional as per CDC (message au gestionnaire) + cguAccepted; + + bool get isRegistrationComplete => + isStep1Complete && isStep2Complete && isStep3Complete; + + @override + String toString() { + return 'NannyRegistrationData(' + 'firstName: $firstName, lastName: $lastName, ' + 'streetAddress: $streetAddress, postalCode: $postalCode, city: $city, ' + 'phone: $phone, email: $email, ' + // 'photoPath: $photoPath, photoConsent: $photoConsent, ' // Commenté car déplacé/modifié + 'dateOfBirth: $dateOfBirth, birthCity: $birthCity, birthCountry: $birthCountry, ' + 'nir: $nir, agrementNumber: $agrementNumber, capacity: $capacity, ' + 'photoPath (step2): $photoPath, photoConsent (step2): $photoConsent, ' + 'presentationText: $presentationText, cguAccepted: $cguAccepted)'; + } +} \ No newline at end of file diff --git a/frontend/lib/models/parent_user_registration_data.dart b/frontend/lib/models/parent_user_registration_data.dart new file mode 100644 index 0000000..d2c6954 --- /dev/null +++ b/frontend/lib/models/parent_user_registration_data.dart @@ -0,0 +1,97 @@ +import 'dart:io'; // Pour File +import '../models/card_assets.dart'; // Import de l'enum CardColorVertical + +class ParentData { + String firstName; + String lastName; + String address; // Rue et numéro + String postalCode; // Ajout + String city; // Ajout + String phone; + String email; + String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si + File? profilePicture; // Chemin ou objet File + + ParentData({ + this.firstName = '', + this.lastName = '', + this.address = '', // Rue + this.postalCode = '', // Ajout + this.city = '', // Ajout + this.phone = '', + this.email = '', + this.password = '', + this.profilePicture, + }); +} + +class ChildData { + String firstName; + String lastName; + String dob; // Date de naissance ou prévisionnelle + bool photoConsent; + bool multipleBirth; + bool isUnbornChild; + File? imageFile; + CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte + + ChildData({ + this.firstName = '', + this.lastName = '', + this.dob = '', + this.photoConsent = false, + this.multipleBirth = false, + this.isUnbornChild = false, + this.imageFile, + required this.cardColor, // Rendre requis dans le constructeur + }); +} + +class UserRegistrationData { + ParentData parent1; + ParentData? parent2; // Optionnel + List children; + String motivationText; + bool cguAccepted; + + UserRegistrationData({ + ParentData? parent1Data, + this.parent2, + List? childrenData, + this.motivationText = '', + this.cguAccepted = false, + }) : parent1 = parent1Data ?? ParentData(), + children = childrenData ?? []; + + // Méthode pour ajouter/mettre à jour le parent 1 + void updateParent1(ParentData data) { + parent1 = data; + } + + // Méthode pour ajouter/mettre à jour le parent 2 + void updateParent2(ParentData? data) { + parent2 = data; + } + + // Méthode pour ajouter un enfant + void addChild(ChildData child) { + children.add(child); + } + + // Méthode pour mettre à jour un enfant (si nécessaire plus tard) + void updateChild(int index, ChildData child) { + if (index >= 0 && index < children.length) { + children[index] = child; + } + } + + // Mettre à jour la motivation + void updateMotivation(String text) { + motivationText = text; + } + + // Accepter les CGU + void acceptCGU() { + cguAccepted = true; + } +} \ No newline at end of file diff --git a/frontend/lib/models/user_registration_data.dart b/frontend/lib/models/user_registration_data.dart index d2c6954..9687898 100644 --- a/frontend/lib/models/user_registration_data.dart +++ b/frontend/lib/models/user_registration_data.dart @@ -1,5 +1,8 @@ import 'dart:io'; // Pour File import '../models/card_assets.dart'; // Import de l'enum CardColorVertical +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +// import 'package:p_tits_pas/models/child.dart'; // Commenté car fichier non trouvé class ParentData { String firstName; @@ -47,12 +50,28 @@ class ChildData { }); } -class UserRegistrationData { +// Nouvelle classe pour les détails bancaires +class BankDetails { + String bankName; + String iban; + String bic; + + BankDetails({ + this.bankName = '', + this.iban = '', + this.bic = '', + }); +} + +class UserRegistrationData extends ChangeNotifier { ParentData parent1; ParentData? parent2; // Optionnel List children; String motivationText; bool cguAccepted; + BankDetails? bankDetails; // Ajouté + String attestationCafNumber; // Ajouté + bool consentQuotientFamilial; // Ajouté UserRegistrationData({ ParentData? parent1Data, @@ -60,38 +79,77 @@ class UserRegistrationData { List? childrenData, this.motivationText = '', this.cguAccepted = false, + this.bankDetails, // Ajouté + this.attestationCafNumber = '', // Ajouté + this.consentQuotientFamilial = false, // Ajouté }) : parent1 = parent1Data ?? ParentData(), children = childrenData ?? []; // Méthode pour ajouter/mettre à jour le parent 1 void updateParent1(ParentData data) { parent1 = data; + notifyListeners(); // Notifier les changements } // Méthode pour ajouter/mettre à jour le parent 2 void updateParent2(ParentData? data) { parent2 = data; + notifyListeners(); } // Méthode pour ajouter un enfant void addChild(ChildData child) { children.add(child); + notifyListeners(); } // Méthode pour mettre à jour un enfant (si nécessaire plus tard) void updateChild(int index, ChildData child) { if (index >= 0 && index < children.length) { children[index] = child; + notifyListeners(); + } + } + + // Méthode pour supprimer un enfant + void removeChild(int index) { + if (index >= 0 && index < children.length) { + children.removeAt(index); + notifyListeners(); } } // Mettre à jour la motivation void updateMotivation(String text) { motivationText = text; + notifyListeners(); + } + + // Mettre à jour les informations bancaires et CAF + void updateFinancialInfo({ + BankDetails? bankDetails, + String? attestationCafNumber, + bool? consentQuotientFamilial, + }) { + if (bankDetails != null) this.bankDetails = bankDetails; + if (attestationCafNumber != null) this.attestationCafNumber = attestationCafNumber; + if (consentQuotientFamilial != null) this.consentQuotientFamilial = consentQuotientFamilial; + notifyListeners(); } // Accepter les CGU - void acceptCGU() { - cguAccepted = true; + void acceptCGU(bool accepted) { // Prend un booléen + cguAccepted = accepted; + notifyListeners(); + } + + // Méthode pour vérifier si toutes les données requises sont là (simplifié) + bool isRegistrationComplete() { + // Ajouter ici les validations nécessaires + // Exemple : parent1 doit avoir des champs remplis, au moins un enfant, CGU acceptées + return parent1.firstName.isNotEmpty && + parent1.lastName.isNotEmpty && + children.isNotEmpty && + cguAccepted; } } \ No newline at end of file diff --git a/frontend/lib/navigation/app_router.dart b/frontend/lib/navigation/app_router.dart deleted file mode 100644 index 70b55a2..0000000 --- a/frontend/lib/navigation/app_router.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import '../screens/auth/login_screen.dart'; -import '../screens/auth/register_choice_screen.dart'; -import '../screens/auth/parent_register_step1_screen.dart'; -import '../screens/auth/parent_register_step2_screen.dart'; -import '../screens/auth/parent_register_step3_screen.dart'; -import '../screens/auth/parent_register_step4_screen.dart'; -import '../screens/auth/parent_register_step5_screen.dart'; -import '../screens/home/home_screen.dart'; -import '../models/user_registration_data.dart'; - -class AppRouter { - static const String login = '/login'; - static const String registerChoice = '/register-choice'; - static const String parentRegisterStep1 = '/parent-register/step1'; - static const String parentRegisterStep2 = '/parent-register/step2'; - static const String parentRegisterStep3 = '/parent-register/step3'; - static const String parentRegisterStep4 = '/parent-register/step4'; - static const String parentRegisterStep5 = '/parent-register/step5'; - static const String home = '/home'; - - static Route generateRoute(RouteSettings settings) { - Widget screen; - bool slideTransition = false; - Object? args = settings.arguments; - - Widget buildErrorScreen(String step) { - print("Erreur: Données UserRegistrationData manquantes ou de mauvais type pour l'étape $step"); - return const ParentRegisterStep1Screen(); - } - - switch (settings.name) { - case login: - screen = const LoginPage(); - break; - case registerChoice: - screen = const RegisterChoiceScreen(); - slideTransition = true; - break; - case parentRegisterStep1: - screen = const ParentRegisterStep1Screen(); - slideTransition = true; - break; - case parentRegisterStep2: - if (args is UserRegistrationData) { - screen = ParentRegisterStep2Screen(registrationData: args); - } else { - screen = buildErrorScreen('2'); - } - slideTransition = true; - break; - case parentRegisterStep3: - if (args is UserRegistrationData) { - screen = ParentRegisterStep3Screen(registrationData: args); - } else { - screen = buildErrorScreen('3'); - } - slideTransition = true; - break; - case parentRegisterStep4: - if (args is UserRegistrationData) { - screen = ParentRegisterStep4Screen(registrationData: args); - } else { - screen = buildErrorScreen('4'); - } - slideTransition = true; - break; - case parentRegisterStep5: - if (args is UserRegistrationData) { - screen = ParentRegisterStep5Screen(registrationData: args); - } else { - screen = buildErrorScreen('5'); - } - slideTransition = true; - break; - case home: - screen = const HomeScreen(); - break; - default: - screen = Scaffold( - body: Center( - child: Text('Route non définie : ${settings.name}'), - ), - ); - } - - if (slideTransition) { - return PageRouteBuilder( - pageBuilder: (context, animation, secondaryAnimation) => screen, - transitionsBuilder: (context, animation, secondaryAnimation, child) { - const begin = Offset(1.0, 0.0); - const end = Offset.zero; - const curve = Curves.easeInOut; - var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); - var offsetAnimation = animation.drive(tween); - return SlideTransition(position: offsetAnimation, child: child); - }, - transitionDuration: const Duration(milliseconds: 400), - ); - } else { - return MaterialPageRoute(builder: (_) => screen); - } - } -} \ No newline at end of file diff --git a/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart new file mode 100644 index 0000000..d10062e --- /dev/null +++ b/frontend/lib/screens/administrateurs/admin_dashboardScreen.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart'; +import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart'; +import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart'; +import 'package:p_tits_pas/widgets/app_footer.dart'; +import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart'; + +class AdminDashboardScreen extends StatefulWidget { + const AdminDashboardScreen({super.key}); + + @override + _AdminDashboardScreenState createState() => _AdminDashboardScreenState(); +} + +class _AdminDashboardScreenState extends State { + int selectedIndex = 0; + + void onTabChange(int index) { + setState(() { + selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(60.0), + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey.shade300), + ), + ), + child: DashboardAppBarAdmin( + selectedIndex: selectedIndex, + onTabChange: onTabChange, + ), + ), + ), + body: Column( + children: [ + Expanded( + child: _getBody(), + ), + const AppFooter(), + ], + ), + ); + } + + Widget _getBody() { + switch (selectedIndex) { + case 0: + return const GestionnaireManagementWidget(); + case 1: + return const ParentManagementWidget(); + case 2: + return const AssistanteMaternelleManagementWidget(); + case 3: + return const Center(child: Text("👨‍💼 Administrateurs")); + default: + return const Center(child: Text("Page non trouvée")); + } + } +} diff --git a/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart new file mode 100644 index 0000000..de00a86 --- /dev/null +++ b/frontend/lib/screens/administrateurs/creation/gestionnaires_create.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class GestionnairesCreate extends StatelessWidget { + const GestionnairesCreate({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Créer un gestionnaire'), + ), + body: const Center( + child: Text('Formulaire de création de gestionnaire'), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/am_register_step1_screen.dart b/frontend/lib/screens/auth/am_register_step1_screen.dart new file mode 100644 index 0000000..06a8fa2 --- /dev/null +++ b/frontend/lib/screens/auth/am_register_step1_screen.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; + +import '../../models/am_registration_data.dart'; +import '../../utils/data_generator.dart'; +import '../../widgets/personal_info_form_screen.dart'; +import '../../models/card_assets.dart'; + +class AmRegisterStep1Screen extends StatelessWidget { + const AmRegisterStep1Screen({super.key}); + + @override + Widget build(BuildContext context) { + final registrationData = Provider.of(context, listen: false); + + // Générer des données de test si vide + PersonalInfoData initialData; + if (registrationData.firstName.isEmpty) { + final genFirstName = DataGenerator.firstName(); + final genLastName = DataGenerator.lastName(); + initialData = PersonalInfoData( + firstName: genFirstName, + lastName: genLastName, + phone: DataGenerator.phone(), + email: DataGenerator.email(genFirstName, genLastName), + address: DataGenerator.address(), + postalCode: DataGenerator.postalCode(), + city: DataGenerator.city(), + ); + } else { + initialData = PersonalInfoData( + firstName: registrationData.firstName, + lastName: registrationData.lastName, + phone: registrationData.phone, + email: registrationData.email, + address: registrationData.streetAddress, + postalCode: registrationData.postalCode, + city: registrationData.city, + ); + } + + return PersonalInfoFormScreen( + stepText: 'Étape 1/4', + title: 'Vos informations personnelles', + cardColor: CardColorHorizontal.blue, + initialData: initialData, + previousRoute: '/register-choice', + onSubmit: (data, {hasSecondPerson, sameAddress}) { + registrationData.updateIdentityInfo( + firstName: data.firstName, + lastName: data.lastName, + phone: data.phone, + email: data.email, + streetAddress: data.address, + postalCode: data.postalCode, + city: data.city, + password: '', + ); + context.go('/am-register-step2'); + }, + ); + } +} diff --git a/frontend/lib/screens/auth/am_register_step2_screen.dart b/frontend/lib/screens/auth/am_register_step2_screen.dart new file mode 100644 index 0000000..1496c6f --- /dev/null +++ b/frontend/lib/screens/auth/am_register_step2_screen.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'dart:io'; + +import '../../models/am_registration_data.dart'; +import '../../models/card_assets.dart'; +import '../../utils/data_generator.dart'; +import '../../widgets/professional_info_form_screen.dart'; + +class AmRegisterStep2Screen extends StatefulWidget { + const AmRegisterStep2Screen({super.key}); + + @override + State createState() => _AmRegisterStep2ScreenState(); +} + +class _AmRegisterStep2ScreenState extends State { + String? _photoPathFramework; + File? _photoFile; + + Future _pickPhoto() async { + // TODO: Remplacer par la vraie logique ImagePicker + // final imagePicker = ImagePicker(); + // final pickedFile = await imagePicker.pickImage(source: ImageSource.gallery); + // if (pickedFile != null) { + // setState(() { + // _photoFile = File(pickedFile.path); + // _photoPathFramework = pickedFile.path; + // }); + // } else { + setState(() { + _photoPathFramework = 'assets/images/icon_assmat.png'; + _photoFile = null; + }); + // } + print("Photo sélectionnée: $_photoPathFramework"); + } + + @override + Widget build(BuildContext context) { + final registrationData = Provider.of(context, listen: false); + + // Préparer les données initiales + ProfessionalInfoData initialData = ProfessionalInfoData( + photoPath: registrationData.photoPath, + photoConsent: registrationData.photoConsent, + dateOfBirth: registrationData.dateOfBirth, + birthCity: registrationData.birthCity, + birthCountry: registrationData.birthCountry, + nir: registrationData.nir, + agrementNumber: registrationData.agrementNumber, + capacity: registrationData.capacity, + ); + + // Générer des données de test si les champs sont vides + if (registrationData.dateOfBirth == null && registrationData.nir.isEmpty) { + initialData = ProfessionalInfoData( + photoPath: 'assets/images/icon_assmat.png', + photoConsent: true, + dateOfBirth: DateTime(1985, 3, 15), + birthCity: DataGenerator.city(), + birthCountry: 'France', + nir: '${DataGenerator.randomIntInRange(1, 3)}${DataGenerator.randomIntInRange(80, 96)}${DataGenerator.randomIntInRange(1, 13).toString().padLeft(2, '0')}${DataGenerator.randomIntInRange(1, 100).toString().padLeft(2, '0')}${DataGenerator.randomIntInRange(100, 1000).toString().padLeft(3, '0')}${DataGenerator.randomIntInRange(100, 1000).toString().padLeft(3, '0')}${DataGenerator.randomIntInRange(10, 100).toString().padLeft(2, '0')}', + agrementNumber: 'AM${DataGenerator.randomIntInRange(10000, 100000)}', + capacity: DataGenerator.randomIntInRange(1, 5), + ); + } + + return ProfessionalInfoFormScreen( + stepText: 'Étape 2/4', + title: 'Vos informations professionnelles', + cardColor: CardColorHorizontal.green, + initialData: initialData, + previousRoute: '/am-register-step1', + onPickPhoto: _pickPhoto, + onSubmit: (data) { + registrationData.updateProfessionalInfo( + photoPath: _photoPathFramework ?? data.photoPath, + photoConsent: data.photoConsent, + dateOfBirth: data.dateOfBirth, + birthCity: data.birthCity, + birthCountry: data.birthCountry, + nir: data.nir, + agrementNumber: data.agrementNumber, + capacity: data.capacity, + ); + context.go('/am-register-step3'); + }, + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/am_register_step3_screen.dart b/frontend/lib/screens/auth/am_register_step3_screen.dart new file mode 100644 index 0000000..1fff3cb --- /dev/null +++ b/frontend/lib/screens/auth/am_register_step3_screen.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; + +import '../../models/am_registration_data.dart'; +import '../../widgets/presentation_form_screen.dart'; +import '../../models/card_assets.dart'; + +class AmRegisterStep3Screen extends StatelessWidget { + const AmRegisterStep3Screen({super.key}); + + @override + Widget build(BuildContext context) { + final data = Provider.of(context, listen: false); + + // Générer un texte de test si vide + String initialText = data.presentationText; + bool initialCgu = data.cguAccepted; + + if (initialText.isEmpty) { + initialText = 'Disponible immédiatement, plus de 10 ans d\'expérience avec les tout-petits. Formation aux premiers secours à jour. Je dispose d\'un jardin sécurisé et d\'un espace de jeu adapté.'; + initialCgu = true; + } + + return PresentationFormScreen( + stepText: 'Étape 3/4', + title: 'Présentation et Conditions', + cardColor: CardColorHorizontal.peach, + textFieldHint: 'Ex: Disponible immédiatement, 10 ans d\'expérience, formation premiers secours...', + initialText: initialText, + initialCguAccepted: initialCgu, + previousRoute: '/am-register-step2', + onSubmit: (text, cguAccepted) { + data.updatePresentationAndCgu( + presentationText: text, + cguAccepted: cguAccepted, + ); + context.go('/am-register-step4'); + }, + ); + } +} diff --git a/frontend/lib/screens/auth/am_register_step4_screen.dart b/frontend/lib/screens/auth/am_register_step4_screen.dart new file mode 100644 index 0000000..8ed3b36 --- /dev/null +++ b/frontend/lib/screens/auth/am_register_step4_screen.dart @@ -0,0 +1,339 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../models/am_registration_data.dart'; +import '../../widgets/image_button.dart'; +import '../../models/card_assets.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; + +// Méthode helper pour afficher un champ de type "lecture seule" stylisé +Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) { + const FontWeight labelFontWeight = FontWeight.w600; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: multiLine ? null : fieldHeight, + constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, + padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/input_field_bg.png'), + fit: BoxFit.fill, + ), + ), + child: Text( + value.isNotEmpty ? value : '-', + style: GoogleFonts.merienda(fontSize: labelFontSize), + maxLines: multiLine ? null : 1, + overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis, + ), + ), + ], + ); +} + +class AmRegisterStep4Screen extends StatefulWidget { + const AmRegisterStep4Screen({super.key}); + + @override + _AmRegisterStep4ScreenState createState() => _AmRegisterStep4ScreenState(); +} + +class _AmRegisterStep4ScreenState extends State { + @override + Widget build(BuildContext context) { + final registrationData = Provider.of(context); + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Étape 4/4', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 20), + Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), + const SizedBox(height: 30), + + _buildPersonalInfoCard(context, registrationData), + const SizedBox(height: 20), + _buildProfessionalInfoCard(context, registrationData), + const SizedBox(height: 20), + _buildPresentationCard(context, registrationData), + const SizedBox(height: 40), + + ImageButton( + bg: 'assets/images/btn_green.png', + text: 'Soumettre ma demande', + textColor: const Color(0xFF2D6A4F), + width: 350, + height: 50, + fontSize: 18, + onPressed: () { + print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}"); + _showConfirmationModal(context); + }, + ), + ], + ), + ), + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/am-register-step3'); + } + }, + tooltip: 'Retour', + ), + ), + ], + ), + ); + } + + void _showConfirmationModal(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text( + 'Demande enregistrée', + style: GoogleFonts.merienda(fontWeight: FontWeight.bold), + ), + content: Text( + 'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.', + style: GoogleFonts.merienda(fontSize: 14), + ), + actions: [ + TextButton( + child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)), + onPressed: () { + Navigator.of(dialogContext).pop(); + context.go('/login'); + }, + ), + ], + ); + }, + ); + } + + // Carte Informations personnelles + Widget _buildPersonalInfoCard(BuildContext context, AmRegistrationData data) { + const double verticalSpacing = 28.0; + const double labelFontSize = 22.0; + + List details = [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)), + const SizedBox(width: 20), + Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)), + ], + ), + const SizedBox(height: verticalSpacing), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)), + const SizedBox(width: 20), + Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)), + ], + ), + const SizedBox(height: verticalSpacing), + _buildDisplayFieldValue(context, "Adresse:", "${data.streetAddress}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize), + const SizedBox(height: verticalSpacing), + _buildDisplayFieldValue(context, "Consentement photo:", data.photoConsent ? "Oui" : "Non", labelFontSize: labelFontSize), + ]; + + return _SummaryCard( + backgroundImagePath: CardColorHorizontal.blue.path, + title: 'Informations personnelles', + content: details, + onEdit: () => context.go('/am-register-step1'), + ); + } + + // Carte Informations professionnelles + Widget _buildProfessionalInfoCard(BuildContext context, AmRegistrationData data) { + const double verticalSpacing = 28.0; + const double labelFontSize = 22.0; + + String formattedDate = '-'; + if (data.dateOfBirth != null) { + formattedDate = '${data.dateOfBirth!.day.toString().padLeft(2, '0')}/${data.dateOfBirth!.month.toString().padLeft(2, '0')}/${data.dateOfBirth!.year}'; + } + String birthPlace = '${data.birthCity}, ${data.birthCountry}'.trim(); + if (birthPlace == ',') birthPlace = '-'; + + List details = [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDisplayFieldValue(context, "Date de naissance:", formattedDate, labelFontSize: labelFontSize)), + const SizedBox(width: 20), + Expanded(child: _buildDisplayFieldValue(context, "Lieu de naissance:", birthPlace, labelFontSize: labelFontSize, multiLine: true)), + ], + ), + const SizedBox(height: verticalSpacing), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDisplayFieldValue(context, "N° Sécurité Sociale:", data.nir, labelFontSize: labelFontSize)), + const SizedBox(width: 20), + Expanded(child: _buildDisplayFieldValue(context, "N° Agrément:", data.agrementNumber, labelFontSize: labelFontSize)), + ], + ), + const SizedBox(height: verticalSpacing), + _buildDisplayFieldValue(context, "Capacité d'accueil:", data.capacity?.toString() ?? '-', labelFontSize: labelFontSize), + ]; + + return _SummaryCard( + backgroundImagePath: CardColorHorizontal.green.path, + title: 'Informations professionnelles', + content: details, + onEdit: () => context.go('/am-register-step2'), + ); + } + + // Carte Présentation & CGU + Widget _buildPresentationCard(BuildContext context, AmRegistrationData data) { + const double labelFontSize = 22.0; + + List details = [ + Text( + 'Votre présentation (facultatif) :', + style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 8), + Container( + width: double.infinity, + constraints: const BoxConstraints(minHeight: 80.0), + padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/input_field_bg.png'), + fit: BoxFit.fill, + ), + ), + child: Text( + data.presentationText.isNotEmpty ? data.presentationText : 'Aucune présentation rédigée.', + style: GoogleFonts.merienda( + fontSize: 18, + fontStyle: data.presentationText.isNotEmpty ? FontStyle.normal : FontStyle.italic, + color: data.presentationText.isNotEmpty ? Colors.black87 : Colors.black54, + ), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Icon( + data.cguAccepted ? Icons.check_circle : Icons.cancel, + color: data.cguAccepted ? Colors.green : Colors.red, + size: 24, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + data.cguAccepted ? 'CGU acceptées' : 'CGU non acceptées', + style: GoogleFonts.merienda(fontSize: 18), + ), + ), + ], + ), + ]; + + return _SummaryCard( + backgroundImagePath: CardColorHorizontal.peach.path, + title: 'Présentation & CGU', + content: details, + onEdit: () => context.go('/am-register-step3'), + ); + } +} + +// Widget générique _SummaryCard +class _SummaryCard extends StatelessWidget { + final String backgroundImagePath; + final String title; + final List content; + final VoidCallback onEdit; + + const _SummaryCard({ + super.key, + required this.backgroundImagePath, + required this.title, + required this.content, + required this.onEdit, + }); + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 2.0, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(backgroundImagePath), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.center, + child: Text( + title, + style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), + ), + ), + const SizedBox(height: 12), + ...content, + ], + ), + ), + IconButton( + icon: const Icon(Icons.edit, color: Colors.black54, size: 28), + onPressed: onEdit, + tooltip: 'Modifier', + ), + ], + ), + ), + ); + } +} diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart index f6c8a8f..6f3cc9c 100644 --- a/frontend/lib/screens/auth/login_screen.dart +++ b/frontend/lib/screens/auth/login_screen.dart @@ -8,14 +8,14 @@ import 'package:go_router/go_router.dart'; import '../../widgets/image_button.dart'; import '../../widgets/custom_app_text_field.dart'; -class LoginPage extends StatefulWidget { - const LoginPage({super.key}); +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); @override - State createState() => _LoginPageState(); + State createState() => _LoginPageState(); } -class _LoginPageState extends State { +class _LoginPageState extends State { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); @@ -168,12 +168,12 @@ class _LoginPageState extends State { ), ), ), - const SizedBox(height: 10), - // Lien de création de compte + const SizedBox(height: 20), + // Lien de création de compte (version originale) Center( child: TextButton( onPressed: () { - Navigator.pushNamed(context, '/register-choice'); + context.go('/register-choice'); }, child: Text( 'Créer un compte', @@ -185,7 +185,6 @@ class _LoginPageState extends State { ), ), ), - const SizedBox(height: 20), // Réduit l'espacement en bas ], ), ), @@ -234,13 +233,13 @@ class _LoginPageState extends State { _FooterLink( text: 'Mentions légales', onTap: () { - Navigator.pushNamed(context, '/legal'); + context.go('/legal'); }, ), _FooterLink( text: 'Politique de confidentialité', onTap: () { - Navigator.pushNamed(context, '/privacy'); + context.go('/privacy'); }, ), ], diff --git a/frontend/lib/screens/auth/nanny_register_confirmation_screen.dart b/frontend/lib/screens/auth/nanny_register_confirmation_screen.dart new file mode 100644 index 0000000..fbad99b --- /dev/null +++ b/frontend/lib/screens/auth/nanny_register_confirmation_screen.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class NannyRegisterConfirmationScreen extends StatelessWidget { + const NannyRegisterConfirmationScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Inscription Soumise'), + automaticallyImplyLeading: false, // Remove back button + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.check_circle_outline, color: Colors.green, size: 80), + const SizedBox(height: 20), + const Text( + 'Votre demande d\'inscription a été soumise avec succès !', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 15), + const Text( + 'Votre compte est en attente de validation par un gestionnaire. Vous recevrez une notification par e-mail une fois votre compte activé.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: () { + // Navigate back to the login screen + context.go('/login'); + }, + child: const Text('Retour à la connexion'), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step1_screen.dart b/frontend/lib/screens/auth/parent_register_step1_screen.dart index 0463967..bfabab3 100644 --- a/frontend/lib/screens/auth/parent_register_step1_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step1_screen.dart @@ -1,226 +1,65 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'dart:math' as math; // Pour la rotation du chevron -import '../../models/user_registration_data.dart'; // Import du modèle de données -import '../../utils/data_generator.dart'; // Import du générateur de données -import '../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField -import '../../models/card_assets.dart'; // Import des enums de cartes +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; -class ParentRegisterStep1Screen extends StatefulWidget { +import '../../models/user_registration_data.dart'; +import '../../utils/data_generator.dart'; +import '../../widgets/personal_info_form_screen.dart'; +import '../../models/card_assets.dart'; + +class ParentRegisterStep1Screen extends StatelessWidget { const ParentRegisterStep1Screen({super.key}); - @override - State createState() => _ParentRegisterStep1ScreenState(); -} - -class _ParentRegisterStep1ScreenState extends State { - final _formKey = GlobalKey(); - late UserRegistrationData _registrationData; - - // Contrôleurs pour les champs (restauration CP et Ville) - final _lastNameController = TextEditingController(); - final _firstNameController = TextEditingController(); - final _phoneController = TextEditingController(); - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - final _confirmPasswordController = TextEditingController(); - final _addressController = TextEditingController(); // Rue seule - final _postalCodeController = TextEditingController(); // Restauré - final _cityController = TextEditingController(); // Restauré - - @override - void initState() { - super.initState(); - _registrationData = UserRegistrationData(); - _generateAndFillData(); - } - - void _generateAndFillData() { - final String genFirstName = DataGenerator.firstName(); - final String genLastName = DataGenerator.lastName(); - - // Utilisation des méthodes publiques de DataGenerator - _addressController.text = DataGenerator.address(); - _postalCodeController.text = DataGenerator.postalCode(); - _cityController.text = DataGenerator.city(); - - _firstNameController.text = genFirstName; - _lastNameController.text = genLastName; - _phoneController.text = DataGenerator.phone(); - _emailController.text = DataGenerator.email(genFirstName, genLastName); - _passwordController.text = DataGenerator.password(); - _confirmPasswordController.text = _passwordController.text; - } - - @override - void dispose() { - _lastNameController.dispose(); - _firstNameController.dispose(); - _phoneController.dispose(); - _emailController.dispose(); - _passwordController.dispose(); - _confirmPasswordController.dispose(); - _addressController.dispose(); - _postalCodeController.dispose(); - _cityController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; + final registrationData = Provider.of(context, listen: false); + final parent1 = registrationData.parent1; + + // Générer des données de test si vide + PersonalInfoData initialData; + if (parent1.firstName.isEmpty) { + final genFirstName = DataGenerator.firstName(); + final genLastName = DataGenerator.lastName(); + initialData = PersonalInfoData( + firstName: genFirstName, + lastName: genLastName, + phone: DataGenerator.phone(), + email: DataGenerator.email(genFirstName, genLastName), + address: DataGenerator.address(), + postalCode: DataGenerator.postalCode(), + city: DataGenerator.city(), + ); + } else { + initialData = PersonalInfoData( + firstName: parent1.firstName, + lastName: parent1.lastName, + phone: parent1.phone, + email: parent1.email, + address: parent1.address, + postalCode: parent1.postalCode, + city: parent1.city, + ); + } - return Scaffold( - body: Stack( - children: [ - // Fond papier - Positioned.fill( - child: Image.asset( - 'assets/images/paper2.png', - fit: BoxFit.cover, - repeat: ImageRepeat.repeat, - ), - ), - - // Contenu centré - Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Indicateur d'étape (à rendre dynamique) - Text( - 'Étape 1/5', - style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), - ), - const SizedBox(height: 10), - // Texte d'instruction - Text( - 'Informations du Parent Principal', - style: GoogleFonts.merienda( - fontSize: 24, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - - // Carte jaune contenant le formulaire - Container( - width: screenSize.width * 0.6, - padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50), - constraints: const BoxConstraints(minHeight: 570), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(CardColorHorizontal.peach.path), - fit: BoxFit.fill, - ), - ), - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - Expanded(flex: 1, child: const SizedBox()), // Espace de 4% - Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - ], - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - Expanded(flex: 1, child: const SizedBox()), // Espace de 4% - Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - ], - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) { - if (value == null || value.isEmpty) return 'Mot de passe requis'; - if (value.length < 6) return '6 caractères minimum'; - return null; - })), - Expanded(flex: 1, child: const SizedBox()), // Espace de 4% - Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) { - if (value == null || value.isEmpty) return 'Confirmation requise'; - if (value != _passwordController.text) return 'Ne correspond pas'; - return null; - })), - ], - ), - const SizedBox(height: 20), - CustomAppTextField( - controller: _addressController, - labelText: 'Adresse (N° et Rue)', - hintText: 'Numéro et nom de votre rue', - style: CustomAppTextFieldStyle.beige, - fieldWidth: double.infinity, - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - const SizedBox(width: 20), - Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ), - - // Chevron de navigation gauche (Retour) - Positioned( - top: screenSize.height / 2 - 20, // Centré verticalement - left: 40, - child: IconButton( - icon: Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(math.pi), // Inverse horizontalement - child: Image.asset('assets/images/chevron_right.png', height: 40), - ), - onPressed: () => Navigator.pop(context), // Retour à l'écran de choix - tooltip: 'Retour', - ), - ), - - // Chevron de navigation droit (Suivant) - Positioned( - top: screenSize.height / 2 - 20, // Centré verticalement - right: 40, - child: IconButton( - icon: Image.asset('assets/images/chevron_right.png', height: 40), - onPressed: () { - if (_formKey.currentState?.validate() ?? false) { - _registrationData.updateParent1( - ParentData( - firstName: _firstNameController.text, - lastName: _lastNameController.text, - address: _addressController.text, // Rue - postalCode: _postalCodeController.text, // Ajout - city: _cityController.text, // Ajout - phone: _phoneController.text, - email: _emailController.text, - password: _passwordController.text, - ) - ); - Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData); - } - }, - tooltip: 'Suivant', - ), - ), - ], - ), + return PersonalInfoFormScreen( + stepText: 'Étape 1/5', + title: 'Informations du Parent Principal', + cardColor: CardColorHorizontal.peach, + initialData: initialData, + previousRoute: '/register-choice', + onSubmit: (data, {hasSecondPerson, sameAddress}) { + registrationData.updateParent1(ParentData( + firstName: data.firstName, + lastName: data.lastName, + phone: data.phone, + email: data.email, + address: data.address, + postalCode: data.postalCode, + city: data.city, + password: '', + )); + context.go('/parent-register-step2'); + }, ); } -} \ No newline at end of file +} diff --git a/frontend/lib/screens/auth/parent_register_step2_screen.dart b/frontend/lib/screens/auth/parent_register_step2_screen.dart index a7ecaf2..efa3277 100644 --- a/frontend/lib/screens/auth/parent_register_step2_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step2_screen.dart @@ -1,255 +1,90 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'dart:math' as math; // Pour la rotation du chevron -import '../../models/user_registration_data.dart'; // Import du modèle -import '../../utils/data_generator.dart'; // Import du générateur -import '../../widgets/custom_app_text_field.dart'; // Import du widget -import '../../models/card_assets.dart'; // Import des enums de cartes +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; -class ParentRegisterStep2Screen extends StatefulWidget { - final UserRegistrationData registrationData; // Accepte les données de l'étape 1 +import '../../models/user_registration_data.dart'; +import '../../utils/data_generator.dart'; +import '../../widgets/personal_info_form_screen.dart'; +import '../../models/card_assets.dart'; - const ParentRegisterStep2Screen({super.key, required this.registrationData}); - - @override - State createState() => _ParentRegisterStep2ScreenState(); -} - -class _ParentRegisterStep2ScreenState extends State { - final _formKey = GlobalKey(); - late UserRegistrationData _registrationData; // Copie locale pour modification - - bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2 - bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi - - // Contrôleurs pour les champs du parent 2 (restauration CP et Ville) - final _lastNameController = TextEditingController(); - final _firstNameController = TextEditingController(); - final _phoneController = TextEditingController(); - final _emailController = TextEditingController(); - final _passwordController = TextEditingController(); - final _confirmPasswordController = TextEditingController(); - final _addressController = TextEditingController(); // Rue seule - final _postalCodeController = TextEditingController(); // Restauré - final _cityController = TextEditingController(); // Restauré - - @override - void initState() { - super.initState(); - _registrationData = widget.registrationData; // Récupère les données de l'étape 1 - if (_addParent2) { - _generateAndFillParent2Data(); - } - } - - void _generateAndFillParent2Data() { - final String genFirstName = DataGenerator.firstName(); - final String genLastName = DataGenerator.lastName(); - _firstNameController.text = genFirstName; - _lastNameController.text = genLastName; - _phoneController.text = DataGenerator.phone(); - _emailController.text = DataGenerator.email(genFirstName, genLastName); - _passwordController.text = DataGenerator.password(); - _confirmPasswordController.text = _passwordController.text; - - _sameAddressAsParent1 = DataGenerator.boolean(); - if (!_sameAddressAsParent1) { - // Générer adresse, CP, Ville séparément - _addressController.text = DataGenerator.address(); - _postalCodeController.text = DataGenerator.postalCode(); - _cityController.text = DataGenerator.city(); - } else { - // Vider les champs si même adresse (seront désactivés) - _addressController.clear(); - _postalCodeController.clear(); - _cityController.clear(); - } - } - - @override - void dispose() { - _lastNameController.dispose(); - _firstNameController.dispose(); - _phoneController.dispose(); - _emailController.dispose(); - _passwordController.dispose(); - _confirmPasswordController.dispose(); - _addressController.dispose(); - _postalCodeController.dispose(); - _cityController.dispose(); - super.dispose(); - } - - bool get _parent2FieldsEnabled => _addParent2; - bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1; +class ParentRegisterStep2Screen extends StatelessWidget { + const ParentRegisterStep2Screen({super.key}); @override Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; + final registrationData = Provider.of(context, listen: false); + final parent1 = registrationData.parent1; + final parent2 = registrationData.parent2; + + bool hasParent2 = parent2 != null; + bool sameAddress = false; + + // Générer des données de test si vide + PersonalInfoData initialData; + if (parent2 == null || parent2.firstName.isEmpty) { + final genFirstName = DataGenerator.firstName(); + final genLastName = DataGenerator.lastName(); + sameAddress = DataGenerator.boolean(); + + initialData = PersonalInfoData( + firstName: genFirstName, + lastName: genLastName, + phone: DataGenerator.phone(), + email: DataGenerator.email(genFirstName, genLastName), + address: sameAddress ? parent1.address : DataGenerator.address(), + postalCode: sameAddress ? parent1.postalCode : DataGenerator.postalCode(), + city: sameAddress ? parent1.city : DataGenerator.city(), + ); + } else { + sameAddress = (parent2.address == parent1.address && + parent2.postalCode == parent1.postalCode && + parent2.city == parent1.city); + initialData = PersonalInfoData( + firstName: parent2.firstName, + lastName: parent2.lastName, + phone: parent2.phone, + email: parent2.email, + address: parent2.address, + postalCode: parent2.postalCode, + city: parent2.city, + ); + } - return Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), - ), - Center( - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), - const SizedBox(height: 10), - Text( - 'Informations du Deuxième Parent (Optionnel)', - style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87), - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - Container( - width: screenSize.width * 0.6, - padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), - decoration: BoxDecoration( - image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill), - ), - child: Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - flex: 12, - child: Row(children: [ - const Icon(Icons.person_add_alt_1, size: 20), const SizedBox(width: 8), - Flexible(child: Text('Ajouter Parent 2 ?', style: GoogleFonts.merienda(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis)), - const Spacer(), - Switch(value: _addParent2, onChanged: (val) => setState(() { - _addParent2 = val ?? false; - if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields(); - }), activeColor: Theme.of(context).primaryColor), - ]), - ), - Expanded(flex: 1, child: const SizedBox()), - Expanded( - flex: 12, - child: Row(children: [ - Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey), - const SizedBox(width: 8), - Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)), - const Spacer(), - Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() { - _sameAddressAsParent1 = val ?? false; - if (_sameAddressAsParent1) { - _addressController.text = _registrationData.parent1.address; - _postalCodeController.text = _registrationData.parent1.postalCode; - _cityController.text = _registrationData.parent1.city; - } else { - _addressController.text = DataGenerator.address(); - _postalCodeController.text = DataGenerator.postalCode(); - _cityController.text = DataGenerator.city(); - } - }) : null, activeColor: Theme.of(context).primaryColor), - ]), - ), - ]), - const SizedBox(height: 25), - Row( - children: [ - Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - Expanded(flex: 1, child: const SizedBox()), // Espace de 4% - Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - ], - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - Expanded(flex: 1, child: const SizedBox()), // Espace de 4% - Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - ], - ), - const SizedBox(height: 20), - Row( - children: [ - Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v.length < 6 ? '6 car. min' : null)) : null)), - Expanded(flex: 1, child: const SizedBox()), // Espace de 4% - Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmer mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v != _passwordController.text ? 'Différent' : null)) : null)), - ], - ), - const SizedBox(height: 20), - CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity), - const SizedBox(height: 20), - Row( - children: [ - Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - const SizedBox(width: 20), - Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), - ], - ), - ], - ), - ), - ), - ), - ], - ), - ), - ), - Positioned( - top: screenSize.height / 2 - 20, - left: 40, - child: IconButton( - icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), - onPressed: () => Navigator.pop(context), - tooltip: 'Retour', - ), - ), - Positioned( - top: screenSize.height / 2 - 20, - right: 40, - child: IconButton( - icon: Image.asset('assets/images/chevron_right.png', height: 40), - onPressed: () { - if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) { - if (_addParent2) { - _registrationData.updateParent2( - ParentData( - firstName: _firstNameController.text, - lastName: _lastNameController.text, - address: _sameAddressAsParent1 ? _registrationData.parent1.address : _addressController.text, - postalCode: _sameAddressAsParent1 ? _registrationData.parent1.postalCode : _postalCodeController.text, - city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text, - phone: _phoneController.text, - email: _emailController.text, - password: _passwordController.text, - ) - ); - } else { - _registrationData.updateParent2(null); - } - Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData); - } - }, - tooltip: 'Suivant', - ), - ), - ], - ), + // Adresse de référence pour "même adresse" + final referenceAddress = PersonalInfoData( + address: parent1.address, + postalCode: parent1.postalCode, + city: parent1.city, + ); + + return PersonalInfoFormScreen( + stepText: 'Étape 2/5', + title: 'Deuxième Parent', + cardColor: CardColorHorizontal.blue, + initialData: initialData, + previousRoute: '/parent-register-step1', + showSecondPersonToggle: true, + initialHasSecondPerson: hasParent2, + showSameAddressCheckbox: true, + initialSameAddress: sameAddress, + referenceAddressData: referenceAddress, + onSubmit: (data, {hasSecondPerson, sameAddress}) { + if (hasSecondPerson == true) { + registrationData.updateParent2(ParentData( + firstName: data.firstName, + lastName: data.lastName, + phone: data.phone, + email: data.email, + address: data.address, + postalCode: data.postalCode, + city: data.city, + password: '', + )); + } else { + registrationData.updateParent2(null); + } + context.go('/parent-register-step3'); + }, ); } - - void _clearParent2Fields() { - _formKey.currentState?.reset(); - _lastNameController.clear(); _firstNameController.clear(); _phoneController.clear(); - _emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear(); - _addressController.clear(); - _postalCodeController.clear(); - _cityController.clear(); - _sameAddressAsParent1 = false; - setState(() {}); - } -} \ No newline at end of file +} diff --git a/frontend/lib/screens/auth/parent_register_step3_screen.dart b/frontend/lib/screens/auth/parent_register_step3_screen.dart index ac9daff..1f62801 100644 --- a/frontend/lib/screens/auth/parent_register_step3_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step3_screen.dart @@ -1,31 +1,28 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'dart:math' as math; // Pour la rotation du chevron -import 'package:flutter/gestures.dart'; // Pour PointerDeviceKind -import '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget import 'package:image_picker/image_picker.dart'; -// import 'package:image_cropper/image_cropper.dart'; // Supprimé -import 'dart:io' show File, Platform; // Ajout de Platform -import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb -import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField -import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox -import '../../models/user_registration_data.dart'; // Import du modèle de données -import '../../utils/data_generator.dart'; // Import du générateur -import '../../models/card_assets.dart'; // Import des enums de cartes - -// La classe _ChildFormData est supprimée car on utilise ChildData du modèle +import 'dart:io' show File; +import '../../widgets/hover_relief_widget.dart'; +import '../../widgets/child_card_widget.dart'; +import '../../models/user_registration_data.dart'; +import '../../utils/data_generator.dart'; +import '../../models/card_assets.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; class ParentRegisterStep3Screen extends StatefulWidget { - final UserRegistrationData registrationData; // Accepte les données + // final UserRegistrationData registrationData; // Supprimé - const ParentRegisterStep3Screen({super.key, required this.registrationData}); + const ParentRegisterStep3Screen({super.key /*, required this.registrationData */}); // Modifié @override - State createState() => _ParentRegisterStep3ScreenState(); + _ParentRegisterStep3ScreenState createState() => + _ParentRegisterStep3ScreenState(); } class _ParentRegisterStep3ScreenState extends State { - late UserRegistrationData _registrationData; // Stocke l'état complet + // late UserRegistrationData _registrationData; // Supprimé final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal bool _isScrollable = false; bool _showLeftFade = false; @@ -52,14 +49,18 @@ class _ParentRegisterStep3ScreenState extends State { @override void initState() { super.initState(); - _registrationData = widget.registrationData; + final registrationData = Provider.of(context, listen: false); + // _registrationData = registrationData; // Supprimé + // Initialiser les couleurs utilisées avec les enfants existants - for (var child in _registrationData.children) { + for (var child in registrationData.children) { _usedColors.add(child.cardColor); } - // S'il n'y a pas d'enfant, en ajouter un automatiquement avec des données générées - if (_registrationData.children.isEmpty) { - _addChild(); + // S'il n'y a pas d'enfant, en ajouter un automatiquement APRÈS le premier build + if (registrationData.children.isEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _addChild(registrationData); + }); } _scrollController.addListener(_scrollListener); WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener()); @@ -87,7 +88,7 @@ class _ParentRegisterStep3ScreenState extends State { } } - void _addChild() { + void _addChild(UserRegistrationData registrationData) { // Prend registrationData setState(() { bool isUnborn = DataGenerator.boolean(); @@ -98,7 +99,7 @@ class _ParentRegisterStep3ScreenState extends State { ); final newChild = ChildData( - lastName: _registrationData.parent1.lastName, + lastName: registrationData.parent1.lastName, firstName: DataGenerator.firstName(), dob: DataGenerator.dob(isUnborn: isUnborn), isUnbornChild: isUnborn, @@ -106,7 +107,7 @@ class _ParentRegisterStep3ScreenState extends State { multipleBirth: DataGenerator.boolean(), cardColor: cardColor, ); - _registrationData.addChild(newChild); + registrationData.addChild(newChild); _usedColors.add(cardColor); }); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -117,33 +118,42 @@ class _ParentRegisterStep3ScreenState extends State { }); } - void _removeChild(int index) { - if (_registrationData.children.length > 1 && index >= 0 && index < _registrationData.children.length) { + void _removeChild(int index, UserRegistrationData registrationData) { + if (registrationData.children.length > 1 && index >= 0 && index < registrationData.children.length) { setState(() { // Ne pas retirer la couleur de _usedColors pour éviter sa réutilisation - _registrationData.children.removeAt(index); + registrationData.children.removeAt(index); }); WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener()); } } - Future _pickImage(int childIndex) async { + Future _pickImage(int childIndex, UserRegistrationData registrationData) async { final ImagePicker picker = ImagePicker(); try { final XFile? pickedFile = await picker.pickImage( source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024); if (pickedFile != null) { - setState(() { - if (childIndex < _registrationData.children.length) { - _registrationData.children[childIndex].imageFile = File(pickedFile.path); - } - }); + if (childIndex < registrationData.children.length) { + final oldChild = registrationData.children[childIndex]; + final updatedChild = ChildData( + firstName: oldChild.firstName, + lastName: oldChild.lastName, + dob: oldChild.dob, + photoConsent: oldChild.photoConsent, + multipleBirth: oldChild.multipleBirth, + isUnbornChild: oldChild.isUnbornChild, + imageFile: File(pickedFile.path), + cardColor: oldChild.cardColor, + ); + registrationData.updateChild(childIndex, updatedChild); + } } } catch (e) { print("Erreur image: $e"); } } - Future _selectDate(BuildContext context, int childIndex) async { - final ChildData currentChild = _registrationData.children[childIndex]; + Future _selectDate(BuildContext context, int childIndex, UserRegistrationData registrationData) async { + final ChildData currentChild = registrationData.children[childIndex]; final DateTime now = DateTime.now(); DateTime initialDatePickerDate = now; DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now; @@ -175,14 +185,24 @@ class _ParentRegisterStep3ScreenState extends State { lastDate: lastDatePickerDate, locale: const Locale('fr', 'FR'), ); if (picked != null) { - setState(() { - currentChild.dob = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}"; - }); + final oldChild = registrationData.children[childIndex]; + final updatedChild = ChildData( + firstName: oldChild.firstName, + lastName: oldChild.lastName, + dob: "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}", + photoConsent: oldChild.photoConsent, + multipleBirth: oldChild.multipleBirth, + isUnbornChild: oldChild.isUnbornChild, + imageFile: oldChild.imageFile, + cardColor: oldChild.cardColor, + ); + registrationData.updateChild(childIndex, updatedChild); } } @override Widget build(BuildContext context) { + final registrationData = Provider.of(context /*, listen: true par défaut */); final screenSize = MediaQuery.of(context).size; return Scaffold( body: Stack( @@ -221,36 +241,57 @@ class _ParentRegisterStep3ScreenState extends State { controller: _scrollController, scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 20.0), - itemCount: _registrationData.children.length + 1, + itemCount: registrationData.children.length + 1, itemBuilder: (context, index) { - if (index < _registrationData.children.length) { + if (index < registrationData.children.length) { // Carte Enfant return Padding( padding: const EdgeInsets.only(right: 20.0), - child: _ChildCardWidget( - key: ValueKey(_registrationData.children[index].hashCode), // Utiliser une clé basée sur les données - childData: _registrationData.children[index], + child: ChildCardWidget( + key: ValueKey(registrationData.children[index].hashCode), // Utiliser une clé basée sur les données + childData: registrationData.children[index], childIndex: index, - onPickImage: () => _pickImage(index), - onDateSelect: () => _selectDate(context, index), - onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value), - onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value), - onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue), - onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue), - onToggleIsUnborn: (newValue) => setState(() { - _registrationData.children[index].isUnbornChild = newValue; - // Générer une nouvelle date si on change le statut - _registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue); - }), - onRemove: () => _removeChild(index), - canBeRemoved: _registrationData.children.length > 1, + onPickImage: () => _pickImage(index, registrationData), + onDateSelect: () => _selectDate(context, index, registrationData), + onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData( + firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent, + multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor + ))), + onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData( + firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent, + multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor + ))), + onTogglePhotoConsent: (newValue) { + final oldChild = registrationData.children[index]; + registrationData.updateChild(index, ChildData( + firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue, + multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor + )); + }, + onToggleMultipleBirth: (newValue) { + final oldChild = registrationData.children[index]; + registrationData.updateChild(index, ChildData( + firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent, + multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor + )); + }, + onToggleIsUnborn: (newValue) { + final oldChild = registrationData.children[index]; + registrationData.updateChild(index, ChildData( + firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue), + photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue, + imageFile: oldChild.imageFile, cardColor: oldChild.cardColor + )); + }, + onRemove: () => _removeChild(index, registrationData), + canBeRemoved: registrationData.children.length > 1, ), ); } else { // Bouton Ajouter return Center( child: HoverReliefWidget( - onPressed: _addChild, + onPressed: () => _addChild(registrationData), borderRadius: BorderRadius.circular(15), child: Image.asset('assets/images/plus.png', height: 80, width: 80), ), @@ -272,7 +313,13 @@ class _ParentRegisterStep3ScreenState extends State { left: 40, child: IconButton( icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), - onPressed: () => Navigator.pop(context), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/parent-register-step2'); + } + }, tooltip: 'Retour', ), ), @@ -282,8 +329,7 @@ class _ParentRegisterStep3ScreenState extends State { child: IconButton( icon: Image.asset('assets/images/chevron_right.png', height: 40), onPressed: () { - // TODO: Validation (si nécessaire) - Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData); + context.go('/parent-register-step4'); }, tooltip: 'Suivant', ), @@ -292,196 +338,4 @@ class _ParentRegisterStep3ScreenState extends State { ), ); } -} - -// Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks) -class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes - final ChildData childData; - final int childIndex; - final VoidCallback onPickImage; - final VoidCallback onDateSelect; - final ValueChanged onFirstNameChanged; - final ValueChanged onLastNameChanged; - final ValueChanged onTogglePhotoConsent; - final ValueChanged onToggleMultipleBirth; - final ValueChanged onToggleIsUnborn; - final VoidCallback onRemove; - final bool canBeRemoved; - - const _ChildCardWidget({ - required Key key, - required this.childData, - required this.childIndex, - required this.onPickImage, - required this.onDateSelect, - required this.onFirstNameChanged, - required this.onLastNameChanged, - required this.onTogglePhotoConsent, - required this.onToggleMultipleBirth, - required this.onToggleIsUnborn, - required this.onRemove, - required this.canBeRemoved, - }) : super(key: key); - - @override - State<_ChildCardWidget> createState() => _ChildCardWidgetState(); -} - -class _ChildCardWidgetState extends State<_ChildCardWidget> { - late TextEditingController _firstNameController; - late TextEditingController _lastNameController; - late TextEditingController _dobController; - - @override - void initState() { - super.initState(); - // Initialiser les contrôleurs avec les données du widget - _firstNameController = TextEditingController(text: widget.childData.firstName); - _lastNameController = TextEditingController(text: widget.childData.lastName); - _dobController = TextEditingController(text: widget.childData.dob); - - // Ajouter des listeners pour mettre à jour les données sources via les callbacks - _firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text)); - _lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text)); - // Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici - } - - @override - void didUpdateWidget(covariant _ChildCardWidget oldWidget) { - super.didUpdateWidget(oldWidget); - // Mettre à jour les contrôleurs si les données externes changent - // (peut arriver si on recharge l'état global) - if (widget.childData.firstName != _firstNameController.text) { - _firstNameController.text = widget.childData.firstName; - } - if (widget.childData.lastName != _lastNameController.text) { - _lastNameController.text = widget.childData.lastName; - } - if (widget.childData.dob != _dobController.text) { - _dobController.text = widget.childData.dob; - } - } - - @override - void dispose() { - _firstNameController.dispose(); - _lastNameController.dispose(); - _dobController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final File? currentChildImage = widget.childData.imageFile; - // Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond - final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender - ? Colors.purple.shade200 - : (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs - final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90); - final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); - - return Container( - width: 345.0 * 1.1, // 379.5 - height: 570.0 * 1.2, // 684.0 - padding: const EdgeInsets.all(22.0 * 1.1), // 24.2 - decoration: BoxDecoration( - image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover), - borderRadius: BorderRadius.circular(20 * 1.1), // 22 - ), - child: Stack( - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - HoverReliefWidget( - onPressed: widget.onPickImage, - borderRadius: BorderRadius.circular(10), - initialShadowColor: initialPhotoShadow, - hoverShadowColor: hoverPhotoShadow, - child: SizedBox( - height: 200.0, - width: 200.0, - child: Center( - child: Padding( - padding: const EdgeInsets.all(5.0 * 1.1), // 5.5 - child: currentChildImage != null - ? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover)) - : Image.asset('assets/images/photo.png', fit: BoxFit.contain), - ), - ), - ), - ), - const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)), - Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor), - ], - ), - const SizedBox(height: 9.0 * 1.1), // 9.9 - CustomAppTextField( - controller: _firstNameController, - labelText: 'Prénom', - hintText: 'Facultatif si à naître', - isRequired: !widget.childData.isUnbornChild, - fieldHeight: 55.0 * 1.1, // 60.5 - ), - const SizedBox(height: 6.0 * 1.1), // 6.6 - CustomAppTextField( - controller: _lastNameController, - labelText: 'Nom', - hintText: 'Nom de l\'enfant', - enabled: true, - fieldHeight: 55.0 * 1.1, // 60.5 - ), - const SizedBox(height: 9.0 * 1.1), // 9.9 - CustomAppTextField( - controller: _dobController, - labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', - hintText: 'JJ/MM/AAAA', - readOnly: true, - onTap: widget.onDateSelect, - suffixIcon: Icons.calendar_today, - fieldHeight: 55.0 * 1.1, // 60.5 - ), - const SizedBox(height: 11.0 * 1.1), // 12.1 - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AppCustomCheckbox( - label: 'Consentement photo', - value: widget.childData.photoConsent, - onChanged: widget.onTogglePhotoConsent, - checkboxSize: 22.0 * 1.1, // 24.2 - ), - const SizedBox(height: 6.0 * 1.1), // 6.6 - AppCustomCheckbox( - label: 'Naissance multiple', - value: widget.childData.multipleBirth, - onChanged: widget.onToggleMultipleBirth, - checkboxSize: 22.0 * 1.1, // 24.2 - ), - ], - ), - ], - ), - if (widget.canBeRemoved) - Positioned( - top: -5, right: -5, - child: InkWell( - onTap: widget.onRemove, - customBorder: const CircleBorder(), - child: Image.asset( - 'images/red_cross2.png', - width: 36, - height: 36, - fit: BoxFit.contain, - ), - ), - ), - ], - ), - ); - } } \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step4_screen.dart b/frontend/lib/screens/auth/parent_register_step4_screen.dart index 62ae003..10ae74d 100644 --- a/frontend/lib/screens/auth/parent_register_step4_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step4_screen.dart @@ -1,217 +1,42 @@ import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart'; // Import du nouveau widget -import 'dart:math' as math; // Pour la rotation du chevron -import 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée -// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé -import '../../models/user_registration_data.dart'; // Import du vrai modèle -import '../../utils/data_generator.dart'; // Import du générateur -import '../../models/card_assets.dart'; // Import des enums de cartes +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; -class ParentRegisterStep4Screen extends StatefulWidget { - final UserRegistrationData registrationData; // Accepte les données +import '../../models/user_registration_data.dart'; +import '../../widgets/presentation_form_screen.dart'; +import '../../models/card_assets.dart'; +import '../../utils/data_generator.dart'; - const ParentRegisterStep4Screen({super.key, required this.registrationData}); - - @override - State createState() => _ParentRegisterStep4ScreenState(); -} - -class _ParentRegisterStep4ScreenState extends State { - late UserRegistrationData _registrationData; // État local - final _motivationController = TextEditingController(); - bool _cguAccepted = true; // Pour le test, CGU acceptées par défaut - - @override - void initState() { - super.initState(); - _registrationData = widget.registrationData; - _motivationController.text = DataGenerator.motivation(); // Générer la motivation - } - - @override - void dispose() { - _motivationController.dispose(); - super.dispose(); - } - - void _showCGUModal() { - // Un long texte Lorem Ipsum pour simuler les CGU - const String loremIpsumText = ''' -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit. - -Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. - -Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. - -Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit. - -Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. Etiam et felis dolor. - -Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. - -Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. -'''; - - showDialog( - context: context, - barrierDismissible: false, // L'utilisateur doit utiliser le bouton - builder: (BuildContext dialogContext) { - return AlertDialog( - title: Text( - 'Conditions Générales d\'Utilisation', - style: GoogleFonts.merienda(fontWeight: FontWeight.bold), - ), - content: SizedBox( - width: MediaQuery.of(dialogContext).size.width * 0.7, // 70% de la largeur de l'écran - height: MediaQuery.of(dialogContext).size.height * 0.6, // 60% de la hauteur de l'écran - child: SingleChildScrollView( - child: Text( - loremIpsumText, - style: GoogleFonts.merienda(fontSize: 13), - textAlign: TextAlign.justify, - ), - ), - ), - actionsPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), - actionsAlignment: MainAxisAlignment.center, - actions: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(dialogContext).primaryColor, - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), - ), - child: Text( - 'Valider et Accepter', - style: GoogleFonts.merienda(fontSize: 15, color: Colors.white, fontWeight: FontWeight.bold), - ), - onPressed: () { - Navigator.of(dialogContext).pop(); // Ferme la modale - setState(() { - _cguAccepted = true; // Met à jour l'état - }); - }, - ), - ], - ); - }, - ); - } +class ParentRegisterStep4Screen extends StatelessWidget { + const ParentRegisterStep4Screen({super.key}); @override Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; - final cardWidth = screenSize.width * 0.6; // Largeur de la carte (60% de l'écran) - final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0) - final cardHeight = cardWidth / imageAspectRatio; + final registrationData = Provider.of(context, listen: false); + + // Générer un texte de test si vide + String initialText = registrationData.motivationText; + bool initialCgu = registrationData.cguAccepted; + + if (initialText.isEmpty) { + initialText = DataGenerator.motivation(); + initialCgu = true; + } - return Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), - ), - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'Étape 4/5', - style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), - ), - const SizedBox(height: 20), - Text( - 'Motivation de votre demande', - style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), - textAlign: TextAlign.center, - ), - const SizedBox(height: 30), - Container( - width: cardWidth, - height: cardHeight, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(CardColorHorizontal.green.path), - fit: BoxFit.fill, - ), - ), - child: Padding( - padding: const EdgeInsets.all(40.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: CustomDecoratedTextField( - controller: _motivationController, - hintText: 'Écrivez ici pour motiver votre demande...', - fieldHeight: cardHeight * 0.6, - maxLines: 10, - expandDynamically: true, - fontSize: 18.0, - ), - ), - const SizedBox(height: 20), - GestureDetector( - onTap: () { - if (!_cguAccepted) { - _showCGUModal(); - } - }, - child: AppCustomCheckbox( - label: 'J\'accepte les conditions générales d\'utilisation', - value: _cguAccepted, - onChanged: (newValue) { - if (!_cguAccepted) { - _showCGUModal(); - } else { - setState(() => _cguAccepted = false); - } - }, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - // Chevrons de navigation - Positioned( - top: screenSize.height / 2 - 20, - left: 40, - child: IconButton( - icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), - onPressed: () => Navigator.pop(context), - tooltip: 'Retour', - ), - ), - Positioned( - top: screenSize.height / 2 - 20, - right: 40, - child: IconButton( - icon: Image.asset('assets/images/chevron_right.png', height: 40), - onPressed: _cguAccepted - ? () { - _registrationData.updateMotivation(_motivationController.text); - _registrationData.acceptCGU(); - - Navigator.pushNamed( - context, - '/parent-register/step5', - arguments: _registrationData - ); - } - : null, - tooltip: 'Suivant', - ), - ), - ], - ), + return PresentationFormScreen( + stepText: 'Étape 4/5', + title: 'Motivation de votre demande', + cardColor: CardColorHorizontal.green, + textFieldHint: 'Écrivez ici pour motiver votre demande...', + initialText: initialText, + initialCguAccepted: initialCgu, + previousRoute: '/parent-register-step3', + onSubmit: (text, cguAccepted) { + registrationData.updateMotivation(text); + registrationData.acceptCGU(cguAccepted); + // Les infos financières peuvent être gérées ailleurs si nécessaire + context.go('/parent-register-step5'); + }, ); } -} \ No newline at end of file +} diff --git a/frontend/lib/screens/auth/parent_register_step5_screen.dart b/frontend/lib/screens/auth/parent_register_step5_screen.dart index 24a914c..5f939f7 100644 --- a/frontend/lib/screens/auth/parent_register_step5_screen.dart +++ b/frontend/lib/screens/auth/parent_register_step5_screen.dart @@ -4,69 +4,153 @@ import '../../models/user_registration_data.dart'; // Utilisation du vrai modèl import '../../widgets/image_button.dart'; // Import du ImageButton import '../../models/card_assets.dart'; // Import des enums de cartes import 'package:flutter/foundation.dart' show kIsWeb; -import '../../widgets/custom_decorated_text_field.dart'; // Import du CustomDecoratedTextField +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; // Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) { const FontWeight labelFontWeight = FontWeight.w600; - // Ne pas afficher le label si labelFontSize est 0 ou si label est vide - bool showLabel = label.isNotEmpty && labelFontSize > 0; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showLabel) - Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)), - if (showLabel) - const SizedBox(height: 4), - // Utiliser Expanded si multiLine et pas de hauteur fixe, sinon Container - multiLine && fieldHeight == null - ? Expanded( - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), - decoration: BoxDecoration( - image: const DecorationImage( - image: AssetImage('assets/images/input_field_bg.png'), - fit: BoxFit.fill, - ), - ), - child: SingleChildScrollView( // Pour le défilement si le texte dépasse - child: Text( - value.isNotEmpty ? value : '-', - style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), // Garder une taille de texte par défaut si label caché - maxLines: null, // Permettre un nombre illimité de lignes - ), - ), - ), - ) - : Container( - width: double.infinity, - height: multiLine ? null : fieldHeight, - constraints: multiLine ? BoxConstraints(minHeight: fieldHeight) : null, - padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), - decoration: BoxDecoration( - image: const DecorationImage( - image: AssetImage('assets/images/input_field_bg.png'), - fit: BoxFit.fill, - ), - ), - child: Text( - value.isNotEmpty ? value : '-', - style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), - maxLines: multiLine ? null : 1, - overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis, - ), + Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)), + const SizedBox(height: 4), + Container( + width: double.infinity, // Prendra la largeur allouée par son parent (Expanded) + height: multiLine ? null : fieldHeight, // Hauteur flexible pour multiligne, sinon fixe + constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, // Hauteur min pour multiligne + padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), // Ajuster au besoin + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/images/input_field_bg.png'), // Image de fond du champ + fit: BoxFit.fill, ), + // Si votre image input_field_bg.png a des coins arrondis intrinsèques, ce borderRadius n'est pas nécessaire + // ou doit correspondre. Sinon, pour une image rectangulaire, vous pouvez l'ajouter. + // borderRadius: BorderRadius.circular(12), + ), + child: Text( + value.isNotEmpty ? value : '-', + style: GoogleFonts.merienda(fontSize: labelFontSize), + maxLines: multiLine ? null : 1, // Permet plusieurs lignes si multiLine est true + overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis, + ), + ), ], ); } -class ParentRegisterStep5Screen extends StatelessWidget { - final UserRegistrationData registrationData; +class ParentRegisterStep5Screen extends StatefulWidget { + const ParentRegisterStep5Screen({super.key}); - const ParentRegisterStep5Screen({super.key, required this.registrationData}); + @override + _ParentRegisterStep5ScreenState createState() => _ParentRegisterStep5ScreenState(); +} + +class _ParentRegisterStep5ScreenState extends State { + @override + Widget build(BuildContext context) { + final registrationData = Provider.of(context); + final screenSize = MediaQuery.of(context).size; + final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran) + final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0) + final cardHeight = cardWidth / imageAspectRatio; + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici + child: Padding( // Ajout du Padding horizontal externe + padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 20), + Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), + const SizedBox(height: 30), + + _buildParent1Card(context, registrationData.parent1), + const SizedBox(height: 20), + if (registrationData.parent2 != null) ...[ + _buildParent2Card(context, registrationData.parent2!), + const SizedBox(height: 20), + ], + ..._buildChildrenCards(context, registrationData.children), + _buildMotivationCard(context, registrationData.motivationText), + const SizedBox(height: 40), + ImageButton( + bg: 'assets/images/btn_green.png', + text: 'Soumettre ma demande', + textColor: const Color(0xFF2D6A4F), + width: 350, + height: 50, + fontSize: 18, + onPressed: () { + print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}"); + _showConfirmationModal(context); + }, + ), + ], + ), + ), + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/parent-register-step4'); + } + }, + tooltip: 'Retour', + ), + ), + ], + ), + ); + } + + void _showConfirmationModal(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text( + 'Demande enregistrée', + style: GoogleFonts.merienda(fontWeight: FontWeight.bold), + ), + content: Text( + 'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.', + style: GoogleFonts.merienda(fontSize: 14), + ), + actions: [ + TextButton( + child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)), + onPressed: () { + Navigator.of(dialogContext).pop(); // Ferme la modale + // Utiliser go_router pour la navigation + context.go('/login'); + }, + ), + ], + ); + }, + ); + } // Méthode pour construire la carte Parent 1 Widget _buildParent1Card(BuildContext context, ParentData data) { @@ -98,7 +182,7 @@ class ParentRegisterStep5Screen extends StatelessWidget { backgroundImagePath: CardColorHorizontal.peach.path, title: 'Parent Principal', content: details, - onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData), + onEdit: () => context.go('/parent-register-step1'), ); } @@ -131,7 +215,7 @@ class ParentRegisterStep5Screen extends StatelessWidget { backgroundImagePath: CardColorHorizontal.blue.path, title: 'Deuxième Parent', content: details, - onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData), + onEdit: () => context.go('/parent-register-step2'), ); } @@ -176,10 +260,7 @@ class ParentRegisterStep5Screen extends StatelessWidget { IconButton( icon: const Icon(Icons.edit, color: Colors.black54, size: 28), onPressed: () { - Navigator.of(context).pushNamed( - '/parent-register/step3', - arguments: registrationData, - ); + context.go('/parent-register-step3', extra: {'childIndex': index}); }, tooltip: 'Modifier', ), @@ -264,23 +345,17 @@ class ParentRegisterStep5Screen extends StatelessWidget { // Méthode pour construire la carte Motivation Widget _buildMotivationCard(BuildContext context, String motivation) { + List details = [ + Text(motivation.isNotEmpty ? motivation : 'Aucune motivation renseignée.', + style: GoogleFonts.merienda(fontSize: 18), + maxLines: 4, + overflow: TextOverflow.ellipsis) + ]; return _SummaryCard( - backgroundImagePath: CardColorHorizontal.green.path, + backgroundImagePath: CardColorHorizontal.pink.path, title: 'Votre Motivation', - content: [ - Expanded( - child: CustomDecoratedTextField( - controller: TextEditingController(text: motivation), - hintText: 'Aucune motivation renseignée.', - fieldHeight: 200, - maxLines: 10, - expandDynamically: true, - readOnly: true, - fontSize: 18.0, - ), - ), - ], - onEdit: () => Navigator.of(context).pushNamed('/parent-register/step4', arguments: registrationData), + content: details, + onEdit: () => context.go('/parent-register-step4'), ); } @@ -306,102 +381,6 @@ class ParentRegisterStep5Screen extends StatelessWidget { ), ); } - - @override - Widget build(BuildContext context) { - final screenSize = MediaQuery.of(context).size; - final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran) - final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0) - final cardHeight = cardWidth / imageAspectRatio; - - return Scaffold( - body: Stack( - children: [ - Positioned.fill( - child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY), - ), - Center( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici - child: Padding( // Ajout du Padding horizontal externe - padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), - const SizedBox(height: 20), - Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), - const SizedBox(height: 30), - - _buildParent1Card(context, registrationData.parent1), - const SizedBox(height: 20), - if (registrationData.parent2 != null) ...[ - _buildParent2Card(context, registrationData.parent2!), - const SizedBox(height: 20), - ], - ..._buildChildrenCards(context, registrationData.children), - _buildMotivationCard(context, registrationData.motivationText), - const SizedBox(height: 40), - ImageButton( - bg: 'assets/images/btn_green.png', - text: 'Soumettre ma demande', - textColor: const Color(0xFF2D6A4F), - width: 350, - height: 50, - fontSize: 18, - onPressed: () { - print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}"); - _showConfirmationModal(context); - }, - ), - ], - ), - ), - ), - ), - Positioned( - top: screenSize.height / 2 - 20, - left: 40, - child: IconButton( - icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), - onPressed: () => Navigator.pop(context), // Retour à l'étape 4 - tooltip: 'Retour', - ), - ), - ], - ), - ); - } - - void _showConfirmationModal(BuildContext context) { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext dialogContext) { - return AlertDialog( - title: Text( - 'Demande enregistrée', - style: GoogleFonts.merienda(fontWeight: FontWeight.bold), - ), - content: Text( - 'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.', - style: GoogleFonts.merienda(fontSize: 14), - ), - actions: [ - TextButton( - child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)), - onPressed: () { - Navigator.of(dialogContext).pop(); // Ferme la modale - // TODO: Naviguer vers l'écran de connexion ou tableau de bord - Navigator.of(context).pushNamedAndRemoveUntil('/login', (Route route) => false); - }, - ), - ], - ); - }, - ); - } } // Widget générique _SummaryCard (ajusté) @@ -422,7 +401,7 @@ class _SummaryCard extends StatelessWidget { @override Widget build(BuildContext context) { return AspectRatio( - aspectRatio: 2.0, + aspectRatio: 2.0, // Le ratio largeur/hauteur de nos images de fond child: Container( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), decoration: BoxDecoration( @@ -432,30 +411,31 @@ class _SummaryCard extends StatelessWidget { ), borderRadius: BorderRadius.circular(15), ), - child: Column( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - title, - style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600), - textAlign: TextAlign.center, - ), - ), - IconButton( - icon: const Icon(Icons.edit, color: Colors.black54, size: 28), - onPressed: onEdit, - tooltip: 'Modifier', - ), - ], - ), - const SizedBox(height: 18), Expanded( child: Column( - children: content, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, // Pour que la colonne prenne la hauteur du contenu + children: [ + Align( // Centrer le titre + alignment: Alignment.center, + child: Text( + title, + style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), // Police légèrement augmentée + ), + ), + const SizedBox(height: 12), // Espacement ajusté après le titre + ...content, + ], ), ), + IconButton( + icon: const Icon(Icons.edit, color: Colors.black54, size: 28), // Icône un peu plus grande + onPressed: onEdit, + tooltip: 'Modifier', + ), ], ), ), diff --git a/frontend/lib/screens/auth/register_choice_screen.dart b/frontend/lib/screens/auth/register_choice_screen.dart index 0b6bd8a..e80238b 100644 --- a/frontend/lib/screens/auth/register_choice_screen.dart +++ b/frontend/lib/screens/auth/register_choice_screen.dart @@ -3,6 +3,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'dart:math' as math; // Pour la rotation du chevron import '../../widgets/hover_relief_widget.dart'; // Import du widget générique import '../../models/card_assets.dart'; // Import des enums de cartes +import 'package:go_router/go_router.dart'; class RegisterChoiceScreen extends StatelessWidget { const RegisterChoiceScreen({super.key}); @@ -28,12 +29,14 @@ class RegisterChoiceScreen extends StatelessWidget { top: 40, left: 40, child: IconButton( - icon: Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(math.pi), - child: Image.asset('assets/images/chevron_right.png', height: 40), - ), - onPressed: () => Navigator.pop(context), + icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/login'); + } + }, tooltip: 'Retour', ), ), @@ -89,7 +92,7 @@ class RegisterChoiceScreen extends StatelessWidget { iconPath: 'assets/images/icon_parents.png', label: 'Parents', onPressed: () { - Navigator.pushNamed(context, '/parent-register/step1'); + context.go('/parent-register-step1'); }, ), // Bouton "Assistante Maternelle" avec HoverReliefWidget appliqué uniquement à l'image @@ -98,8 +101,7 @@ class RegisterChoiceScreen extends StatelessWidget { iconPath: 'assets/images/icon_assmat.png', label: 'Assistante Maternelle', onPressed: () { - // TODO: Naviguer vers l'écran d'inscription assmat - print('Choix: Assistante Maternelle'); + context.go('/am-register-step1'); }, ), ], diff --git a/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart b/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart new file mode 100644 index 0000000..81deb26 --- /dev/null +++ b/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/controllers/parent_dashboard_controller.dart'; +import 'package:p_tits_pas/services/dashboardService.dart'; +import 'package:p_tits_pas/widgets/app_footer.dart'; +import 'package:p_tits_pas/widgets/dashbord_parent/app_layout.dart'; +import 'package:p_tits_pas/widgets/dashbord_parent/children_sidebar.dart'; +import 'package:p_tits_pas/widgets/dashbord_parent/dashboard_app_bar.dart'; +import 'package:p_tits_pas/widgets/dashbord_parent/wid_dashbord.dart'; +import 'package:p_tits_pas/widgets/main_content_area.dart'; +import 'package:p_tits_pas/widgets/messaging_sidebar.dart'; +import 'package:provider/provider.dart'; + +class ParentDashboardScreen extends StatefulWidget { + const ParentDashboardScreen({Key? key}) : super(key: key); + + @override + State createState() => _ParentDashboardScreenState(); +} + +class _ParentDashboardScreenState extends State { + int selectedIndex = 0; + + void onTabChange(int index) { + setState(() { + selectedIndex = index; + }); + } + + @override + void initState() { + super.initState(); + // Initialiser les données du dashboard + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().initDashboard(); + }); + } + + Widget _getBody() { + switch (selectedIndex) { + case 0: + return Dashbord_body(); + case 1: + return const Center(child: Text("🔍 Trouver une nounou")); + case 2: + return const Center(child: Text("⚙️ Paramètres")); + default: + return const Center(child: Text("Page non trouvée")); + } + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => ParentDashboardController(DashboardService())..initDashboard(), + child: Scaffold( + appBar: PreferredSize(preferredSize: const Size.fromHeight(60.0), + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey.shade300), + ), + ), + child: DashboardAppBar( + selectedIndex: selectedIndex, + onTabChange: onTabChange, + ), + ), + ), + body: Column( + children: [ + Expanded (child: _getBody(), + ), + const AppFooter(), + ], + ), + ) + // body: _buildResponsiveBody(context, controller), + // footer: const AppFooter(), + ); + } + + Widget _buildResponsiveBody(BuildContext context, ParentDashboardController controller) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 768) { + // Layout mobile : colonnes empilées + return _buildMobileLayout(controller); + } else if (constraints.maxWidth < 1024) { + // Layout tablette : 2 colonnes + return _buildTabletLayout(controller); + } else { + // Layout desktop : 3 colonnes + return _buildDesktopLayout(controller); + } + }, + ); + } + + Widget _buildDesktopLayout(ParentDashboardController controller) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Sidebar gauche - Enfants + SizedBox( + width: 280, + child: ChildrenSidebar( + children: controller.children, + selectedChildId: controller.selectedChildId, + onChildSelected: controller.selectChild, + onAddChild: controller.showAddChildModal, + ), + ), + + // Contenu central + Expanded( + flex: 2, + child: MainContentArea( + selectedChild: controller.selectedChild, + selectedAssistant: controller.selectedAssistant, + events: controller.upcomingEvents, + contracts: controller.contracts, + ), + ), + + // Sidebar droite - Messagerie + SizedBox( + width: 320, + child: MessagingSidebar( + conversations: controller.conversations, + notifications: controller.notifications, + ), + ), + ], + ); + } + + Widget _buildTabletLayout(ParentDashboardController controller) { + return Row( + children: [ + // Sidebar enfants plus étroite + SizedBox( + width: 240, + child: ChildrenSidebar( + children: controller.children, + selectedChildId: controller.selectedChildId, + onChildSelected: controller.selectChild, + onAddChild: controller.showAddChildModal, + isCompact: true, + ), + ), + + // Contenu principal avec messagerie intégrée + Expanded( + child: Column( + children: [ + Expanded( + flex: 2, + child: MainContentArea( + selectedChild: controller.selectedChild, + selectedAssistant: controller.selectedAssistant, + events: controller.upcomingEvents, + contracts: controller.contracts, + ), + ), + SizedBox( + height: 200, + child: MessagingSidebar( + conversations: controller.conversations, + notifications: controller.notifications, + isCompact: true, + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildMobileLayout(ParentDashboardController controller) { + return DefaultTabController( + length: 4, + child: Column( + children: [ + // Navigation par onglets sur mobile + Container( + color: Theme.of(context).primaryColor.withOpacity(0.1), + child: const TabBar( + isScrollable: true, + tabs: [ + Tab(text: 'Enfants', icon: Icon(Icons.child_care)), + Tab(text: 'Planning', icon: Icon(Icons.calendar_month)), + Tab(text: 'Contrats', icon: Icon(Icons.description)), + Tab(text: 'Messages', icon: Icon(Icons.message)), + ], + ), + ), + + Expanded( + child: TabBarView( + children: [ + // Onglet Enfants + ChildrenSidebar( + children: controller.children, + selectedChildId: controller.selectedChildId, + onChildSelected: controller.selectChild, + onAddChild: controller.showAddChildModal, + isMobile: true, + ), + + // Onglet Planning + MainContentArea( + selectedChild: controller.selectedChild, + selectedAssistant: controller.selectedAssistant, + events: controller.upcomingEvents, + contracts: controller.contracts, + showOnlyCalendar: true, + ), + + // Onglet Contrats + MainContentArea( + selectedChild: controller.selectedChild, + selectedAssistant: controller.selectedAssistant, + events: controller.upcomingEvents, + contracts: controller.contracts, + showOnlyContracts: true, + ), + + // Onglet Messages + MessagingSidebar( + conversations: controller.conversations, + notifications: controller.notifications, + isMobile: true, + ), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/home/parent_screen/find_nanny.dart b/frontend/lib/screens/home/parent_screen/find_nanny.dart new file mode 100644 index 0000000..6955ac7 --- /dev/null +++ b/frontend/lib/screens/home/parent_screen/find_nanny.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class FindNannyScreen extends StatelessWidget { + const FindNannyScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Trouver une nounou"), + ), + body: Center( + child: const Text("Contenu de la page Trouver une nounou"), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/unknown_screen.dart b/frontend/lib/screens/unknown_screen.dart new file mode 100644 index 0000000..509f849 --- /dev/null +++ b/frontend/lib/screens/unknown_screen.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class UnknownScreen extends StatelessWidget { + const UnknownScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Page Introuvable')), + body: const Center( + child: Text('Désolé, cette page n\'existe pas.'), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/services/api/api_config.dart b/frontend/lib/services/api/api_config.dart new file mode 100644 index 0000000..700673e --- /dev/null +++ b/frontend/lib/services/api/api_config.dart @@ -0,0 +1,32 @@ +class ApiConfig { + // static const String baseUrl = 'http://localhost:3000/api/v1/'; + static const String baseUrl = 'https://ynov.ptits-pas.fr/api/v1'; + + // Auth endpoints + static const String login = '/auth/login'; + static const String register = '/auth/register'; + static const String refreshToken = '/auth/refresh'; + + // Users endpoints + static const String users = '/users'; + static const String userProfile = '/users/profile'; + static const String userChildren = '/users/children'; + + // Dashboard endpoints + static const String dashboard = '/dashboard'; + static const String events = '/events'; + static const String contracts = '/contracts'; + static const String conversations = '/conversations'; + static const String notifications = '/notifications'; + + // Headers + static Map get headers => { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + static Map authHeaders(String token) => { + ...headers, + 'Authorization': 'Bearer $token', + }; +} \ No newline at end of file diff --git a/frontend/lib/services/api/tokenService.dart b/frontend/lib/services/api/tokenService.dart new file mode 100644 index 0000000..1d451f3 --- /dev/null +++ b/frontend/lib/services/api/tokenService.dart @@ -0,0 +1,71 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class TokenService { + // static const _storage = FlutterSecureStorage(); + static const _tokenKey = 'access_token'; + static const String _refreshTokenKey = 'refresh_token'; + static const _roleKey = 'user_role'; + + // Stockage du token + static Future saveToken(String token) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_tokenKey, token); + } + + // Stockage du refresh token + static Future saveRefreshToken(String refreshToken) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_refreshTokenKey, refreshToken); + } + + // Stockage du rôle + static Future saveRole(String role) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_roleKey, role); + } + + // Récupération du token + static Future getToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_tokenKey); + } + + // Récupération du refresh token + static Future getRefreshToken() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_refreshTokenKey); + } + + // Récupération du rôle + static Future getRole() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_roleKey); + } + + // Suppression du token + static Future deleteToken() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_tokenKey); + } + + // Suppression du refresh token + static Future deleteRefreshToken() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_refreshTokenKey); + } + + + // Suppression du rôle + static Future deleteRole() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_roleKey); + } + + // Nettoyage complet + static Future clearAll() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_tokenKey); + await prefs.remove(_refreshTokenKey); + await prefs.remove(_roleKey); + } +} \ No newline at end of file diff --git a/frontend/lib/services/dashboardService.dart b/frontend/lib/services/dashboardService.dart new file mode 100644 index 0000000..31bc45d --- /dev/null +++ b/frontend/lib/services/dashboardService.dart @@ -0,0 +1,202 @@ +import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/child_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/contract_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/event_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/notification_model.dart'; + +class DashboardService { + // URL de base de l'API + static const String _baseUrl = 'YOUR_API_BASE_URL'; + + // Récupérer la liste des enfants + Future> getChildren() async { + try { + // TODO: Implémenter l'appel API + // Exemple de mock data pour le développement + return [ + ChildModel( + id: '1', + firstName: 'Emma', + birthDate: DateTime(2020, 5, 15), + photoUrl: 'assets/images/child1.jpg', + status: ChildStatus.onHoliday, + ), + ChildModel( + id: '2', + firstName: 'Lucas', + birthDate: DateTime(2021, 3, 10), + photoUrl: 'assets/images/child2.jpg', + status: ChildStatus.searching, + ), + ]; + } catch (e) { + throw Exception('Erreur lors de la récupération des enfants: $e'); + } + } + + // Récupérer l'assistante maternelle pour un enfant + Future getAssistantForChild(String childId) async { + try { + // TODO: Implémenter l'appel API + return AssistantModel( + id: 'am1', + firstName: 'Marie', + lastName: 'Dupont', + hourlyRate: 10.0, + dailyFees: 80.0, + status: AssistantStatus.available, + photoUrl: 'assets/images/assistant1.jpg', + address: '123 rue des Lilas', + phone: '0123456789', + ); + } catch (e) { + throw Exception('Erreur lors de la récupération de l\'assistante: $e'); + } + } + + // Récupérer les événements pour un enfant + Future> getEventsForChild(String childId) async { + try { + // TODO: Implémenter l'appel API + return [ + EventModel( + id: 'evt1', + title: 'Rendez-vous médical', + startDate: DateTime.now().add(const Duration(days: 2)), + type: EventType.parentVacation, + status: EventStatus.pending, + description: 'Visite de routine', + childId: childId, + ), + ]; + } catch (e) { + throw Exception('Erreur lors de la récupération des événements: $e'); + } + } + + // Récupérer tous les événements à venir + Future> getUpcomingEvents() async { + try { + // TODO: Implémenter l'appel API + return [ + EventModel( + id: 'evt1', + title: 'Activité peinture', + startDate: DateTime.now().add(const Duration(days: 1)), + endDate: DateTime.now().add(const Duration(days: 1, hours: 2)), + type: EventType.parentVacation, + status: EventStatus.pending, + description: 'Atelier créatif', + childId: '1', + ), + ]; + } catch (e) { + throw Exception('Erreur lors de la récupération des événements: $e'); + } + } + + // Récupérer les contrats + Future> getContracts() async { + try { + // TODO: Implémenter l'appel API + return [ + ContractModel( + id: 'contract1', + childId: '1', + assistantId: 'am1', + startDate: DateTime(2023, 9, 1), + endDate: DateTime(2024, 8, 31), + status: ContractStatus.pending, + hourlyRate: 10.0, + createdAt: DateTime.now(), + ), + ]; + } catch (e) { + throw Exception('Erreur lors de la récupération des contrats: $e'); + } + } + + // Récupérer les contrats pour un enfant spécifique + Future> getContractsForChild(String childId) async { + try { + // TODO: Implémenter l'appel API + return [ + ContractModel( + id: 'contract1', + childId: childId, + assistantId: 'am1', + startDate: DateTime(2023, 9, 1), + endDate: DateTime(2024, 8, 31), + status: ContractStatus.active, + hourlyRate: 10.0, + createdAt: DateTime.now(), + ), + ]; + } catch (e) { + throw Exception('Erreur lors de la récupération des contrats: $e'); + } + } + + // Récupérer les conversations + Future> getConversations() async { + try { + // TODO: Implémenter l'appel API + return [ + ConversationModel( + id: 'conv1', + title: 'Conversation avec Marie Dupont', + participantIds: ['am1'], + messages: [ + MessageModel( + id: 'msg1', + content: 'Bonjour, comment ça va ?', + senderId: 'am1', + sentAt: DateTime.now().subtract(const Duration(hours: 2)), + status: MessageStatus.read, + ), + MessageModel( + id: 'msg2', + content: 'Tout va bien, merci !', + senderId: 'parent1', + sentAt: DateTime.now().subtract(const Duration(hours: 1, minutes: 30)), + status: MessageStatus.read, + ), + ], + lastMessageAt: DateTime.now().subtract(const Duration(hours: 2)), + unreadCount: 2, + ), + ]; + } catch (e) { + throw Exception('Erreur lors de la récupération des conversations: $e'); + } + } + + // Récupérer les notifications + Future> getNotifications() async { + try { + // TODO: Implémenter l'appel API + return [ + NotificationModel( + id: 'notif1', + title: 'Nouveau message', + createdAt: DateTime.now(), + isRead: false, + type: NotificationType.contractPending, + content: 'Votre contrat est en attente', + ), + ]; + } catch (e) { + throw Exception('Erreur lors de la récupération des notifications: $e'); + } + } + + // Marquer une notification comme lue + Future markNotificationAsRead(String notificationId) async { + try { + // TODO: Implémenter l'appel API + } catch (e) { + throw Exception('Erreur lors du marquage de la notification: $e'); + } + } +} \ No newline at end of file diff --git a/frontend/lib/services/login_navigation_service.dart b/frontend/lib/services/login_navigation_service.dart new file mode 100644 index 0000000..6b6ca24 --- /dev/null +++ b/frontend/lib/services/login_navigation_service.dart @@ -0,0 +1,20 @@ +import 'package:flutter/cupertino.dart'; + +class NavigationService { + static void handleLoginSuccess(BuildContext context, String role) { + switch (role) { + case 'admin': + Navigator.pushReplacementNamed(context, '/admin_dashboard'); + break; + case 'gestionnaire': + Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard'); + break; + case 'parent': + Navigator.pushReplacementNamed(context, '/parent-dashboard'); + break; + case 'assistante_maternelle': + Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard'); + break; + } + } +} \ No newline at end of file diff --git a/frontend/lib/utils/data_generator.dart b/frontend/lib/utils/data_generator.dart index a6f9077..d7ffde5 100644 --- a/frontend/lib/utils/data_generator.dart +++ b/frontend/lib/utils/data_generator.dart @@ -3,6 +3,11 @@ import 'dart:math'; class DataGenerator { static final Random _random = Random(); + // Méthodes publiques pour la génération de nombres aléatoires + static int randomInt(int max) => _random.nextInt(max); + static int randomIntInRange(int min, int max) => min + _random.nextInt(max - min); + static bool randomBool() => _random.nextBool(); + static final List _firstNames = [ 'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Félix', 'Gabrielle', 'Hugo', 'Inès', 'Jules', 'Léa', 'Manon', 'Nathan', 'Oscar', 'Pauline', 'Quentin', 'Raphaël', 'Sophie', 'Théo', 'Victoire' diff --git a/frontend/lib/widgets/FormFieldConfig.dart b/frontend/lib/widgets/FormFieldConfig.dart new file mode 100644 index 0000000..6e14cc7 --- /dev/null +++ b/frontend/lib/widgets/FormFieldConfig.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:p_tits_pas/widgets/custom_app_text_field.dart'; + +class ModularFormField { + final String label; + final String hint; + final TextEditingController controller; + final TextInputType? keyboardType; + final bool isPassword; + final String? Function(String?)? validator; + final bool isRequired; + final int flex; + + ModularFormField({ + required this.label, + required this.hint, + required this.controller, + this.keyboardType, + this.isPassword = false, + this.validator, + this.isRequired = false, + this.flex = 1, + }); +} + +class ModularForm extends StatelessWidget { + final List> fieldGroups; + final GlobalKey formKey; + final double? width; + final EdgeInsets padding; + final String? title; + final VoidCallback? onSubmit; + final String submitLabel; + + const ModularForm({ + super.key, + required this.fieldGroups, + required this.formKey, + this.width, + this.padding = const EdgeInsets.all(20), + this.title, + this.onSubmit, + this.submitLabel = "Suivant", + }); + + @override + Widget build(BuildContext context) { + return Container( + width: width ?? MediaQuery.of(context).size.width * 0.6, + padding: padding, + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null) ...[ + Text( + title!, + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + ], + ...fieldGroups.map((group) { + return Column( + children: [ + Row( + children: group.asMap().entries.map((entry) { + final index = entry.key; + final field = entry.value; + + return [ + Expanded( + flex: field.flex, + child: CustomAppTextField( + controller: field.controller, + labelText: field.label, + hintText: field.hint, + obscureText: field.isPassword, + keyboardType: field.keyboardType ?? TextInputType.text, + validator: field.validator, + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, // CORRECTION PRINCIPALE + ), + ), + // Ajouter un espaceur entre les champs (sauf pour le dernier) + if (index < group.length - 1) + const Expanded( + flex: 1, + child: SizedBox(), // Espacement de 4% comme dans l'original + ), + ]; + }).expand((element) => element).toList(), + ), + const SizedBox(height: 20), + ], + ); + }).toList(), + if (onSubmit != null) + Center( + child: ElevatedButton( + onPressed: onSubmit, + child: Text(submitLabel), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/Summary.dart b/frontend/lib/widgets/Summary.dart new file mode 100644 index 0000000..7f486b7 --- /dev/null +++ b/frontend/lib/widgets/Summary.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class SummaryCard extends StatelessWidget { + final String backgroundImagePath; + final String title; + final List content; + final VoidCallback onEdit; + + const SummaryCard({ + super.key, + required this.backgroundImagePath, + required this.title, + required this.content, + required this.onEdit, + }); + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 2.0, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(backgroundImagePath), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + title, + style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + IconButton( + icon: const Icon(Icons.edit, color: Colors.black54, size: 28), + onPressed: onEdit, + tooltip: 'Modifier', + ), + ], + ), + const SizedBox(height: 18), + Expanded( + child: Column( + children: content, + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart new file mode 100644 index 0000000..220f946 --- /dev/null +++ b/frontend/lib/widgets/admin/assistante_maternelle_management_widget.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +class AssistanteMaternelleManagementWidget extends StatelessWidget { + const AssistanteMaternelleManagementWidget({super.key}); + + @override + Widget build(BuildContext context) { + final assistantes = [ + { + "nom": "Marie Dupont", + "numeroAgrement": "AG123456", + "zone": "Paris 14", + "capacite": 3, + }, + { + "nom": "Claire Martin", + "numeroAgrement": "AG654321", + "zone": "Lyon 7", + "capacite": 2, + }, + ]; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🔎 Zone de filtre + _buildFilterSection(), + + const SizedBox(height: 16), + + // 📋 Liste des assistantes + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: assistantes.length, + itemBuilder: (context, index) { + final assistante = assistantes[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: ListTile( + leading: const Icon(Icons.face), + title: Text(assistante['nom'].toString()), + subtitle: Text( + "N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + // TODO: Ajouter modification + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + // TODO: Ajouter suppression + }, + ), + ], + ), + ), + ); + }, + ), + ], + ), + ); + } + + Widget _buildFilterSection() { + return Wrap( + spacing: 16, + runSpacing: 8, + children: [ + SizedBox( + width: 200, + child: TextField( + decoration: const InputDecoration( + labelText: "Zone géographique", + border: OutlineInputBorder(), + ), + onChanged: (value) { + // TODO: Ajouter logique de filtrage par zone + }, + ), + ), + SizedBox( + width: 200, + child: TextField( + decoration: const InputDecoration( + labelText: "Capacité minimum", + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + onChanged: (value) { + // TODO: Ajouter logique de filtrage par capacité + }, + ), + ), + ], + ); + } +} diff --git a/frontend/lib/widgets/admin/dashboard_admin.dart b/frontend/lib/widgets/admin/dashboard_admin.dart new file mode 100644 index 0000000..6f63760 --- /dev/null +++ b/frontend/lib/widgets/admin/dashboard_admin.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; + +class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidget { + final int selectedIndex; + final ValueChanged onTabChange; + + const DashboardAppBarAdmin({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10); + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 768; + return AppBar( + elevation: 0, + automaticallyImplyLeading: false, + title: Row( + children: [ + SizedBox(width: MediaQuery.of(context).size.width * 0.19), + const Text( + "P'tit Pas", + style: TextStyle( + color: Color(0xFF9CC5C0), + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + + // Navigation principale + _buildNavItem(context, 'Gestionnaires', 0), + const SizedBox(width: 24), + _buildNavItem(context, 'Parents', 1), + const SizedBox(width: 24), + _buildNavItem(context, 'Assistantes maternelles', 2), + const SizedBox(width: 24), + _buildNavItem(context, 'Administrateurs', 3), + ], + ), + actions: isMobile + ? [_buildMobileMenu(context)] + : [ + // Nom de l'utilisateur + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: Text( + 'Admin', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + + // Bouton déconnexion + Padding( + padding: const EdgeInsets.only(right: 16), + child: TextButton( + onPressed: () => _handleLogout(context), + style: TextButton.styleFrom( + backgroundColor: const Color(0xFF9CC5C0), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: const Text('Se déconnecter'), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + ], + ); + } + + Widget _buildNavItem(BuildContext context, String title, int index) { + final bool isActive = index == selectedIndex; + return InkWell( + onTap: () => onTabChange(index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: isActive ? null : Border.all(color: Colors.black26), + ), + child: Text( + title, + style: TextStyle( + color: isActive ? Colors.white : Colors.black, + fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, + fontSize: 14, + ), + ), + ), + ); +} + + + Widget _buildMobileMenu(BuildContext context) { + return PopupMenuButton( + icon: const Icon(Icons.menu, color: Colors.white), + onSelected: (value) { + if (value == 4) { + _handleLogout(context); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 0, child: Text("Gestionnaires")), + const PopupMenuItem(value: 1, child: Text("Parents")), + const PopupMenuItem(value: 2, child: Text("Assistantes maternelles")), + const PopupMenuItem(value: 3, child: Text("Administrateurs")), + const PopupMenuDivider(), + const PopupMenuItem(value: 4, child: Text("Se déconnecter")), + ], + ); + } + + void _handleLogout(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Déconnexion'), + content: const Text('Êtes-vous sûr de vouloir vous déconnecter ?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + // TODO: Implémenter la logique de déconnexion + }, + child: const Text('Déconnecter'), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/admin/gestionnaire_card.dart b/frontend/lib/widgets/admin/gestionnaire_card.dart new file mode 100644 index 0000000..5d80255 --- /dev/null +++ b/frontend/lib/widgets/admin/gestionnaire_card.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +class GestionnaireCard extends StatelessWidget { + final String name; + final String email; + + const GestionnaireCard({ + Key? key, + required this.name, + required this.email, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 🔹 Infos principales + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(name, style: const TextStyle(fontWeight: FontWeight.bold)), + Text(email, style: const TextStyle(color: Colors.grey)), + ], + ), + const SizedBox(height: 12), + + // 🔹 Attribution à des RPE (dropdown fictif ici) + Row( + children: [ + const Text("RPE attribué : "), + const SizedBox(width: 8), + DropdownButton( + value: "RPE 1", + items: const [ + DropdownMenuItem(value: "RPE 1", child: Text("RPE 1")), + DropdownMenuItem(value: "RPE 2", child: Text("RPE 2")), + DropdownMenuItem(value: "RPE 3", child: Text("RPE 3")), + ], + onChanged: (value) {}, + ), + ], + ), + const SizedBox(height: 12), + + // 🔹 Boutons d'action + Row( + children: [ + TextButton.icon( + onPressed: () { + // Réinitialisation mot de passe + }, + icon: const Icon(Icons.lock_reset), + label: const Text("Réinitialiser MDP"), + ), + const SizedBox(width: 12), + TextButton.icon( + onPressed: () { + // Suppression du compte + }, + icon: const Icon(Icons.delete, color: Colors.red), + label: const Text("Supprimer", style: TextStyle(color: Colors.red)), + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/frontend/lib/widgets/admin/gestionnaire_management_widget.dart b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart new file mode 100644 index 0000000..3f5d6c2 --- /dev/null +++ b/frontend/lib/widgets/admin/gestionnaire_management_widget.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/widgets/admin/gestionnaire_card.dart'; + +class GestionnaireManagementWidget extends StatelessWidget { + const GestionnaireManagementWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 🔹 Barre du haut avec bouton + Row( + children: [ + const Expanded( + child: TextField( + decoration: InputDecoration( + hintText: "Rechercher un gestionnaire...", + prefixIcon: Icon(Icons.search), + border: OutlineInputBorder(), + ), + ), + ), + const SizedBox(width: 16), + ElevatedButton.icon( + onPressed: () { + // Rediriger vers la page de création + }, + icon: const Icon(Icons.add), + label: const Text("Créer un gestionnaire"), + ), + ], + ), + const SizedBox(height: 24), + + // 🔹 Liste des gestionnaires + Expanded( + child: ListView.builder( + itemCount: 5, // À remplacer par liste dynamique + itemBuilder: (context, index) { + return GestionnaireCard( + name: "Dupont $index", + email: "dupont$index@mail.com", + ); + }, + ), + ) + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/admin/parent_managmant_widget.dart b/frontend/lib/widgets/admin/parent_managmant_widget.dart new file mode 100644 index 0000000..1bf78a5 --- /dev/null +++ b/frontend/lib/widgets/admin/parent_managmant_widget.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; + +class ParentManagementWidget extends StatelessWidget { + const ParentManagementWidget({super.key}); + + @override + Widget build(BuildContext context) { + // 🔁 Simulation de données parents + final parents = [ + { + "nom": "Jean Dupuis", + "email": "jean.dupuis@email.com", + "statut": "Actif", + "enfants": 2, + }, + { + "nom": "Lucie Morel", + "email": "lucie.morel@email.com", + "statut": "En attente", + "enfants": 1, + }, + ]; + + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + _buildSearchSection(), + + const SizedBox(height: 16), + + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: parents.length, + itemBuilder: (context, index) { + final parent = parents[index]; + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: ListTile( + leading: const Icon(Icons.person_outline), + title: Text(parent['nom'].toString()), + subtitle: Text( + "${parent['email']}\nStatut : ${parent['statut']} | Enfants : ${parent['enfants']}", + ), + isThreeLine: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.visibility), + tooltip: "Voir dossier", + onPressed: () { + // TODO: Voir le statut du dossier + }, + ), + IconButton( + icon: const Icon(Icons.edit), + tooltip: "Modifier", + onPressed: () { + // TODO: Modifier parent + }, + ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: "Supprimer", + onPressed: () { + // TODO: Supprimer compte + }, + ), + ], + ), + ), + ); + }, + ), + ], + ) + ); + } + + Widget _buildSearchSection() { + return Wrap( + spacing: 16, + runSpacing: 8, + children: [ + SizedBox( + width: 220, + child: TextField( + decoration: const InputDecoration( + labelText: "Nom du parent", + border: OutlineInputBorder(), + ), + onChanged: (value) { + // TODO: Ajouter logique de recherche + }, + ), + ), + SizedBox( + width: 220, + child: DropdownButtonFormField( + decoration: const InputDecoration( + labelText: "Statut", + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: "Actif", child: Text("Actif")), + DropdownMenuItem(value: "En attente", child: Text("En attente")), + DropdownMenuItem(value: "Supprimé", child: Text("Supprimé")), + ], + onChanged: (value) { + // TODO: Ajouter logique de filtrage + }, + ), + ), + ], + ); + } +} diff --git a/frontend/lib/widgets/app_footer.dart b/frontend/lib/widgets/app_footer.dart new file mode 100644 index 0000000..a4559d2 --- /dev/null +++ b/frontend/lib/widgets/app_footer.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:p_tits_pas/models/m_dashbord/child_model.dart'; +import 'package:p_tits_pas/services/bug_report_service.dart'; + +class AppFooter extends StatelessWidget { + const AppFooter({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + decoration: BoxDecoration( + // color: Colors.white, + border: Border( + top: BorderSide(color: Colors.grey.shade300), + ), + ), + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 768) { + return _buildMobileFooter(context); + } else { + return _buildDesktopFooter(context); + } + }, + ), + ); + } + + Widget _buildDesktopFooter(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildFooterLink(context, 'Contact support', () => _handleContactSupport(context)), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + // _buildFooterDivider(), + _buildFooterLink(context, 'Signaler un bug', () => _handleReportBug(context)), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + // _buildFooterDivider(), + _buildFooterLink(context, 'Mentions légales', () => _handleLegalNotices(context)), + // _buildFooterDivider(), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + _buildFooterLink(context, 'Politique de confidentialité', () => _handlePrivacyPolicy(context)), + ], + ); + } + + Widget _buildMobileFooter(BuildContext context) { + return PopupMenuButton( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: const [ + Icon(Icons.info_outline, size: 20), + SizedBox(width: 8), + Text('Informations'), + Icon(Icons.keyboard_arrow_down), + ], + ), + ), + itemBuilder: (context) => [ + const PopupMenuItem(value: 'support', child: Text('Contact support')), + const PopupMenuItem(value: 'bug', child: Text('Signaler un bug')), + const PopupMenuItem(value: 'legal', child: Text('Mentions légales')), + const PopupMenuItem(value: 'privacy', child: Text('Politique de confidentialité')), + ], + onSelected: (value) { + switch (value) { + case 'support': + _handleContactSupport(context); + break; + case 'bug': + _handleReportBug(context); + break; + case 'legal': + _handleLegalNotices(context); + break; + case 'privacy': + _handlePrivacyPolicy(context); + break; + } + }, + ); + } + + Widget _buildFooterLink(BuildContext context, String text, VoidCallback onTap) { + return InkWell( + onTap: onTap, + child: Text( + text, + style: TextStyle( + color: Theme.of(context).primaryColor, + ), + ), + ); + } + + void _handleReportBug(BuildContext context) { + final TextEditingController controller = TextEditingController(); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text( + 'Signaler un bug', + style: GoogleFonts.merienda(), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: controller, + maxLines: 5, + decoration: InputDecoration( + hintText: 'Décrivez le problème rencontré...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Annuler', + style: GoogleFonts.merienda(), + ), + ), + TextButton( + onPressed: () async { + if (controller.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Veuillez décrire le problème', + style: GoogleFonts.merienda(), + ), + ), + ); + return; + } + + try { + await BugReportService.sendReport(controller.text); + if (context.mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Rapport envoyé avec succès', + style: GoogleFonts.merienda(), + ), + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Erreur lors de l\'envoi du rapport', + style: GoogleFonts.merienda(), + ), + ), + ); + } + } + }, + child: Text( + 'Envoyer', + style: GoogleFonts.merienda(), + ), + ), + ], + ), + ); + } + + void _handleLegalNotices(BuildContext context) { + // Handle legal notices action + Navigator.pushNamed(context, '/legal'); + } + + void _handlePrivacyPolicy(BuildContext context) { + // Handle privacy policy action + Navigator.pushNamed(context, '/privacy'); + } + + void _handleContactSupport(BuildContext context) { + // Handle contact support action + // Navigator.pushNamed(context, '/support'); + } + + Widget _buildFooterDivider() { + return Divider( + color: Colors.grey[300], + thickness: 1, + height: 40, + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/child_card_widget.dart b/frontend/lib/widgets/child_card_widget.dart new file mode 100644 index 0000000..93f6044 --- /dev/null +++ b/frontend/lib/widgets/child_card_widget.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:io' show File; +import 'package:flutter/foundation.dart' show kIsWeb; +import '../models/user_registration_data.dart'; +import '../models/card_assets.dart'; +import 'custom_app_text_field.dart'; +import 'app_custom_checkbox.dart'; +import 'hover_relief_widget.dart'; + +/// Widget pour afficher et éditer une carte enfant +/// Utilisé dans le workflow d'inscription des parents +class ChildCardWidget extends StatefulWidget { + final ChildData childData; + final int childIndex; + final VoidCallback onPickImage; + final VoidCallback onDateSelect; + final ValueChanged onFirstNameChanged; + final ValueChanged onLastNameChanged; + final ValueChanged onTogglePhotoConsent; + final ValueChanged onToggleMultipleBirth; + final ValueChanged onToggleIsUnborn; + final VoidCallback onRemove; + final bool canBeRemoved; + + const ChildCardWidget({ + required Key key, + required this.childData, + required this.childIndex, + required this.onPickImage, + required this.onDateSelect, + required this.onFirstNameChanged, + required this.onLastNameChanged, + required this.onTogglePhotoConsent, + required this.onToggleMultipleBirth, + required this.onToggleIsUnborn, + required this.onRemove, + required this.canBeRemoved, + }) : super(key: key); + + @override + State createState() => _ChildCardWidgetState(); +} + +class _ChildCardWidgetState extends State { + late TextEditingController _firstNameController; + late TextEditingController _lastNameController; + late TextEditingController _dobController; + + @override + void initState() { + super.initState(); + // Initialiser les contrôleurs avec les données du widget + _firstNameController = TextEditingController(text: widget.childData.firstName); + _lastNameController = TextEditingController(text: widget.childData.lastName); + _dobController = TextEditingController(text: widget.childData.dob); + + // Ajouter des listeners pour mettre à jour les données sources via les callbacks + _firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text)); + _lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text)); + // Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici + } + + @override + void didUpdateWidget(covariant ChildCardWidget oldWidget) { + super.didUpdateWidget(oldWidget); + // Mettre à jour les contrôleurs si les données externes changent + // (peut arriver si on recharge l'état global) + if (widget.childData.firstName != _firstNameController.text) { + _firstNameController.text = widget.childData.firstName; + } + if (widget.childData.lastName != _lastNameController.text) { + _lastNameController.text = widget.childData.lastName; + } + if (widget.childData.dob != _dobController.text) { + _dobController.text = widget.childData.dob; + } + } + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + _dobController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final File? currentChildImage = widget.childData.imageFile; + // Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond + final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender + ? Colors.purple.shade200 + : (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs + final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90); + final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); + + return Container( + width: 345.0 * 1.1, // 379.5 + height: 570.0 * 1.2, // 684.0 + padding: const EdgeInsets.all(22.0 * 1.1), // 24.2 + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover), + borderRadius: BorderRadius.circular(20 * 1.1), // 22 + ), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + HoverReliefWidget( + onPressed: widget.onPickImage, + borderRadius: BorderRadius.circular(10), + initialShadowColor: initialPhotoShadow, + hoverShadowColor: hoverPhotoShadow, + child: SizedBox( + height: 200.0, + width: 200.0, + child: Center( + child: Padding( + padding: const EdgeInsets.all(5.0 * 1.1), // 5.5 + child: currentChildImage != null + ? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover)) + : Image.asset('assets/images/photo.png', fit: BoxFit.contain), + ), + ), + ), + ), + const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)), + Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor), + ], + ), + const SizedBox(height: 9.0 * 1.1), // 9.9 + CustomAppTextField( + controller: _firstNameController, + labelText: 'Prénom', + hintText: 'Facultatif si à naître', + isRequired: !widget.childData.isUnbornChild, + fieldHeight: 55.0 * 1.1, // 60.5 + ), + const SizedBox(height: 6.0 * 1.1), // 6.6 + CustomAppTextField( + controller: _lastNameController, + labelText: 'Nom', + hintText: 'Nom de l\'enfant', + enabled: true, + fieldHeight: 55.0 * 1.1, // 60.5 + ), + const SizedBox(height: 9.0 * 1.1), // 9.9 + CustomAppTextField( + controller: _dobController, + labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', + hintText: 'JJ/MM/AAAA', + readOnly: true, + onTap: widget.onDateSelect, + suffixIcon: Icons.calendar_today, + fieldHeight: 55.0 * 1.1, // 60.5 + ), + const SizedBox(height: 11.0 * 1.1), // 12.1 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppCustomCheckbox( + label: 'Consentement photo', + value: widget.childData.photoConsent, + onChanged: widget.onTogglePhotoConsent, + checkboxSize: 22.0 * 1.1, // 24.2 + ), + const SizedBox(height: 6.0 * 1.1), // 6.6 + AppCustomCheckbox( + label: 'Naissance multiple', + value: widget.childData.multipleBirth, + onChanged: widget.onToggleMultipleBirth, + checkboxSize: 22.0 * 1.1, // 24.2 + ), + ], + ), + ], + ), + if (widget.canBeRemoved) + Positioned( + top: -5, right: -5, + child: InkWell( + onTap: widget.onRemove, + customBorder: const CircleBorder(), + child: Image.asset( + 'assets/images/red_cross2.png', + width: 36, + height: 36, + fit: BoxFit.contain, + ), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/dashbord_parent/ChildrenSidebarwidget.dart b/frontend/lib/widgets/dashbord_parent/ChildrenSidebarwidget.dart new file mode 100644 index 0000000..de46d41 --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/ChildrenSidebarwidget.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +class Childrensidebarwidget extends StatelessWidget{ + final void Function(String childId) onChildSelected; + + const Childrensidebarwidget({ + Key? key, + required this.onChildSelected, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final children = [ + {'id': '1', 'name': 'Léna', 'photo': null, 'status': 'Actif'}, + {'id': '2', 'name': 'Noé', 'photo': null, 'status': 'Inactif'}, + ]; + + return Container( + color: const Color(0xFFF7F7F7), + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // Avatar parent + bouton + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const CircleAvatar(radius: 24, child: Icon(Icons.person)), + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + // Naviguer vers ajout d'enfant + }, + ) + ], + ), + const SizedBox(height: 16), + const Text("Mes enfants", style: TextStyle(fontWeight: FontWeight.bold)), + + const SizedBox(height: 16), + // Liste des enfants + ...children.map((child) { + return GestureDetector( + onTap: () => onChildSelected(child['id']!), + child: Card( + color: child['status'] == 'Actif' ? Colors.teal.shade50 : Colors.white, + child: ListTile( + leading: const CircleAvatar(child: Icon(Icons.child_care)), + title: Text(child['name']!), + subtitle: Text(child['status']!), + ), + ), + ); + }).toList() + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/dashbord_parent/app_layout.dart b/frontend/lib/widgets/dashbord_parent/app_layout.dart new file mode 100644 index 0000000..d1dbd45 --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/app_layout.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class AppLayout extends StatelessWidget { + final PreferredSizeWidget appBar; + final Widget body; + final Widget? footer; + + const AppLayout({ + Key? key, + required this.appBar, + required this.body, + this.footer, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F7FA), + appBar: appBar, + body: Column( + children: [ + Expanded(child: body), + if (footer != null) footer!, + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/dashbord_parent/children_sidebar.dart b/frontend/lib/widgets/dashbord_parent/children_sidebar.dart new file mode 100644 index 0000000..0dd681f --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/children_sidebar.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/m_dashbord/child_model.dart'; + +class ChildrenSidebar extends StatelessWidget { + final List children; + final String? selectedChildId; + final Function(String) onChildSelected; + final VoidCallback onAddChild; + final bool isCompact; + final bool isMobile; + + const ChildrenSidebar({ + Key? key, + required this.children, + this.selectedChildId, + required this.onChildSelected, + required this.onAddChild, + this.isCompact = false, + this.isMobile = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(isMobile ? 16 : 24), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: 20), + _buildAddChildButton(context), + const SizedBox(height: 16), + Expanded(child: _buildChildrenList()), + ], + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Row( + children: [ + // UserAvatar( + // size: isCompact ? 40 : 60, + // name: 'Emma Dupont', // TODO: Récupérer depuis le contexte utilisateur + // ), + if (!isCompact) ...[ + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text( + 'Emma Dupont', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + Icon(Icons.keyboard_arrow_down), + ], + ), + ), + ], + ], + ); + } + + Widget _buildAddChildButton(BuildContext context) { + return SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: onAddChild, + icon: const Icon(Icons.add), + label: Text(isCompact ? 'Ajouter' : 'Ajouter un enfant'), + style: OutlinedButton.styleFrom( + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: isCompact ? 8 : 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ); + } + + Widget _buildChildrenList() { + if (children.isEmpty) { + return const Center( + child: Text( + 'Aucun enfant ajouté', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + ); + } + + return ListView.separated( + itemCount: children.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final child = children[index]; + final isSelected = child.id == selectedChildId; + + return _buildChildCard(context, child, isSelected); + }, + ); + } + + Widget _buildChildCard(BuildContext context, ChildModel child, bool isSelected) { + return InkWell( + onTap: () => onChildSelected(child.id), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF9CC5C0).withOpacity(0.1) : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? const Color(0xFF9CC5C0) : Colors.grey.shade300, + width: isSelected ? 2 : 1, + ), + ), + child: Row( + children: [ + // UserAvatar( + // // size: isCompact ? 32 : 40, + // // name: child.fullName, + // // imageUrl: child.photoUrl, + // ), + if (!isCompact) ...[ + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + child.firstName, + style: TextStyle( + fontSize: 14, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + ), + ), + const SizedBox(height: 4), + _buildChildStatus(child.status), + ], + ), + ), + ], + ], + ), + ), + ); + } + + Widget _buildChildStatus(ChildStatus status) { + String label; + Color color; + + switch (status) { + case ChildStatus.withAssistant: + label = 'En garde'; + color = Colors.green; + break; + case ChildStatus.available: + label = 'Disponible'; + color = Colors.blue; + break; + case ChildStatus.onHoliday: + label = 'En vacances'; + color = Colors.orange; + break; + case ChildStatus.sick: + label = 'Malade'; + color = Colors.red; + break; + case ChildStatus.searching: + label = 'Recherche AM'; + color = Colors.purple; + break; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/dashbord_parent/dashboard_app_bar.dart b/frontend/lib/widgets/dashbord_parent/dashboard_app_bar.dart new file mode 100644 index 0000000..c89fd6e --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/dashboard_app_bar.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; + +class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { + final int selectedIndex; + final ValueChanged onTabChange; + + const DashboardAppBar({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key); + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10); + + @override + Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < 768; + return AppBar( + // backgroundColor: Colors.white, + elevation: 0, + title: Row( + children: [ + // Logo de la ville + // Container( + // height: 32, + // width: 32, + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(8), + // ), + // child: const Icon( + // Icons.location_city, + // color: Color(0xFF9CC5C0), + // size: 20, + // ), + // ), + SizedBox(width: MediaQuery.of(context).size.width * 0.19), + const Text( + "P'tit Pas", + style: TextStyle( + color: Color(0xFF9CC5C0), + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + + // Navigation principale + _buildNavItem(context, 'Mon tableau de bord', 0), + const SizedBox(width: 24), + _buildNavItem(context, 'Trouver une nounou', 1), + const SizedBox(width: 24), + _buildNavItem(context, 'Paramètres', 2), + ], + ), + actions: isMobile + ? [_buildMobileMenu(context)] + : [ + // Nom de l'utilisateur + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: Text( + 'Jean Dupont', + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + + // Bouton déconnexion + Padding( + padding: const EdgeInsets.only(right: 16), + child: TextButton( + onPressed: () => _handleLogout(context), + style: TextButton.styleFrom( + backgroundColor: const Color(0xFF9CC5C0), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + child: const Text('Se déconnecter'), + ), + ), + SizedBox(width: MediaQuery.of(context).size.width * 0.1), + ], + ); + } + + Widget _buildNavItem(BuildContext context, String title, int index) { + final bool isActive = index == selectedIndex; + return InkWell( + onTap: () => onTabChange(index), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent, + borderRadius: BorderRadius.circular(20), + border: isActive ? null : Border.all(color: Colors.black26), + ), + child: Text( + title, + style: TextStyle( + color: isActive ? Colors.white : Colors.black, + fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, + fontSize: 14, + ), + ), + ), + ); +} + + + Widget _buildMobileMenu(BuildContext context) { + return PopupMenuButton( + icon: const Icon(Icons.menu, color: Colors.white), + onSelected: (value) { + if (value == 3) { + _handleLogout(context); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 0, child: Text("Mon tableau de bord")), + const PopupMenuItem(value: 1, child: Text("Trouver une nounou")), + const PopupMenuItem(value: 2, child: Text("Paramètres")), + const PopupMenuDivider(), + const PopupMenuItem(value: 3, child: Text("Se déconnecter")), + ], + ); + } + + void _handleLogout(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Déconnexion'), + content: const Text('Êtes-vous sûr de vouloir vous déconnecter ?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + // TODO: Implémenter la logique de déconnexion + }, + child: const Text('Déconnecter'), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/dashbord_parent/wid_dashbord.dart b/frontend/lib/widgets/dashbord_parent/wid_dashbord.dart new file mode 100644 index 0000000..6ed87cf --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/wid_dashbord.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/widgets/dashbord_parent/ChildrenSidebarwidget.dart'; +import 'package:p_tits_pas/widgets/dashbord_parent/children_sidebar.dart'; +import 'package:p_tits_pas/widgets/dashbord_parent/wid_mainContentArea.dart'; +import 'package:p_tits_pas/widgets/messaging_sidebar.dart'; + +Widget Dashbord_body() { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 1️⃣ Colonne de gauche : enfants + SizedBox( + width: 250, + child: Childrensidebarwidget( + onChildSelected: (childId) { + // Met à jour l'enfant sélectionné + // Tu peux stocker cet ID dans un state `selectedChildId` + }, + ), + ), + + Expanded( + flex: 2, + child: WMainContentArea( + // Passe l’enfant sélectionné si besoin + ), + ), + + ], + ); +} diff --git a/frontend/lib/widgets/dashbord_parent/wid_mainContentArea.dart b/frontend/lib/widgets/dashbord_parent/wid_mainContentArea.dart new file mode 100644 index 0000000..6bc8d07 --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/wid_mainContentArea.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/widgets/messaging_sidebar.dart'; + +class WMainContentArea extends StatelessWidget { + const WMainContentArea({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 🔷 Informations assistante maternelle (ligne complète) + Card( + margin: const EdgeInsets.only(bottom: 16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const CircleAvatar( + radius: 30, + backgroundImage: AssetImage("assets/images/am_photo.jpg"), // à adapter + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text("Julie Dupont", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + SizedBox(height: 4), + Text("Taux horaire : 10€/h"), + Text("Frais journaliers : 5€"), + ], + ), + ), + ElevatedButton( + onPressed: () { + // Ouvrir le contrat + }, + child: const Text("Voir le contrat"), + ) + ], + ), + ), + ), + + // 🔷 Deux colonnes : planning + messagerie + Expanded( + child: Row( + children: [ + // 📆 Planning de garde + Expanded( + flex: 2, + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text("Planning de garde", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + SizedBox(height: 12), + Expanded( + child: Center( + child: Text("Composant calendrier à intégrer ici"), + ), + ) + ], + ), + ), + ), + ), + + const SizedBox(width: 16), + + // 💬 Messagerie + Expanded( + flex: 1, + child: MessagingSidebar( + conversations: [], + notifications: [], + isCompact: false, + isMobile: false, + ), + ), + ], + ), + ) + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/main_content_area.dart b/frontend/lib/widgets/main_content_area.dart new file mode 100644 index 0000000..6b8c0ac --- /dev/null +++ b/frontend/lib/widgets/main_content_area.dart @@ -0,0 +1,326 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/child_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/contract_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/event_model.dart'; + +class MainContentArea extends StatelessWidget { + final ChildModel? selectedChild; + final AssistantModel? selectedAssistant; + final List events; + final List contracts; + final bool showOnlyCalendar; + final bool showOnlyContracts; + + const MainContentArea({ + Key? key, + this.selectedChild, + this.selectedAssistant, + required this.events, + required this.contracts, + this.showOnlyCalendar = false, + this.showOnlyContracts = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!showOnlyCalendar && !showOnlyContracts) ...[ + if (selectedAssistant != null) _buildAssistantProfile(), + const SizedBox(height: 24), + ], + + if (showOnlyContracts || (!showOnlyCalendar && !showOnlyContracts)) ...[ + _buildContractsSection(), + if (!showOnlyContracts) const SizedBox(height: 24), + ], + + if (showOnlyCalendar || (!showOnlyCalendar && !showOnlyContracts)) ...[ + Expanded(child: _buildCalendarSection()), + ], + ], + ), + ); + } + + Widget _buildAssistantProfile() { + if (selectedAssistant == null) { + return _buildSearchAssistantCard(); + } + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: const Color(0xFF9CC5C0), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.person, + color: Colors.white, + size: 40, + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + selectedAssistant!.fullName, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + 'Taux horaire : ${selectedAssistant!.hourlyRateFormatted}', + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(width: 20), + Text( + 'Frais journaliers : ${selectedAssistant!.dailyFeesFormatted}', + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ], + ), + ), + ElevatedButton( + onPressed: () { + // TODO: Navigation vers le contrat + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF9CC5C0), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('Voir le contrat'), + ), + ], + ), + ); + } + + Widget _buildSearchAssistantCard() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + children: [ + Icon( + Icons.search, + size: 48, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + const Text( + 'Aucune assistante maternelle assignée', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'Trouvez une assistante maternelle pour votre enfant', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + // TODO: Navigation vers la recherche + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF9CC5C0), + foregroundColor: Colors.white, + ), + child: const Text('Rechercher une assistante maternelle'), + ), + ], + ), + ); + } + + Widget _buildCalendarSection() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Planning de garde pour ${selectedChild?.firstName ?? "votre enfant"}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + TextButton( + onPressed: () { + // TODO: Mode sélection de plage + }, + child: const Text('Mode sélection de plage'), + ), + ], + ), + const SizedBox(height: 20), + Expanded( + child: _buildCalendar(), + ), + ], + ), + ); + } + + Widget _buildCalendar() { + // Placeholder pour le calendrier - sera développé dans FRONT-11 + return const Center( + child: Text( + 'Calendrier à implémenter\n(FRONT-11)', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + ); + } + + Widget _buildContractsSection() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + spreadRadius: 1, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Contrats', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + if (contracts.isEmpty) + const Text( + 'Aucun contrat en cours', + style: TextStyle(color: Colors.grey), + ) + else + ...contracts.map((contract) => _buildContractItem(contract)), + ], + ), + ); + } + + Widget _buildContractItem(ContractModel contract) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: _getContractStatusColor(contract.status), + borderRadius: BorderRadius.circular(6), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text(contract.statusLabel), + ), + if (contract.needsSignature) + TextButton( + onPressed: () { + // TODO: Action signature + }, + child: const Text('Signer'), + ), + ], + ), + ); + } + + Color _getContractStatusColor(ContractStatus status) { + switch (status) { + case ContractStatus.draft: + return Colors.grey; + case ContractStatus.pending: + return Colors.orange; + case ContractStatus.active: + return Colors.green; + case ContractStatus.ended: + return Colors.blue; + case ContractStatus.cancelled: + return Colors.red; + } + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/messaging_sidebar.dart b/frontend/lib/widgets/messaging_sidebar.dart new file mode 100644 index 0000000..a417be4 --- /dev/null +++ b/frontend/lib/widgets/messaging_sidebar.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart'; +import 'package:p_tits_pas/models/m_dashbord/notification_model.dart'; + +class MessagingSidebar extends StatelessWidget { + final List conversations; + final List notifications; + final bool isCompact; + final bool isMobile; + + const MessagingSidebar({ + Key? key, + required this.conversations, + required this.notifications, + this.isCompact = false, + this.isMobile = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(isMobile ? 16 : 20), + color: Colors.white, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMessagingHeader(), + const SizedBox(height: 20), + Expanded( + child: _buildMessagingContent(), + ), + const SizedBox(height: 16), + _buildContactRPEButton(), + ], + ), + ); + } + + Widget _buildMessagingHeader() { + return const Text( + 'Messagerie avec Emma Dupont', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ); + } + + Widget _buildMessagingContent() { + return Column( + children: [ + // Messages existants + Expanded( + child: _buildMessagesList(), + ), + const SizedBox(height: 12), + // Zone de saisie + _buildMessageInput(), + ], + ); + } + + Widget _buildMessagesList() { + if (conversations.isEmpty) { + return const Center( + child: Text( + 'Aucun message', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + ), + ), + ); + } + + // Pour la démo, on affiche quelques messages fictifs + return ListView( + children: [ + _buildMessageBubble( + 'Bonjour, Emma a bien dormi aujourd\'hui.', + isFromCurrentUser: false, + timestamp: DateTime.now().subtract(const Duration(hours: 2)), + ), + const SizedBox(height: 12), + _buildMessageBubble( + 'Merci pour l\'information. Elle a bien mangé ?', + isFromCurrentUser: true, + timestamp: DateTime.now().subtract(const Duration(hours: 1)), + ), + ], + ); + } + + Widget _buildMessageBubble(String content, {required bool isFromCurrentUser, required DateTime timestamp}) { + return Align( + alignment: isFromCurrentUser ? Alignment.centerRight : Alignment.centerLeft, + child: Container( + constraints: const BoxConstraints(maxWidth: 250), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isFromCurrentUser + ? const Color(0xFF9CC5C0) + : Colors.grey.shade200, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + content, + style: TextStyle( + color: isFromCurrentUser ? Colors.white : Colors.black87, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + _formatTimestamp(timestamp), + style: TextStyle( + color: isFromCurrentUser + ? Colors.white.withOpacity(0.8) + : Colors.grey.shade600, + fontSize: 11, + ), + ), + ], + ), + ), + ); + } + + Widget _buildMessageInput() { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Expanded( + child: TextField( + decoration: InputDecoration( + hintText: 'Écrivez votre message...', + border: InputBorder.none, + isDense: true, + ), + maxLines: null, + ), + ), + const SizedBox(width: 8), + CircleAvatar( + radius: 16, + backgroundColor: const Color(0xFF9CC5C0), + child: IconButton( + iconSize: 16, + padding: EdgeInsets.zero, + onPressed: () { + // TODO: Envoyer le message + }, + icon: const Icon( + Icons.send, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } + + Widget _buildContactRPEButton() { + return SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () { + // TODO: Contacter le RPE + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Contacter le Relais Petite Enfance', + textAlign: TextAlign.center, + ), + ), + ); + } + + String _formatTimestamp(DateTime timestamp) { + final now = DateTime.now(); + final difference = now.difference(timestamp); + + if (difference.inMinutes < 1) { + return 'À l\'instant'; + } else if (difference.inHours < 1) { + return '${difference.inMinutes}m'; + } else if (difference.inDays < 1) { + return '${difference.inHours}h'; + } else { + return '${timestamp.day}/${timestamp.month}'; + } + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/personal_info_form_screen.dart b/frontend/lib/widgets/personal_info_form_screen.dart new file mode 100644 index 0000000..9881f57 --- /dev/null +++ b/frontend/lib/widgets/personal_info_form_screen.dart @@ -0,0 +1,405 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:math' as math; + +import 'custom_app_text_field.dart'; +import 'app_custom_checkbox.dart'; +import '../models/card_assets.dart'; + +/// Modèle de données pour le formulaire +class PersonalInfoData { + String firstName; + String lastName; + String phone; + String email; + String address; + String postalCode; + String city; + + PersonalInfoData({ + this.firstName = '', + this.lastName = '', + this.phone = '', + this.email = '', + this.address = '', + this.postalCode = '', + this.city = '', + }); +} + +/// Widget générique pour les formulaires d'informations personnelles +class PersonalInfoFormScreen extends StatefulWidget { + final String stepText; // Ex: "Étape 1/5" + final String title; // Ex: "Informations du Parent Principal" + final CardColorHorizontal cardColor; + final PersonalInfoData initialData; + final Function(PersonalInfoData data, {bool? hasSecondPerson, bool? sameAddress}) onSubmit; + final String previousRoute; + + // Options spécifiques pour Parent 2 + final bool showSecondPersonToggle; // Afficher "Il y a un 2ème parent" + final bool? initialHasSecondPerson; + final bool showSameAddressCheckbox; // Afficher "Même adresse que parent 1" + final bool? initialSameAddress; + final PersonalInfoData? referenceAddressData; // Pour pré-remplir si "même adresse" + + const PersonalInfoFormScreen({ + super.key, + required this.stepText, + required this.title, + required this.cardColor, + required this.initialData, + required this.onSubmit, + required this.previousRoute, + this.showSecondPersonToggle = false, + this.initialHasSecondPerson, + this.showSameAddressCheckbox = false, + this.initialSameAddress, + this.referenceAddressData, + }); + + @override + State createState() => _PersonalInfoFormScreenState(); +} + +class _PersonalInfoFormScreenState extends State { + final _formKey = GlobalKey(); + late TextEditingController _lastNameController; + late TextEditingController _firstNameController; + late TextEditingController _phoneController; + late TextEditingController _emailController; + late TextEditingController _addressController; + late TextEditingController _postalCodeController; + late TextEditingController _cityController; + + bool _hasSecondPerson = false; + bool _sameAddress = false; + bool _fieldsEnabled = true; + + @override + void initState() { + super.initState(); + _lastNameController = TextEditingController(text: widget.initialData.lastName); + _firstNameController = TextEditingController(text: widget.initialData.firstName); + _phoneController = TextEditingController(text: widget.initialData.phone); + _emailController = TextEditingController(text: widget.initialData.email); + _addressController = TextEditingController(text: widget.initialData.address); + _postalCodeController = TextEditingController(text: widget.initialData.postalCode); + _cityController = TextEditingController(text: widget.initialData.city); + + if (widget.showSecondPersonToggle) { + _hasSecondPerson = widget.initialHasSecondPerson ?? true; + _fieldsEnabled = _hasSecondPerson; + } + + if (widget.showSameAddressCheckbox) { + _sameAddress = widget.initialSameAddress ?? false; + _updateAddressFields(); + } + } + + @override + void dispose() { + _lastNameController.dispose(); + _firstNameController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + _addressController.dispose(); + _postalCodeController.dispose(); + _cityController.dispose(); + super.dispose(); + } + + void _updateAddressFields() { + if (_sameAddress && widget.referenceAddressData != null) { + _addressController.text = widget.referenceAddressData!.address; + _postalCodeController.text = widget.referenceAddressData!.postalCode; + _cityController.text = widget.referenceAddressData!.city; + } + } + + void _handleSubmit() { + if (_formKey.currentState!.validate()) { + final data = PersonalInfoData( + firstName: _firstNameController.text, + lastName: _lastNameController.text, + phone: _phoneController.text, + email: _emailController.text, + address: _addressController.text, + postalCode: _postalCodeController.text, + city: _cityController.text, + ); + + widget.onSubmit( + data, + hasSecondPerson: widget.showSecondPersonToggle ? _hasSecondPerson : null, + sameAddress: widget.showSameAddressCheckbox ? _sameAddress : null, + ); + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(widget.stepText, style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 10), + Text( + widget.title, + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Container( + width: screenSize.width * 0.6, + padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50), + constraints: const BoxConstraints(minHeight: 570), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(widget.cardColor.path), + fit: BoxFit.fill, + ), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Toggles "Ajouter Parent 2" et "Même Adresse" (uniquement pour Parent 2) + if (widget.showSecondPersonToggle) ...[ + Row( + children: [ + Expanded( + flex: 12, + child: Row( + children: [ + const Icon(Icons.person_add_alt_1, size: 20), + const SizedBox(width: 8), + Flexible( + child: Text( + 'Ajouter Parent 2 ?', + style: GoogleFonts.merienda(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), + ), + const Spacer(), + Switch( + value: _hasSecondPerson, + onChanged: (value) { + setState(() { + _hasSecondPerson = value; + _fieldsEnabled = value; + }); + }, + activeColor: Theme.of(context).primaryColor, + ), + ], + ), + ), + const Expanded(flex: 1, child: SizedBox()), + if (widget.showSameAddressCheckbox) + Expanded( + flex: 12, + child: Row( + children: [ + Icon( + Icons.home_work_outlined, + size: 20, + color: _fieldsEnabled ? null : Colors.grey, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + 'Même Adresse ?', + style: GoogleFonts.merienda( + color: _fieldsEnabled ? null : Colors.grey, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const Spacer(), + Switch( + value: _sameAddress, + onChanged: _fieldsEnabled ? (value) { + setState(() { + _sameAddress = value ?? false; + _updateAddressFields(); + }); + } : null, + activeColor: Theme.of(context).primaryColor, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + ], + Row( + children: [ + Expanded( + flex: 12, + child: CustomAppTextField( + controller: _lastNameController, + labelText: 'Nom', + hintText: 'Votre nom de famille', + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + enabled: _fieldsEnabled, + ), + ), + const Expanded(flex: 1, child: SizedBox()), + Expanded( + flex: 12, + child: CustomAppTextField( + controller: _firstNameController, + labelText: 'Prénom', + hintText: 'Votre prénom', + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + enabled: _fieldsEnabled, + ), + ), + ], + ), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + flex: 12, + child: CustomAppTextField( + controller: _phoneController, + labelText: 'Téléphone', + keyboardType: TextInputType.phone, + hintText: 'Votre numéro de téléphone', + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + enabled: _fieldsEnabled, + ), + ), + const Expanded(flex: 1, child: SizedBox()), + Expanded( + flex: 12, + child: CustomAppTextField( + controller: _emailController, + labelText: 'Email', + keyboardType: TextInputType.emailAddress, + hintText: 'Votre adresse e-mail', + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + enabled: _fieldsEnabled, + ), + ), + ], + ), + const SizedBox(height: 32), + CustomAppTextField( + controller: _addressController, + labelText: 'Adresse (N° et Rue)', + hintText: 'Numéro et nom de votre rue', + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + enabled: _fieldsEnabled && !_sameAddress, + ), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + flex: 1, + child: CustomAppTextField( + controller: _postalCodeController, + labelText: 'Code Postal', + keyboardType: TextInputType.number, + hintText: 'Code postal', + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + enabled: _fieldsEnabled && !_sameAddress, + ), + ), + const Expanded(flex: 1, child: SizedBox()), + Expanded( + flex: 4, + child: CustomAppTextField( + controller: _cityController, + labelText: 'Ville', + hintText: 'Votre ville', + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + enabled: _fieldsEnabled && !_sameAddress, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + // Chevrons + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(widget.previousRoute); + } + }, + tooltip: 'Retour', + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _handleSubmit, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/presentation_form_screen.dart b/frontend/lib/widgets/presentation_form_screen.dart new file mode 100644 index 0000000..8008f32 --- /dev/null +++ b/frontend/lib/widgets/presentation_form_screen.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:math' as math; + +import 'custom_decorated_text_field.dart'; +import 'app_custom_checkbox.dart'; +import '../models/card_assets.dart'; + +class PresentationFormScreen extends StatefulWidget { + final String stepText; // Ex: "Étape 3/4" ou "Étape 4/5" + final String title; // Ex: "Présentation et Conditions" ou "Motivation de votre demande" + final CardColorHorizontal cardColor; + final String textFieldHint; + final String initialText; + final bool initialCguAccepted; + final String previousRoute; + final Function(String text, bool cguAccepted) onSubmit; + + const PresentationFormScreen({ + super.key, + required this.stepText, + required this.title, + required this.cardColor, + required this.textFieldHint, + required this.initialText, + required this.initialCguAccepted, + required this.previousRoute, + required this.onSubmit, + }); + + @override + State createState() => _PresentationFormScreenState(); +} + +class _PresentationFormScreenState extends State { + late TextEditingController _textController; + late bool _cguAccepted; + + @override + void initState() { + super.initState(); + _textController = TextEditingController(text: widget.initialText); + _cguAccepted = widget.initialCguAccepted; + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + void _handleSubmit() { + if (!_cguAccepted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Vous devez accepter les CGU pour continuer.'), + backgroundColor: Colors.red, + ), + ); + return; + } + widget.onSubmit(_textController.text, _cguAccepted); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final cardWidth = screenSize.width * 0.6; + final double imageAspectRatio = 2.0; + final cardHeight = cardWidth / imageAspectRatio; + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.stepText, + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 20), + Text( + widget.title, + style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Container( + width: cardWidth, + height: cardHeight, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(widget.cardColor.path), + fit: BoxFit.fill, + ), + ), + child: Padding( + padding: const EdgeInsets.all(40.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: CustomDecoratedTextField( + controller: _textController, + hintText: widget.textFieldHint, + fieldHeight: cardHeight * 0.6, + maxLines: 10, + expandDynamically: true, + fontSize: 18.0, + ), + ), + const SizedBox(height: 20), + AppCustomCheckbox( + label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', + value: _cguAccepted, + onChanged: (value) => setState(() => _cguAccepted = value ?? false), + ), + ], + ), + ), + ), + ], + ), + ), + ), + // Chevron Gauche (Retour) + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(widget.previousRoute); + } + }, + tooltip: 'Retour', + ), + ), + // Chevron Droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _cguAccepted ? _handleSubmit : null, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/professional_info_form_screen.dart b/frontend/lib/widgets/professional_info_form_screen.dart new file mode 100644 index 0000000..dbc19dd --- /dev/null +++ b/frontend/lib/widgets/professional_info_form_screen.dart @@ -0,0 +1,386 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; +import 'dart:math' as math; +import 'dart:io'; +import '../models/card_assets.dart'; +import 'custom_app_text_field.dart'; +import 'app_custom_checkbox.dart'; +import 'hover_relief_widget.dart'; + +/// Données pour le formulaire d'informations professionnelles +class ProfessionalInfoData { + final String? photoPath; + final File? photoFile; + final bool photoConsent; + final DateTime? dateOfBirth; + final String birthCity; + final String birthCountry; + final String nir; + final String agrementNumber; + final int? capacity; + + ProfessionalInfoData({ + this.photoPath, + this.photoFile, + this.photoConsent = false, + this.dateOfBirth, + this.birthCity = '', + this.birthCountry = '', + this.nir = '', + this.agrementNumber = '', + this.capacity, + }); +} + +/// Widget générique pour le formulaire d'informations professionnelles +/// Utilisé pour l'inscription des Assistantes Maternelles +class ProfessionalInfoFormScreen extends StatefulWidget { + final String stepText; + final String title; + final CardColorHorizontal cardColor; + final ProfessionalInfoData? initialData; + final String previousRoute; + final Function(ProfessionalInfoData) onSubmit; + final Future Function()? onPickPhoto; + + const ProfessionalInfoFormScreen({ + super.key, + required this.stepText, + required this.title, + required this.cardColor, + this.initialData, + required this.previousRoute, + required this.onSubmit, + this.onPickPhoto, + }); + + @override + State createState() => _ProfessionalInfoFormScreenState(); +} + +class _ProfessionalInfoFormScreenState extends State { + final _formKey = GlobalKey(); + + final _dateOfBirthController = TextEditingController(); + final _birthCityController = TextEditingController(); + final _birthCountryController = TextEditingController(); + final _nirController = TextEditingController(); + final _agrementController = TextEditingController(); + final _capacityController = TextEditingController(); + + DateTime? _selectedDate; + String? _photoPathFramework; + File? _photoFile; + bool _photoConsent = false; + + @override + void initState() { + super.initState(); + + final data = widget.initialData; + if (data != null) { + _selectedDate = data.dateOfBirth; + _dateOfBirthController.text = data.dateOfBirth != null + ? DateFormat('dd/MM/yyyy').format(data.dateOfBirth!) + : ''; + _birthCityController.text = data.birthCity; + _birthCountryController.text = data.birthCountry; + _nirController.text = data.nir; + _agrementController.text = data.agrementNumber; + _capacityController.text = data.capacity?.toString() ?? ''; + _photoPathFramework = data.photoPath; + _photoFile = data.photoFile; + _photoConsent = data.photoConsent; + } + } + + @override + void dispose() { + _dateOfBirthController.dispose(); + _birthCityController.dispose(); + _birthCountryController.dispose(); + _nirController.dispose(); + _agrementController.dispose(); + _capacityController.dispose(); + super.dispose(); + } + + Future _selectDate(BuildContext context) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? DateTime.now().subtract(const Duration(days: 365 * 25)), + firstDate: DateTime(1920, 1), + lastDate: DateTime.now().subtract(const Duration(days: 365 * 18)), + locale: const Locale('fr', 'FR'), + ); + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + _dateOfBirthController.text = DateFormat('dd/MM/yyyy').format(picked); + }); + } + } + + Future _pickPhoto() async { + if (widget.onPickPhoto != null) { + await widget.onPickPhoto!(); + } else { + // Comportement par défaut : utiliser un asset de test + setState(() { + _photoPathFramework = 'assets/images/icon_assmat.png'; + _photoFile = null; + }); + } + } + + void _submitForm() { + if (_formKey.currentState!.validate()) { + if (_photoPathFramework != null && !_photoConsent) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Veuillez accepter le consentement photo pour continuer.')), + ); + return; + } + + final data = ProfessionalInfoData( + photoPath: _photoPathFramework, + photoFile: _photoFile, + photoConsent: _photoConsent, + dateOfBirth: _selectedDate, + birthCity: _birthCityController.text, + birthCountry: _birthCountryController.text, + nir: _nirController.text, + agrementNumber: _agrementController.text, + capacity: int.tryParse(_capacityController.text), + ); + + widget.onSubmit(data); + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final Color baseCardColorForShadow = Colors.green.shade300; + final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90); + final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); + + ImageProvider? currentImageProvider; + if (_photoFile != null) { + currentImageProvider = FileImage(_photoFile!); + } else if (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')) { + currentImageProvider = AssetImage(_photoPathFramework!); + } + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(widget.stepText, style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 10), + Text( + widget.title, + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Container( + width: screenSize.width * 0.6, + padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50), + constraints: const BoxConstraints(minHeight: 650), + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage(widget.cardColor.path), fit: BoxFit.fill), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Colonne Gauche: Photo et Checkbox + SizedBox( + width: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + HoverReliefWidget( + onPressed: _pickPhoto, + borderRadius: BorderRadius.circular(10.0), + initialShadowColor: initialPhotoShadow, + hoverShadowColor: hoverPhotoShadow, + child: SizedBox( + height: 270, + width: 270, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: currentImageProvider != null + ? DecorationImage(image: currentImageProvider, fit: BoxFit.cover) + : null, + ), + child: currentImageProvider == null + ? Image.asset('assets/images/photo.png', fit: BoxFit.contain) + : null, + ), + ), + ), + const SizedBox(height: 10), + AppCustomCheckbox( + label: 'J\'accepte l\'utilisation\nde ma photo.', + value: _photoConsent, + onChanged: (val) => setState(() => _photoConsent = val ?? false), + ), + ], + ), + ), + const SizedBox(width: 30), + // Colonne Droite: Champs de naissance + Expanded( + child: Column( + children: [ + CustomAppTextField( + controller: _birthCityController, + labelText: 'Ville de naissance', + hintText: 'Votre ville de naissance', + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + validator: (v) => v!.isEmpty ? 'Ville requise' : null, + ), + const SizedBox(height: 32), + CustomAppTextField( + controller: _birthCountryController, + labelText: 'Pays de naissance', + hintText: 'Votre pays de naissance', + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + validator: (v) => v!.isEmpty ? 'Pays requis' : null, + ), + const SizedBox(height: 32), + CustomAppTextField( + controller: _dateOfBirthController, + labelText: 'Date de naissance', + hintText: 'JJ/MM/AAAA', + readOnly: true, + onTap: () => _selectDate(context), + suffixIcon: Icons.calendar_today, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + validator: (v) => _selectedDate == null ? 'Date requise' : null, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 32), + CustomAppTextField( + controller: _nirController, + labelText: 'N° Sécurité Sociale (NIR)', + hintText: 'Votre NIR à 13 chiffres', + keyboardType: TextInputType.number, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + validator: (v) { + if (v == null || v.isEmpty) return 'NIR requis'; + if (v.length != 13) return 'Le NIR doit contenir 13 chiffres'; + if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3'; + return null; + }, + ), + const SizedBox(height: 32), + Row( + children: [ + Expanded( + child: CustomAppTextField( + controller: _agrementController, + labelText: 'N° d\'agrément', + hintText: 'Votre numéro d\'agrément', + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + validator: (v) => v!.isEmpty ? 'Agrément requis' : null, + ), + ), + const SizedBox(width: 20), + Expanded( + child: CustomAppTextField( + controller: _capacityController, + labelText: 'Capacité d\'accueil', + hintText: 'Ex: 3', + keyboardType: TextInputType.number, + fieldWidth: double.infinity, + labelFontSize: 22.0, + inputFontSize: 20.0, + validator: (v) { + if (v == null || v.isEmpty) return 'Capacité requise'; + final n = int.tryParse(v); + if (n == null || n <= 0) return 'Nombre invalide'; + return null; + }, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + // Chevron Gauche (Retour) + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(widget.previousRoute); + } + }, + tooltip: 'Précédent', + ), + ), + // Chevron Droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _submitForm, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/summary_screen.dart b/frontend/lib/widgets/summary_screen.dart new file mode 100644 index 0000000..a623891 --- /dev/null +++ b/frontend/lib/widgets/summary_screen.dart @@ -0,0 +1,248 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:go_router/go_router.dart'; +import 'dart:math' as math; +import '../models/card_assets.dart'; +import 'image_button.dart'; + +/// Widget générique pour afficher un écran de récapitulatif +/// Utilisé pour le récapitulatif d'inscription Parents et AM +class SummaryScreen extends StatelessWidget { + final String stepText; + final String title; + final List summaryCards; + final String previousRoute; + final VoidCallback onSubmit; + final String submitButtonText; + + const SummaryScreen({ + super.key, + required this.stepText, + required this.title, + required this.summaryCards, + required this.previousRoute, + required this.onSubmit, + this.submitButtonText = 'Soumettre ma demande', + }); + + void _showConfirmationModal(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext ctx) { + return AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + backgroundColor: const Color(0xFFF4F1DE), + title: Text('Demande envoyée !', style: GoogleFonts.merienda(fontSize: 20, fontWeight: FontWeight.bold, color: const Color(0xFF2D6A4F)), textAlign: TextAlign.center), + content: Text('Votre demande a bien été enregistrée. Nous reviendrons vers vous prochainement.', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black87), textAlign: TextAlign.center), + actions: [ + Center( + child: ImageButton( + bg: 'assets/images/btn_green.png', + text: 'OK', + textColor: const Color(0xFF2D6A4F), + width: 150, + height: 40, + fontSize: 16, + onPressed: () { + Navigator.of(ctx).pop(); + context.go('/'); + }, + ), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(stepText, style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 20), + Text(title, style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), + const SizedBox(height: 30), + + // Cartes de récapitulatif passées en paramètre + ...summaryCards.map((card) => Padding( + padding: const EdgeInsets.only(bottom: 20), + child: card, + )), + + const SizedBox(height: 20), + + ImageButton( + bg: 'assets/images/btn_green.png', + text: submitButtonText, + textColor: const Color(0xFF2D6A4F), + width: 350, + height: 50, + fontSize: 18, + onPressed: () { + onSubmit(); + _showConfirmationModal(context); + }, + ), + ], + ), + ), + ), + ), + // Chevron gauche (retour) + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(previousRoute); + } + }, + tooltip: 'Retour', + ), + ), + ], + ), + ); + } +} + +/// Helper widget pour créer une carte de récapitulatif avec un titre et un contenu +class SummaryCard extends StatelessWidget { + final String title; + final String backgroundImagePath; + final List content; + final VoidCallback? onEdit; + + const SummaryCard({ + super.key, + required this.title, + required this.backgroundImagePath, + required this.content, + this.onEdit, + }); + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 2.0, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(backgroundImagePath), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.center, + child: Text( + title, + style: GoogleFonts.merienda( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 15), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: content, + ), + ), + ), + ], + ), + ), + if (onEdit != null) ...[ + const SizedBox(width: 15), + Align( + alignment: Alignment.center, + child: IconButton( + icon: Image.asset('assets/images/input_field_bg.png', height: 35), + tooltip: 'Modifier', + onPressed: onEdit, + ), + ), + ], + ], + ), + ), + ); + } +} + +/// Fonction helper pour afficher un champ de type "lecture seule" stylisé +Widget buildDisplayFieldValue( + BuildContext context, + String label, + String value, { + bool multiLine = false, + double fieldHeight = 50.0, + double labelFontSize = 18.0, +}) { + const FontWeight labelFontWeight = FontWeight.w600; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: multiLine ? null : fieldHeight, + constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, + padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/input_field_bg.png'), + fit: BoxFit.fill, + ), + ), + child: Text( + value.isNotEmpty ? value : '-', + style: GoogleFonts.merienda(fontSize: labelFontSize), + maxLines: multiLine ? null : 1, + overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis, + ), + ), + ], + ); +} diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..29d7e49 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,13 @@ +server { + listen 80; + server_name ynov.ptits-pas.fr; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + # Gestion des erreurs + error_page 404 /index.html; +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 9d0bbd5..c9dd468 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -169,10 +169,10 @@ packages: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" http_parser: dependency: transitive description: @@ -249,10 +249,10 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" js: dependency: "direct main" description: @@ -265,26 +265,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -321,10 +321,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -526,10 +526,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" typed_data: dependency: transitive description: @@ -606,10 +606,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -635,5 +635,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 1a73bb5..369295c 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -18,7 +18,8 @@ dependencies: image_picker: ^1.0.7 js: ^0.6.7 url_launcher: ^6.2.4 - http: ^1.2.0 + http: ^1.2.2 + # flutter_secure_storage: ^9.0.0 dev_dependencies: flutter_test: diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart index a6ee807..a26aed4 100644 --- a/frontend/test/widget_test.dart +++ b/frontend/test/widget_test.dart @@ -7,8 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:p_tits_pas/main.dart'; -import 'package:petitspas/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 043a96f..0000000 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,17 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - FileSelectorWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FileSelectorWindows")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/frontend/windows/flutter/generated_plugin_registrant.h b/frontend/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d8..0000000 --- a/frontend/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake deleted file mode 100644 index a95e267..0000000 --- a/frontend/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - file_selector_windows - url_launcher_windows -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin)