diff --git a/backend/scripts/close-gitea-issue-120.js b/backend/scripts/close-gitea-issue-120.js index fbdc75a..dae726e 100644 --- a/backend/scripts/close-gitea-issue-120.js +++ b/backend/scripts/close-gitea-issue-120.js @@ -1,7 +1,7 @@ /** * Commentaire de clôture + fermeture issue Gitea #120. * Usage: node backend/scripts/close-gitea-issue-120.js - * Token : .gitea-token (racine), GITEA_TOKEN, ou docs/BRIEFING-FRONTEND.md + * Token : .gitea-token (racine), GITEA_TOKEN, ou docs/27_BRIEFING-FRONTEND.md */ const https = require('https'); const fs = require('fs'); @@ -45,7 +45,10 @@ if (!token) { } if (!token) { try { - const briefing = fs.readFileSync(path.join(repoRoot, 'docs/BRIEFING-FRONTEND.md'), 'utf8'); + const briefing = fs.readFileSync( + path.join(repoRoot, 'docs/27_BRIEFING-FRONTEND.md'), + 'utf8', + ); const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/); if (m) token = m[1].trim(); } catch (_) {} diff --git a/backend/scripts/create-gitea-issue-parent-api.js b/backend/scripts/create-gitea-issue-parent-api.js index 68f2541..7d457fa 100644 --- a/backend/scripts/create-gitea-issue-parent-api.js +++ b/backend/scripts/create-gitea-issue-parent-api.js @@ -1,7 +1,8 @@ /** * Crée l'issue Gitea "[Frontend] Inscription Parent – Branchement soumission formulaire à l'API" * Usage: node backend/scripts/create-gitea-issue-parent-api.js - * Token : .gitea-token (racine du dépôt), sinon GITEA_TOKEN, sinon docs/BRIEFING-FRONTEND.md (voir PROCEDURE-API-GITEA.md) + * Token : .gitea-token (racine du dépôt), sinon GITEA_TOKEN, sinon + * docs/27_BRIEFING-FRONTEND.md (voir docs/26_GITEA-API.md) */ const https = require('https'); const fs = require('fs'); @@ -19,13 +20,18 @@ if (!token) { } if (!token) { try { - const briefing = fs.readFileSync(path.join(repoRoot, 'docs/BRIEFING-FRONTEND.md'), 'utf8'); + const briefing = fs.readFileSync( + path.join(repoRoot, 'docs/27_BRIEFING-FRONTEND.md'), + 'utf8', + ); const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/); if (m) token = m[1].trim(); } catch (_) {} } if (!token) { - console.error('Token non trouvé : créer .gitea-token à la racine ou export GITEA_TOKEN (voir docs/PROCEDURE-API-GITEA.md)'); + console.error( + 'Token non trouvé : créer .gitea-token à la racine ou export GITEA_TOKEN (voir docs/26_GITEA-API.md)', + ); process.exit(1); } diff --git a/backend/scripts/list-gitea-issues.js b/backend/scripts/list-gitea-issues.js index 57742c8..599b738 100644 --- a/backend/scripts/list-gitea-issues.js +++ b/backend/scripts/list-gitea-issues.js @@ -1,6 +1,6 @@ /** * Liste toutes les issues Gitea (ouvertes + fermées) pour jmartin/petitspas. - * Token : .gitea-token (racine), GITEA_TOKEN, ou docs/BRIEFING-FRONTEND.md + * Token : .gitea-token (racine), GITEA_TOKEN, ou docs/27_BRIEFING-FRONTEND.md */ const https = require('https'); const fs = require('fs'); @@ -16,7 +16,10 @@ if (!token) { } if (!token) { try { - const briefing = fs.readFileSync(path.join(repoRoot, 'docs/BRIEFING-FRONTEND.md'), 'utf8'); + const briefing = fs.readFileSync( + path.join(repoRoot, 'docs/27_BRIEFING-FRONTEND.md'), + 'utf8', + ); const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/); if (m) token = m[1].trim(); } catch (_) {} diff --git a/backend/scripts/update-gitea-issue-119-dossiers.js b/backend/scripts/update-gitea-issue-119-dossiers.js index 225dbcb..81257b2 100644 --- a/backend/scripts/update-gitea-issue-119-dossiers.js +++ b/backend/scripts/update-gitea-issue-119-dossiers.js @@ -1,7 +1,7 @@ /** * Met à jour l'issue Gitea #119 : endpoint unifié GET /dossiers/:numeroDossier (option A) * Usage: node backend/scripts/update-gitea-issue-119-dossiers.js - * Token : .gitea-token (racine), GITEA_TOKEN, ou docs/BRIEFING-FRONTEND.md + * Token : .gitea-token (racine), GITEA_TOKEN, ou docs/27_BRIEFING-FRONTEND.md */ const https = require('https'); const fs = require('fs'); @@ -17,7 +17,10 @@ if (!token) { } if (!token) { try { - const briefing = fs.readFileSync(path.join(repoRoot, 'docs/BRIEFING-FRONTEND.md'), 'utf8'); + const briefing = fs.readFileSync( + path.join(repoRoot, 'docs/27_BRIEFING-FRONTEND.md'), + 'utf8', + ); const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/); if (m) token = m[1].trim(); } catch (_) {} diff --git a/docs/00_INDEX.md b/docs/00_INDEX.md index df689ca..2f8a601 100644 --- a/docs/00_INDEX.md +++ b/docs/00_INDEX.md @@ -19,15 +19,39 @@ Ce fichier sert d'index pour naviguer dans toute la documentation du projet. ### 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 +- [**14 - Note backend config setup**](./14_NOTE-BACKEND-CONFIG-SETUP.md) - Setup configuration +- [**92 - Note backend gestionnaires**](./92_NOTE-BACKEND-GESTIONNAIRES.md) - Gestionnaires +- [**99 - Règles de codage**](./99_REGLES-CODAGE.md) - Conventions de code ### 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 +- [**22 - Documents Légaux**](./juridique/22_DOCUMENTS-LEGAUX.md) - Gestion CGU/Privacy avec versioning + +### Juridique (sources & technique) +- [**Dossier juridique**](./juridique/README.md) - Index : CGU/CGC en Markdown, + export PDF, lien vers la doc technique n°22 + +### Projet & suivi (Gitea / tickets) - [**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 - [**26 - API Gitea**](./26_GITEA-API.md) - Procédure d'utilisation de l'API Gitea (issues, PR, branches, labels) +- [**27 - Briefing frontend**](./27_BRIEFING-FRONTEND.md) - Accès Git, priorités, scripts Gitea (token) + +### Archive & convention de nommage +- [**Dossier archive**](./archive/README.md) - Fichiers **sans** `NN_` déplacés + (temporaires, obsolètes) ; règles de rangement et suppression +- Pointeur : [PROCEDURE-API-GITEA.md](./PROCEDURE-API-GITEA.md) → voir **26** + +### Exceptions de nommage (racine `docs/`) +Fichiers **sans préfixe numérique** encore à la racine par **héritage** ou +références outils (`.cursorrules`, etc.) — **à renommer** en `NN_` quand +possible : +- `CHARTE_GRAPHIQUE.md` +- `EVOLUTIONS_CDC.md` +- `SuperNounou_Cahier_Des_Charges_Complet_V1.1.md` +- `SuperNounou_SSS-001.md` ### Administration (À créer) - [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs diff --git a/docs/22_DOCUMENTS-LEGAUX.md b/docs/22_DOCUMENTS-LEGAUX.md index 4bd5bc5..c213372 100644 --- a/docs/22_DOCUMENTS-LEGAUX.md +++ b/docs/22_DOCUMENTS-LEGAUX.md @@ -1,698 +1,8 @@ -# 📄 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é +# Fichier déplacé +La documentation **Documents légaux** a été déplacée vers : +**[juridique/22_DOCUMENTS-LEGAUX.md](./juridique/22_DOCUMENTS-LEGAUX.md)** +Voir aussi le dossier **[juridique/](./juridique/)** pour les sources **CGU** +et **CGC** en Markdown. diff --git a/docs/23_LISTE-TICKETS.md b/docs/23_LISTE-TICKETS.md index fdebf49..eee299d 100644 --- a/docs/23_LISTE-TICKETS.md +++ b/docs/23_LISTE-TICKETS.md @@ -55,7 +55,7 @@ | 44 | [Frontend] Dashboard Gestionnaire - Structure | ✅ Terminé | | 45 | [Frontend] Dashboard Gestionnaire - Liste Parents | ✅ Fermé (obsolète, couvert #103-#111) | | 46 | [Frontend] Dashboard Gestionnaire - Liste AM | ✅ Fermé (obsolète, couvert #103-#111) | -| 47 | [Frontend] Écran Changement MDP Obligatoire | Ouvert | +| 47 | [Frontend] Écran Changement MDP Obligatoire | ✅ Fermé | | 48 | [Frontend] Gestion Erreurs & Messages | Ouvert | | 49 | [Frontend] Écran Gestion Documents Légaux (Admin) | Ouvert | | 50 | [Frontend] Affichage dynamique CGU lors inscription | Ouvert | @@ -243,7 +243,7 @@ Créer les tables pour gérer les versions des documents légaux (CGU/Privacy) e - [ ] Créer migration Prisma - [ ] Tester migration -**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md) +**Référence** : [22_DOCUMENTS-LEGAUX.md](./juridique/22_DOCUMENTS-LEGAUX.md) --- @@ -679,7 +679,7 @@ Créer le service de gestion des documents légaux (CGU/Privacy) avec versioning - [ ] Méthode `listerVersions()` - [ ] Tests unitaires -**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md#service-documents-légaux) +**Référence** : [22_DOCUMENTS-LEGAUX.md](./juridique/22_DOCUMENTS-LEGAUX.md#service-documents-légaux) --- @@ -699,7 +699,7 @@ Créer les endpoints REST pour gérer les documents légaux. - [ ] Guards + validation - [ ] Tests unitaires -**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md#apis) +**Référence** : [22_DOCUMENTS-LEGAUX.md](./juridique/22_DOCUMENTS-LEGAUX.md#apis) --- @@ -716,7 +716,7 @@ Enregistrer les acceptations de documents légaux lors de l'inscription (traçab - [ ] API `GET /api/v1/users/:id/acceptations` (admin) - [ ] Tests unitaires -**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md#workflow-acceptation-utilisateur) +**Référence** : [22_DOCUMENTS-LEGAUX.md](./juridique/22_DOCUMENTS-LEGAUX.md#workflow-acceptation-utilisateur) --- @@ -1005,7 +1005,7 @@ Créer l'écran de gestion des documents légaux (CGU/Privacy) pour l'admin. - [ ] Activation version - [ ] Historique versions -**Référence** : [22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md#interface-admin) +**Référence** : [22_DOCUMENTS-LEGAUX.md](./juridique/22_DOCUMENTS-LEGAUX.md#interface-admin) --- @@ -1165,7 +1165,7 @@ Branchement du formulaire d'inscription parent (étape 5, récapitulatif) à l'e **Référence** : [20_WORKFLOW-CREATION-COMPTE.md](./20_WORKFLOW-CREATION-COMPTE.md#étape-3--inscription-dun-parent), backend #18 (refonte API inscription parent). -**Création** : issue Gitea #101 créée. Pour recréer ou script : `node backend/scripts/create-gitea-issue-parent-api.js` (token dans `.gitea-token` ou voir [PROCEDURE-API-GITEA.md](./PROCEDURE-API-GITEA.md)). +**Création** : issue Gitea #101 créée. Pour recréer ou script : `node backend/scripts/create-gitea-issue-parent-api.js` (token dans `.gitea-token` ou voir [26_GITEA-API.md](./26_GITEA-API.md)). --- diff --git a/docs/24_DECISIONS-PROJET.md b/docs/24_DECISIONS-PROJET.md index fd8a72d..62bcbe5 100644 --- a/docs/24_DECISIONS-PROJET.md +++ b/docs/24_DECISIONS-PROJET.md @@ -95,7 +95,7 @@ ptitspas-app/ - 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) +**Référence** : [22_DOCUMENTS-LEGAUX.md](./juridique/22_DOCUMENTS-LEGAUX.md) --- @@ -413,7 +413,18 @@ docs/ ├── 11_API.md ├── 20_WORKFLOW-CREATION-COMPTE.md ├── 21_CONFIGURATION-SYSTEME.md -├── 22_DOCUMENTS-LEGAUX.md +├── 22_DOCUMENTS-LEGAUX.md # pointeur → juridique/ +├── 27_BRIEFING-FRONTEND.md +├── PROCEDURE-API-GITEA.md # pointeur → 26_GITEA-API.md +├── juridique/ +│ ├── README.md +│ ├── cgu.md +│ ├── cgc.md +│ └── 22_DOCUMENTS-LEGAUX.md +├── archive/ +│ ├── README.md +│ ├── temporaires/ +│ └── obsolete/ ├── 23_LISTE-TICKETS.md ├── 24_DECISIONS-PROJET.md (ce document) ├── 90_AUDIT.md diff --git a/docs/BRIEFING-FRONTEND.md b/docs/27_BRIEFING-FRONTEND.md similarity index 100% rename from docs/BRIEFING-FRONTEND.md rename to docs/27_BRIEFING-FRONTEND.md diff --git a/docs/PROCEDURE-API-GITEA.md b/docs/PROCEDURE-API-GITEA.md index 6a41046..fcae5a0 100644 --- a/docs/PROCEDURE-API-GITEA.md +++ b/docs/PROCEDURE-API-GITEA.md @@ -1,176 +1,8 @@ -# Procédure – Utilisation de l’API Gitea +# Fichier déplacé / fusionné -## 1. Contexte +La procédure **API Gitea** est désormais documentée sous : -- **Instance** : https://git.ptits-pas.fr -- **API de base** : `https://git.ptits-pas.fr/api/v1` -- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`) +**[26_GITEA-API.md](./26_GITEA-API.md)** -## 2. Authentification - -### 2.1 Token - -Le token est défini dans l’environnement (ex. `~/.bashrc`) : - -```bash -export GITEA_TOKEN="" -``` - -Pour l’utiliser dans les commandes : - -```bash -source ~/.bashrc # ou : . ~/.bashrc -# Puis utiliser $GITEA_TOKEN dans les curl -``` - -### 2.2 En-tête HTTP - -Toutes les requêtes API doivent envoyer le token : - -```bash --H "Authorization: token $GITEA_TOKEN" -``` - -Exemple : - -```bash -curl -s -H "Authorization: token $GITEA_TOKEN" \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" -``` - -## 3. Endpoints utiles - -### 3.1 Dépôt (repository) - -| Action | Méthode | URL | -|---------------|---------|-----| -| Infos dépôt | GET | `/repos/{owner}/{repo}` | -| Liste dépôts | GET | `/repos/search?q=petitspas` | - -Exemple – infos du dépôt : - -```bash -curl -s -H "Authorization: token $GITEA_TOKEN" \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq . -``` - -### 3.2 Issues (tickets) - -| Action | Méthode | URL | -|------------------|---------|-----| -| Liste des issues | GET | `/repos/{owner}/{repo}/issues` | -| Détail d’une issue | GET | `/repos/{owner}/{repo}/issues/{index}` | -| Créer une issue | POST | `/repos/{owner}/{repo}/issues` | -| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` | -| Fermer une issue | PATCH | (même URL, `state: "closed"`) | - -**Paramètres GET utiles pour la liste :** - -- `state` : `open` ou `closed` -- `labels` : filtre par label (ex. `frontend`) -- `page`, `limit` : pagination - -Exemples : - -```bash -# Toutes les issues ouvertes -curl -s -H "Authorization: token $GITEA_TOKEN" \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq . - -# Issues ouvertes avec label "frontend" -curl -s -H "Authorization: token $GITEA_TOKEN" \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \ - jq '.[] | select(.labels[].name == "frontend") | {number, title, state}' - -# Détail de l’issue #47 -curl -s -H "Authorization: token $GITEA_TOKEN" \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq . - -# Fermer l’issue #31 -curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"state":"closed"}' \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31" - -# Créer une issue -curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues" -``` - -### 3.3 Pull requests - -| Action | Méthode | URL | -|---------------|---------|-----| -| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` | -| Détail d’une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` | -| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` | -| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` | - -Exemples : - -```bash -# Liste des PR ouvertes -curl -s -H "Authorization: token $GITEA_TOKEN" \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq . - -# Créer une PR (head = branche source, base = branche cible) -curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"head":"develop","base":"master","title":"Titre de la PR"}' \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls" -``` - -### 3.4 Branches - -| Action | Méthode | URL | -|---------------|---------|-----| -| Liste des branches | GET | `/repos/{owner}/{repo}/branches` | - -```bash -curl -s -H "Authorization: token $GITEA_TOKEN" \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name' -``` - -### 3.5 Webhooks - -| Action | Méthode | URL | -|---------------|---------|-----| -| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` | -| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` | - -### 3.6 Labels - -| Action | Méthode | URL | -|---------------|---------|-----| -| Liste des labels | GET | `/repos/{owner}/{repo}/labels` | - -```bash -curl -s -H "Authorization: token $GITEA_TOKEN" \ - "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}' -``` - -## 4. Résumé des URLs pour P'titsPas - -Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` : - -| Ressource | URL | -|------------------|-----| -| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` | -| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` | -| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` | -| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` | -| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` | -| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` | - -## 5. Documentation officielle - -- Swagger / OpenAPI : https://docs.gitea.com/api -- Référence selon la version de Gitea installée (ex. 1.21, 1.25). - -## 6. Dépannage - -- **401 Unauthorized** : vérifier le token et l’en-tête `Authorization: token `. -- **404** : vérifier owner/repo et l’URL (sensible à la casse). -- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide. +L’ancienne copie `PROCEDURE-API-GITEA.md` est archivée dans +`docs/archive/obsolete/` (doublon). diff --git a/docs/archive/README.md b/docs/archive/README.md new file mode 100644 index 0000000..794b038 --- /dev/null +++ b/docs/archive/README.md @@ -0,0 +1,30 @@ +# Archive documentation · P'titsPas + +Ce dossier regroupe les fichiers **sans préfixe numérique** à la racine de +`docs/` qui ne sont plus des **références actives**, ou qui sont des +**brouillons / temporaires**. + +## Règle de nommage (racine `docs/`) + +- Les documents **normatifs** à la racine portent un préfixe **`NN_`** + (deux chiffres), ex. `23_LISTE-TICKETS.md`. +- **Exceptions** (héritage ou outillage) listées dans + [**00_INDEX.md**](../00_INDEX.md#exceptions-de-nommage) : charte, CDC + historique, évolutions — **cible** : les renommer progressivement en `NN_` + et mettre à jour `.cursorrules` / liens. + +## Sous-dossiers ici + +| Dossier | Usage | +|---------|--------| +| [**temporaires/**](./temporaires/) | Notes jetables, exports de travail. + **Supprimables** quand la tâche associée est close. | +| [**obsolete/**](./obsolete/) | Ancienne doc **remplacée** ou **doublon** + (conservée un temps pour historique). **Supprimer** après bascule confirmée + si plus aucune référence. | + +## Hors `docs/` racine + +Les dossiers thématiques (**`juridique/`**, **`test-data/`**, etc.) peuvent +contenir des fichiers sans `NN_` : la règle `NN_` s’applique surtout aux +fichiers **directement** sous `docs/`. diff --git a/docs/ARCHITECTURE_TECHNIQUE.md b/docs/archive/obsolete/ARCHITECTURE_TECHNIQUE.md similarity index 98% rename from docs/ARCHITECTURE_TECHNIQUE.md rename to docs/archive/obsolete/ARCHITECTURE_TECHNIQUE.md index 5fead37..d7b245b 100644 --- a/docs/ARCHITECTURE_TECHNIQUE.md +++ b/docs/archive/obsolete/ARCHITECTURE_TECHNIQUE.md @@ -1,4 +1,9 @@ # Architecture Technique - P'titsPas + +> **Archivé** (`docs/archive/obsolete/`) — non maintenu à la racine `docs/`. +> Voir [`02_ARCHITECTURE.md`](../../02_ARCHITECTURE.md) pour la référence +> courante. + ## Guide d'Infrastructure et de Déploiement --- diff --git a/docs/archive/obsolete/PROCEDURE-API-GITEA.md b/docs/archive/obsolete/PROCEDURE-API-GITEA.md new file mode 100644 index 0000000..5d68f27 --- /dev/null +++ b/docs/archive/obsolete/PROCEDURE-API-GITEA.md @@ -0,0 +1,180 @@ +# Procédure – Utilisation de l’API Gitea + +> **Archivé** (`docs/archive/obsolete/`) — **doublon** de +> [`26_GITEA-API.md`](../../26_GITEA-API.md). Ne pas modifier ici ; éditer le +> fichier numéroté. + +## 1. Contexte + +- **Instance** : https://git.ptits-pas.fr +- **API de base** : `https://git.ptits-pas.fr/api/v1` +- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`) + +## 2. Authentification + +### 2.1 Token + +Le token est défini dans l’environnement (ex. `~/.bashrc`) : + +```bash +export GITEA_TOKEN="" +``` + +Pour l’utiliser dans les commandes : + +```bash +source ~/.bashrc # ou : . ~/.bashrc +# Puis utiliser $GITEA_TOKEN dans les curl +``` + +### 2.2 En-tête HTTP + +Toutes les requêtes API doivent envoyer le token : + +```bash +-H "Authorization: token $GITEA_TOKEN" +``` + +Exemple : + +```bash +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" +``` + +## 3. Endpoints utiles + +### 3.1 Dépôt (repository) + +| Action | Méthode | URL | +|---------------|---------|-----| +| Infos dépôt | GET | `/repos/{owner}/{repo}` | +| Liste dépôts | GET | `/repos/search?q=petitspas` | + +Exemple – infos du dépôt : + +```bash +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas" | jq . +``` + +### 3.2 Issues (tickets) + +| Action | Méthode | URL | +|------------------|---------|-----| +| Liste des issues | GET | `/repos/{owner}/{repo}/issues` | +| Détail d’une issue | GET | `/repos/{owner}/{repo}/issues/{index}` | +| Créer une issue | POST | `/repos/{owner}/{repo}/issues` | +| Modifier une issue | PATCH | `/repos/{owner}/{repo}/issues/{index}` | +| Fermer une issue | PATCH | (même URL, `state: "closed"`) | + +**Paramètres GET utiles pour la liste :** + +- `state` : `open` ou `closed` +- `labels` : filtre par label (ex. `frontend`) +- `page`, `limit` : pagination + +Exemples : + +```bash +# Toutes les issues ouvertes +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | jq . + +# Issues ouvertes avec label "frontend" +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues?state=open" | \ + jq '.[] | select(.labels[].name == "frontend") | {number, title, state}' + +# Détail de l’issue #47 +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq . + +# Fermer l’issue #31 +curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"state":"closed"}' \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/31" + +# Créer une issue +curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"title":"Titre du ticket","body":"Description","labels":[1]}' \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues" +``` + +### 3.3 Pull requests + +| Action | Méthode | URL | +|---------------|---------|-----| +| Liste des PR | GET | `/repos/{owner}/{repo}/pulls` | +| Détail d’une PR | GET | `/repos/{owner}/{repo}/pulls/{index}` | +| Créer une PR | POST | `/repos/{owner}/{repo}/pulls` | +| Fusionner une PR | POST | `/repos/{owner}/{repo}/pulls/{index}/merge` | + +Exemples : + +```bash +# Liste des PR ouvertes +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls?state=open" | jq . + +# Créer une PR (head = branche source, base = branche cible) +curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"head":"develop","base":"master","title":"Titre de la PR"}' \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls" +``` + +### 3.4 Branches + +| Action | Méthode | URL | +|---------------|---------|-----| +| Liste des branches | GET | `/repos/{owner}/{repo}/branches` | + +```bash +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches" | jq '.[].name' +``` + +### 3.5 Webhooks + +| Action | Méthode | URL | +|---------------|---------|-----| +| Liste webhooks | GET | `/repos/{owner}/{repo}/hooks` | +| Créer webhook | POST | `/repos/{owner}/{repo}/hooks` | + +### 3.6 Labels + +| Action | Méthode | URL | +|---------------|---------|-----| +| Liste des labels | GET | `/repos/{owner}/{repo}/labels` | + +```bash +curl -s -H "Authorization: token $GITEA_TOKEN" \ + "https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels" | jq '.[] | {id, name}' +``` + +## 4. Résumé des URLs pour P'titsPas + +Remplacer `{owner}` par `jmartin` et `{repo}` par `petitspas` : + +| Ressource | URL | +|------------------|-----| +| Dépôt | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas` | +| Issues | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues` | +| Issue #n | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/{n}` | +| Pull requests | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/pulls` | +| Branches | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/branches` | +| Labels | `https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/labels` | + +## 5. Documentation officielle + +- Swagger / OpenAPI : https://docs.gitea.com/api +- Référence selon la version de Gitea installée (ex. 1.21, 1.25). + +## 6. Dépannage + +- **401 Unauthorized** : vérifier le token et l’en-tête `Authorization: token `. +- **404** : vérifier owner/repo et l’URL (sensible à la casse). +- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide. diff --git a/docs/archive/obsolete/README.md b/docs/archive/obsolete/README.md new file mode 100644 index 0000000..ade8bb1 --- /dev/null +++ b/docs/archive/obsolete/README.md @@ -0,0 +1,14 @@ +# Obsolete + +Ancienne documentation **déplacée** depuis `docs/` : + +| Fichier | Motif | +|---------|--------| +| `PROCEDURE-API-GITEA.md` | Doublon fonctionnel de + [**26_GITEA-API.md**](../../26_GITEA-API.md). | +| `ARCHITECTURE_TECHNIQUE.md` | Non référencé ; la vue d’ensemble est dans + [**02_ARCHITECTURE.md**](../../02_ARCHITECTURE.md). | +| `STATUS-APPLICATION.md` | Instantané daté ; non tenu comme doc vivante. | + +Après vérification qu’aucun lien externe ne pointe encore vers ces chemins, on +peut **supprimer** ce sous-dossier ou ne garder que des pointeurs minimalistes. diff --git a/docs/STATUS-APPLICATION.md b/docs/archive/obsolete/STATUS-APPLICATION.md similarity index 92% rename from docs/STATUS-APPLICATION.md rename to docs/archive/obsolete/STATUS-APPLICATION.md index 83461d0..fb25f26 100644 --- a/docs/STATUS-APPLICATION.md +++ b/docs/archive/obsolete/STATUS-APPLICATION.md @@ -1,5 +1,8 @@ # Statut de l'application P'titsPas +> **Archivé** (`docs/archive/obsolete/`) — instantané du **8 février 2026**, non +> maintenu comme référence courante. Voir `docs/00_INDEX.md` pour la doc à jour. + **Date du point** : 8 février 2026 --- @@ -99,8 +102,8 @@ Un seul déploiement à la fois (verrou) ; plus d’étape Prisma dans le script | `11_API.md` | Endpoints API | | `20_WORKFLOW-CREATION-COMPTE.md` | Workflow création compte | | `23_LISTE-TICKETS.md` | Liste des tickets | -| `BRIEFING-FRONTEND.md` | Brief frontend, accès Git, tickets prioritaires | -| `PROCEDURE-API-GITEA.md` | Utilisation API Gitea (issues, PR, token) | +| `27_BRIEFING-FRONTEND.md` | Brief frontend, accès Git, tickets prioritaires | +| `26_GITEA-API.md` | Utilisation API Gitea (issues, PR, token) | --- diff --git a/docs/POINT_TICKETS_FRONT_API.txt b/docs/archive/temporaires/POINT_TICKETS_FRONT_API.txt similarity index 95% rename from docs/POINT_TICKETS_FRONT_API.txt rename to docs/archive/temporaires/POINT_TICKETS_FRONT_API.txt index 7f32219..fac0b2d 100644 --- a/docs/POINT_TICKETS_FRONT_API.txt +++ b/docs/archive/temporaires/POINT_TICKETS_FRONT_API.txt @@ -1,3 +1,4 @@ +# Archivé docs/archive/temporaires/ — export jetable, supprimer si inutile. Point tickets frontend (API Gitea) - 27/01/2026 ================================================ diff --git a/docs/archive/temporaires/README.md b/docs/archive/temporaires/README.md new file mode 100644 index 0000000..bde6fa3 --- /dev/null +++ b/docs/archive/temporaires/README.md @@ -0,0 +1,10 @@ +# Temporaires + +Fichiers **non numérotés** de travail (brouillons, listes de tickets exportées, +alignements UI en cours, etc.). + +- Préfixe conseillé pour les nouveaux fichiers jetables : **`TEMP_`** ou + **`WIP_`** dans ce dossier. +- **Suppression** : dès que la fonctionnalité est livrée ou le sujet clos, + supprimer le fichier (ou le déplacer vers `obsolete/` si une trace utile + reste nécessaire). diff --git a/docs/TEMP_FRONT_alignement_AM.md b/docs/archive/temporaires/TEMP_FRONT_alignement_AM.md similarity index 92% rename from docs/TEMP_FRONT_alignement_AM.md rename to docs/archive/temporaires/TEMP_FRONT_alignement_AM.md index 6a53adc..21f2eed 100644 --- a/docs/TEMP_FRONT_alignement_AM.md +++ b/docs/archive/temporaires/TEMP_FRONT_alignement_AM.md @@ -1,6 +1,8 @@ # TEMP — Alignement front / API (inscription AM & validation gestionnaire) -> **Fichier temporaire** : à supprimer ou renommer une fois le front livré. +> **Archivé** (`docs/archive/temporaires/`) — **fichier temporaire** ; à +> **supprimer** une fois le front livré ou le sujet clos (voir +> `docs/archive/temporaires/README.md`). Ce document décrit les changements **côté API** et ce que **Flutter** doit faire pour rester aligné. Aucune modification front n’a été faite dans le chantier backend associé. diff --git a/docs/juridique/22_DOCUMENTS-LEGAUX.md b/docs/juridique/22_DOCUMENTS-LEGAUX.md new file mode 100644 index 0000000..89e3f4e --- /dev/null +++ b/docs/juridique/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/juridique/README.md b/docs/juridique/README.md new file mode 100644 index 0000000..6cc5728 --- /dev/null +++ b/docs/juridique/README.md @@ -0,0 +1,41 @@ +# Juridique · P'titsPas + +Ce dossier regroupe la **documentation juridique** et les **sources Markdown** +destinées à être publiées (souvent en **PDF**) dans l’application : notamment +l’écran d’**acceptation à l’inscription** (CGU + politique de confidentialité). + +## Documents « application » (à exporter en PDF) + +| Fichier | Rôle | +|--------|------| +| [**cgu.md**](./cgu.md) | **Conditions générales d’utilisation** du service | +| [**politique-confidentialite.md**](./politique-confidentialite.md) | **Politique de confidentialité** (RGPD, droits, finalités) | + +Ces deux fichiers correspondent aux types **`cgu`** et **`privacy`** côté API +documents légaux (voir doc technique ci-dessous). + +## Document complémentaire (optionnel) + +| Fichier | Rôle | +|--------|------| +| [**cgc.md**](./cgc.md) | **Conditions générales contractuelles** (cadre + d’engagement des parties) — distinct de la politique de confidentialité | + +## Documentation technique + +| Fichier | Rôle | +|--------|------| +| [**22_DOCUMENTS-LEGAUX.md**](./22_DOCUMENTS-LEGAUX.md) | API, BDD, versioning, + traçabilité des acceptations | + +> Tous les textes ci-dessus sont des **brouillons** à valider par un **juriste** +> ou la **DPD/DPO** avant diffusion en production. + +## Export PDF (local) + +1. Ouvrir le `.md` dans Cursor. +2. `Ctrl+Shift+P` → **Markdown PDF: Export (pdf)** (extension *Markdown PDF*, + auteur yzane). + +Les PDF déployés côté API sont décrits dans +[22_DOCUMENTS-LEGAUX.md](./22_DOCUMENTS-LEGAUX.md). diff --git a/docs/juridique/cgc.md b/docs/juridique/cgc.md new file mode 100644 index 0000000..97ee909 --- /dev/null +++ b/docs/juridique/cgc.md @@ -0,0 +1,116 @@ +# Conditions générales contractuelles (CGC) + +**Produit** : P'titsPas +**Version document** : 0.1 (brouillon) +**Date** : avril 2026 +**Statut** : à valider juridiquement avant publication + +> **Ce document n’est pas** la **politique de confidentialité** (données +> personnelles / RGPD) : voir +> [**politique-confidentialite.md**](./politique-confidentialite.md). + +--- + +## 1. Objet + +Les présentes **conditions générales contractuelles** (« CGC ») définissent le +**cadre contractuel** entre les **parties** impliquées dans l’usage de la +plateforme **P'titsPas** pour la gestion de la garde d’enfants au sein des +**collectivités** ou structures habilitées. + +Elles complètent les [**conditions générales d’utilisation**](./cgu.md) (CGU), +qui régissent l’usage du **Service numérique** lui-même. + +--- + +## 2. Parties + +Selon les cas d’usage du déploiement, peuvent notamment être parties : + +- la **collectivité** ou l’**employeur public** porteur du dispositif ; +- l’**Éditeur** ou l’**opérateur** assurant l’hébergement et la maintenance du + Service ; +- les **Utilisateurs** (parents, assistantes maternelles, relais d’assistantes + maternelles, gestionnaires, etc.). + +[À compléter : identification précise des personnes morales signataires et +rôles (maîtrise d’ouvrage, maîtrise d’œuvre, délégation).] + +--- + +## 3. Objet du contrat + +Le contrat a pour objet de préciser : + +- les **missions** et **responsabilités** de chaque catégorie d’acteur ; +- les **conditions d’inscription**, de **validation** des dossiers et de + **clôture** des comptes ; +- les **obligations** en matière de **sincérité** des informations et de + **coopération** avec les services compétents. + +--- + +## 4. Obligations des Utilisateurs + +Les Utilisateurs s’engagent à : + +- respecter le **cadre légal** applicable à la garde d’enfants et à la + protection de l’enfance ; +- ne fournir que des **documents** et **informations** licites et, le cas échéant, + conformes aux demandes des gestionnaires ; +- respecter la **charte d’usage** et les consignes communiquées par la + collectivité ou l’Éditeur. + +--- + +## 5. Obligations de la collectivité / de l’Éditeur + +[À compléter : disponibilité cible, support, sauvegardes, sous-traitance, +sous-traitants, transferts hors UE, etc.] + +--- + +## 6. Durée et résiliation + +Le présent cadre contractuel est conclu pour la **durée** d’utilisation du +Service par les Utilisateurs concernés, sous réserve des **résiliations** +prévues par la réglementation ou par décision légitime de l’Éditeur / de la +collectivité. + +La **résiliation** d’un compte Utilisateur n’implique pas nécessairement la +**suppression immédiate** de toutes les données ; les **archives** et délais +peuvent être imposés par la loi. + +--- + +## 7. Modification + +Les CGC peuvent être **adaptées** (évolution réglementaire, organisation du +dispositif). Les Utilisateurs en sont informés par un **moyen approprié** et, +lorsque la loi ou la pratique l’exige, une **nouvelle acceptation** est +enregistrée. + +--- + +## 8. Responsabilité et assurance + +[À compléter : plafonds, exclusions, co-traitance, garanties, assurance +responsabilité civile, etc.] + +--- + +## 9. Litiges + +[À compléter : médiation, compétence territoriale, droit applicable.] + +--- + +## 10. Dispositions finales + +[À compléter : intégralité de l’accord, nullité partielle, langue de référence, +date d’effet.] + +--- + +*Document généré comme base de travail pour le projet P'titsPas. Il ne +constitue pas un conseil juridique.* diff --git a/docs/juridique/cgu.md b/docs/juridique/cgu.md new file mode 100644 index 0000000..609e607 --- /dev/null +++ b/docs/juridique/cgu.md @@ -0,0 +1,119 @@ +# Conditions générales d’utilisation (CGU) + +**Produit** : P'titsPas +**Version document** : 0.2 (brouillon) +**Date** : avril 2026 +**Statut** : à valider juridiquement avant publication + +--- + +## 1. Objet et champ d’application + +Les présentes **conditions générales d’utilisation** (« CGU ») encadrent +l’accès et l’usage de la **plateforme logicielle P'titsPas** (ci-après le +« **Service** »), dédiée à l’organisation de la **garde d’enfants de 0 à 3 ans** +dans le cadre des **collectivités locales** ou structures habilitées. + +Toute **création de compte** ou **utilisation** du Service vaut acceptation des +CGU en vigueur au moment de l’utilisation. + +--- + +## 2. Définitions + +- **Éditeur** : la personne morale ou la collectivité responsable du + déploiement et de l’administration du Service. +- **Utilisateur** : toute personne disposant d’un compte (parent, assistante + maternelle, relais, gestionnaire, administrateur, etc.). +- **Données** : informations saisies ou générées dans le Service. + +--- + +## 3. Accès au Service + +Le Service est accessible selon les **modalités techniques** définies par +l’Éditeur (URL, authentification, pare-feu, etc.). + +L’Éditeur peut **suspendre** ou **restreindre** l’accès pour maintenance, +sécurité ou obligation légale, dans la mesure du possible en informant les +Utilisateurs par un moyen adapté. + +--- + +## 4. Comptes et sécurité + +Chaque Utilisateur s’engage à : + +- fournir des **informations exactes** et les mettre à jour ; +- **garder confidentiels** ses identifiants et mots de passe ; +- signaler sans délai toute **utilisation non autorisée** de son compte. + +L’Éditeur peut **désactiver** un compte en cas de manquement grave aux présentes +CGU ou à la réglementation applicable. + +--- + +## 5. Usage loyal du Service + +Il est notamment interdit d’utiliser le Service pour : + +- porter atteinte aux **droits des tiers** ou à l’**ordre public** ; +- tenter d’**accéder** de manière frauduleuse à des données ou des fonctions non + autorisées ; +- **perturber** le fonctionnement du Service (attaques, surcharge, etc.). + +--- + +## 6. Propriété intellectuelle + +Les **éléments** du Service (logiciel, charte graphique, documentation interne +non publique, etc.) restent la propriété de leurs titulaires respectifs, sauf +mention contraire. + +Les **contenus** saisis par les Utilisateurs (textes, pièces jointes autorisées) +restent sous leur responsabilité ; ils concèdent à l’Éditeur les droits +**strictement nécessaires** à l’hébergement et au fonctionnement du Service. + +--- + +## 7. Données personnelles + +Le traitement des données personnelles est décrit dans la [**politique de +confidentialité**](./politique-confidentialite.md) et les documents affichés +dans le Service, conformément au **RGPD** et aux obligations locales. + +--- + +## 8. Responsabilité + +Le Service est fourni **en l’état**, dans les limites de l’état de l’art et des +moyens raisonnables de l’Éditeur. + +L’Éditeur ne saurait être tenu responsable des **dommages indirects** ou des +**indisponibilités** indépendantes de sa volonté, dans les limites autorisées par +la loi. + +--- + +## 9. Évolution des CGU + +Les CGU peuvent être **modifiées**. Les Utilisateurs sont informés selon les +moyens prévus (notification, ré-affichage à la connexion, etc.). La **version** +acceptée est **tracée** dans le Service lorsque la fonctionnalité est activée. + +--- + +## 10. Droit applicable et litiges + +[À compléter : droit applicable, juridiction compétente, médiation éventuelle.] + +--- + +## 11. Contact + +[À compléter : coordonnées du DPO ou du service utilisateurs.] + +--- + +*Document généré comme base de travail pour le projet P'titsPas. Il ne +constitue pas un conseil juridique.* diff --git a/docs/juridique/cgu.pdf b/docs/juridique/cgu.pdf new file mode 100644 index 0000000..0ee1b68 Binary files /dev/null and b/docs/juridique/cgu.pdf differ diff --git a/docs/juridique/politique-confidentialite.md b/docs/juridique/politique-confidentialite.md new file mode 100644 index 0000000..2954929 --- /dev/null +++ b/docs/juridique/politique-confidentialite.md @@ -0,0 +1,188 @@ +# Politique de confidentialité + +**Produit** : P'titsPas +**Version document** : 0.1 (brouillon) +**Date** : avril 2026 +**Statut** : à valider juridiquement avant publication + +Document distinct des [**conditions générales d’utilisation**](./cgu.md) +(CGU), qui régissent l’usage du service. + +--- + +## 1. Introduction + +La présente **politique de confidentialité** décrit comment sont traitées les +**données à caractère personnel** dans le cadre de la plateforme **P'titsPas**, +application dédiée à l’organisation de la **garde d’enfants** (notamment 0 à 3 +ans) pour le compte de **collectivités** ou structures habilitées. + +Elle s’applique aux **Utilisateurs** du Service (parents, assistantes +maternelles, relais, gestionnaires, administrateurs, etc.). + +--- + +## 2. Responsable du traitement + +[À compléter : identité et coordonnées du **responsable du traitement** — en +général la collectivité ou la personne morale désignée selon le mode de +déploiement (on-premise, délégation, etc.).] + +Pour toute question relative à cette politique ou à vos droits : + +- **Contact** : [À compléter : courriel / adresse postale du service ou du + DPD/DPO.] +- **Délégué à la protection des données (DPD/DPO)** : [À compléter si désigné.] + +--- + +## 3. Données collectées + +Selon les profils et les fonctionnalités activées, peuvent notamment être +traitées : + +- **Données d’identité et de contact** : nom, prénom, adresse électronique, + numéro de téléphone, adresse postale, etc. +- **Données d’authentification** : identifiant, données de connexion, indicateur + de changement de mot de passe obligatoire, etc. +- **Données professionnelles** (assistantes maternelles, relais) : agrément, + capacité, présentation, pièces ou identifiants professionnels selon le + formulaire. +- **Données relatives aux enfants** : prénom, date de naissance, lien avec les + titulaires de l’autorité parentale, informations de santé ou de garde **uniquement** + dans la mesure où leur collecte est **nécessaire** au service et **encadrée** + par la collectivité et la loi. +- **Données de connexion et de traçabilité** : journaux techniques, adresse IP, + agent utilisateur, horodatages, traces d’**acceptation** des documents légaux + (CGU, présente politique), etc. +- **Contenus** saisis dans le Service (textes libres, messages si la messagerie + est activée, etc.). + +[À compléter : liste exhaustive alignée sur les écrans et tables réelles du +déploiement.] + +--- + +## 4. Finalités et bases légales + +Les traitements poursuivent notamment les finalités suivantes (bases légales +indicatives, **à affiner** avec le juriste) : + +| Finalité | Base légale (indicative) | +|----------|---------------------------| +| Création et gestion des **comptes** | Exécution de mesures précontractuelles / + contrat ; obligation légale le cas échéant | +| Gestion des **dossiers** de garde et des **inscriptions** | Mission de + service public / contrat / obligation légale selon contexte | +| **Sécurité** du Service, prévention des abus | Intérêt légitime ; obligation + légale | +| **Traçabilité** des acceptations de documents (CGU, politique) | Obligation + légale ; preuve d’un contrat | +| **Amélioration** et statistiques d’usage (si activées) | Intérêt légitime ou + consentement selon les moyens | + +--- + +## 5. Destinataires des données + +Les données peuvent être accessibles : + +- aux **personnels habilités** de la collectivité ou de l’Éditeur (selon les + rôles : gestionnaire, administrateur, etc.) ; +- aux **Utilisateurs** dans la limite de ce qui est nécessaire au fonctionnement + du dispositif (ex. dossier partagé) ; +- aux **sous-traitants** techniques strictement nécessaires (hébergement, + sauvegarde, messagerie, etc.) **sous contrat** conforme au RGPD. + +[À compléter : liste des sous-traitants et pays d’hébergement.] + +--- + +## 6. Durée de conservation + +Les données sont conservées : + +- pendant la **durée d’utilisation** du compte et du dossier ; +- puis, le cas échéant, pour les **durées légales** ou réglementaires de + conservation (archives, preuve, comptabilité, etc.). + +[À compléter : grille de conservation par type de donnée.] + +--- + +## 7. Vos droits + +Conformément au **RGPD**, vous disposez notamment des droits suivants (dans les +conditions et limites prévues par la loi) : + +- **droit d’accès** et de **rectification** ; +- **droit à l’effacement** (« droit à l’oubli ») ; +- **droit à la limitation** du traitement ; +- **droit à la portabilité** (le cas échéant) ; +- **droit d’opposition** (notamment pour prospection, selon contexte) ; +- **droit de retirer votre consentement** lorsque le traitement repose sur le + consentement ; +- **droit d’introduire une réclamation** auprès de la **CNIL** (France) : + [https://www.cnil.fr](https://www.cnil.fr). + +Pour exercer vos droits : [À compléter : procédure, délais de réponse, pièce +d’identité si exigée.] + +--- + +## 8. Sécurité + +L’Éditeur met en œuvre des **mesures techniques et organisationnelles** +appropriées (contrôle d’accès, chiffrement en transit selon configuration, +journalisation, mots de passe, etc.). + +[À compléter : politique de sauvegarde, PRA, certification éventuelle.] + +--- + +## 9. Transferts hors Union européenne + +[À compléter : absence de transfert, ou garanties (clauses types, pays +adéquats, etc.).] + +--- + +## 10. Cookies et traceurs (application web) + +Lorsque le Service est utilisé via un **navigateur**, des **cookies** ou +mécanismes équivalents peuvent être utilisés pour la **session**, la +**sécurité** ou des mesures d’**audience** si elles sont activées. + +[À compléter : liste des traceurs, finalité, durée, consentement si requis.] + +--- + +## 11. Données des enfants + +Les données concernant les **enfants** ne sont traitées que dans le **cadre +légal** de la garde et des missions de la collectivité, avec les **garants** +habituellement titulaires de l’autorité parentale ou personnes désignées par la +loi. + +[À compléter : référence au cadre local (protection de l’enfance, secret +professionnel, etc.).] + +--- + +## 12. Évolution de la politique + +La présente politique peut être **modifiée**. Les Utilisateurs en sont informés +par un **moyen adapté** ; une **nouvelle version** peut être présentée lors de la +connexion ou de l’inscription, avec **trace** de l’acceptation lorsque le +Service le prévoit. + +--- + +## 13. Contact + +[À compléter : même bloc que section 2 si besoin.] + +--- + +*Document généré comme base de travail pour le projet P'titsPas. Il ne +constitue pas un conseil juridique.* diff --git a/docs/juridique/politique-confidentialite.pdf b/docs/juridique/politique-confidentialite.pdf new file mode 100644 index 0000000..35190c9 Binary files /dev/null and b/docs/juridique/politique-confidentialite.pdf differ diff --git a/frontend/assets/documents/cgu_ptitspas.pdf b/frontend/assets/documents/cgu_ptitspas.pdf new file mode 100644 index 0000000..13ecab9 Binary files /dev/null and b/frontend/assets/documents/cgu_ptitspas.pdf differ diff --git a/frontend/lib/services/api/api_config.dart b/frontend/lib/services/api/api_config.dart index 5e32ef4..b3fe61d 100644 --- a/frontend/lib/services/api/api_config.dart +++ b/frontend/lib/services/api/api_config.dart @@ -27,6 +27,12 @@ class ApiConfig { return u; } final base = baseUrl.replaceAll(RegExp(r'/+$'), ''); + // Le back renvoie parfois des chemins déjà préfixés `/api/v1/...` (ex. documents + // légaux). Ne pas les coller à [baseUrl] sous peine de doubler `/api/v1`. + if (u.startsWith('/api/')) { + final origin = apiOrigin.replaceAll(RegExp(r'/+$'), ''); + return '$origin$u'; + } return u.startsWith('/') ? '$base$u' : '$base/$u'; } diff --git a/frontend/lib/services/user_service.dart b/frontend/lib/services/user_service.dart index 425e4bf..4db19ed 100644 --- a/frontend/lib/services/user_service.dart +++ b/frontend/lib/services/user_service.dart @@ -8,6 +8,39 @@ import 'package:p_tits_pas/models/dossier_unifie.dart'; import 'package:p_tits_pas/services/api/api_config.dart'; import 'package:p_tits_pas/services/api/tokenService.dart'; +class DocumentActifInfo { + final String id; + final String type; + final int version; + final String url; + + const DocumentActifInfo({ + required this.id, + required this.type, + required this.version, + required this.url, + }); + + factory DocumentActifInfo.fromJson(Map json) { + return DocumentActifInfo( + id: (json['id'] ?? '').toString(), + type: (json['type'] ?? '').toString(), + version: int.tryParse((json['version'] ?? '').toString()) ?? 0, + url: (json['url'] ?? '').toString(), + ); + } +} + +class DocumentsActifsInfo { + final DocumentActifInfo cgu; + final DocumentActifInfo privacy; + + const DocumentsActifsInfo({ + required this.cgu, + required this.privacy, + }); +} + class UserService { static Future> _headers() async { final token = await TokenService.getToken(); @@ -34,6 +67,46 @@ class UserService { return _toStr(err) ?? 'Erreur inconnue'; } + /// Documents légaux actifs (CGU + privacy). GET /documents-legaux/actifs. + static Future getDocumentsLegauxActifs() async { + final response = await http.get( + Uri.parse('${ApiConfig.baseUrl}/documents-legaux/actifs'), + headers: ApiConfig.headers, + ); + if (response.statusCode != 200) { + try { + final err = jsonDecode(response.body); + throw Exception(_errMessage(err is Map ? err['message'] : err)); + } catch (e) { + if (e is Exception) rethrow; + throw Exception( + 'Erreur chargement documents légaux (${response.statusCode})', + ); + } + } + + try { + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + throw const FormatException('Réponse invalide'); + } + final cgu = decoded['cgu']; + final privacy = decoded['privacy']; + if (cgu is! Map || privacy is! Map) { + throw const FormatException('Documents actifs manquants'); + } + return DocumentsActifsInfo( + cgu: DocumentActifInfo.fromJson(Map.from(cgu)), + privacy: DocumentActifInfo.fromJson(Map.from(privacy)), + ); + } catch (e) { + if (e is FormatException) rethrow; + throw Exception( + 'Réponse invalide (documents légaux): ${e is Exception ? e.toString() : "format inattendu"}', + ); + } + } + /// Utilisateurs en attente de validation (GET /users/pending). Ticket #107. static Future> getPendingUsers({String? role}) async { final query = role != null ? '?role=$role' : ''; diff --git a/frontend/lib/widgets/cgu_privacy_validation_dialog.dart b/frontend/lib/widgets/cgu_privacy_validation_dialog.dart new file mode 100644 index 0000000..73b9ac2 --- /dev/null +++ b/frontend/lib/widgets/cgu_privacy_validation_dialog.dart @@ -0,0 +1,449 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:http/http.dart' as http; +import 'package:pdfx/pdfx.dart'; +import 'package:universal_platform/universal_platform.dart'; + +import '../services/api/api_config.dart'; +import '../services/user_service.dart'; + +/// Tons **turquoise / menthe** (charte : `#8AD0C8`) — plus doux que le vert +/// Material « gazon ». +abstract final class _ModaleCouleur { + static const Color turquoise = Color(0xFF8AD0C8); + static const Color turquoiseFonce = Color(0xFF5A9D94); + static const Color mentheTresClair = Color(0xFFF3FAF8); + static const Color mentheClair = Color(0xFFE2F3EF); + static const Color ivoire = Color(0xFFFFFEF9); + static const Color encre = Color(0xFF2F2F2F); +} + +enum LegalDocType { + cgu, + privacy, +} + +class CguPrivacyValidationDialog extends StatefulWidget { + const CguPrivacyValidationDialog({super.key}); + + @override + State createState() => + _CguPrivacyValidationDialogState(); +} + +class _CguPrivacyValidationDialogState extends State { + bool _loadingLegalDocs = false; + DocumentActifInfo? _cguDoc; + DocumentActifInfo? _privacyDoc; + LegalDocType _selectedDocType = LegalDocType.cgu; + + static const double _cornerRadius = 16; + + @override + void initState() { + super.initState(); + _loadActiveLegalDocs(); + } + + Future _loadActiveLegalDocs() async { + setState(() => _loadingLegalDocs = true); + try { + final docs = await UserService.getDocumentsLegauxActifs(); + if (!mounted) return; + setState(() { + _cguDoc = docs.cgu; + _privacyDoc = docs.privacy; + }); + } catch (_) { + // Silencieux : message dans la zone PDF si besoin. + } finally { + if (mounted) setState(() => _loadingLegalDocs = false); + } + } + + /// Sur le web, [PdfDocument.openFile] n’est pas implémenté : on charge les + /// octets puis [PdfDocument.openData]. + Future _fetchPdfBytes(String pathOrUrl) async { + final absolute = ApiConfig.absoluteMediaUrl(pathOrUrl); + final response = await http.get(Uri.parse(absolute)); + if (response.statusCode != 200) { + throw StateError('PDF indisponible (HTTP ${response.statusCode})'); + } + return response.bodyBytes; + } + + Future? get _currentPdfDocumentFuture { + switch (_selectedDocType) { + case LegalDocType.cgu: + if (_cguDoc == null) return null; + return PdfDocument.openData(_fetchPdfBytes(_cguDoc!.url)); + case LegalDocType.privacy: + if (_privacyDoc == null) return null; + return PdfDocument.openData(_fetchPdfBytes(_privacyDoc!.url)); + } + } + + String get _dialogTitle { + switch (_selectedDocType) { + case LegalDocType.cgu: + return 'Conditions Générales d’Utilisation'; + case LegalDocType.privacy: + return 'Politique de Confidentialité'; + } + } + + bool get _canValidate { + return (_cguDoc != null && _privacyDoc != null) || + (_cguDoc != null && + _privacyDoc == null && + _selectedDocType == LegalDocType.cgu) || + (_privacyDoc != null && + _cguDoc == null && + _selectedDocType == LegalDocType.privacy); + } + + /// [PdfViewPinch] n’est pas supporté sur Windows (pdfx) : repli [PdfView]. + Widget _buildPdfBody() { + final doc = _currentPdfDocumentFuture!; + final key = ValueKey(_selectedDocType); + if (UniversalPlatform.isWindows) { + return PdfView( + key: key, + scrollDirection: Axis.vertical, + controller: PdfController(document: doc), + backgroundDecoration: const BoxDecoration(color: Colors.white), + ); + } + return _PdfPaneWithScrollbar( + key: key, + documentFuture: doc, + ); + } + + @override + Widget build(BuildContext context) { + final size = MediaQuery.sizeOf(context); + final dialogW = size.width * 0.92; + final dialogH = size.height * 0.88; + + return Dialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 20), + backgroundColor: Colors.transparent, + elevation: 0, + child: ClipRRect( + borderRadius: BorderRadius.circular(_cornerRadius), + child: Material( + color: _ModaleCouleur.ivoire, + elevation: 6, + shadowColor: _ModaleCouleur.encre.withValues(alpha: 0.12), + child: SizedBox( + width: dialogW, + height: dialogH, + child: Column( + children: [ + _buildHeader(context), + Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _tabButton( + context, + label: 'CGU', + selected: _selectedDocType == LegalDocType.cgu, + enabled: !_loadingLegalDocs && _cguDoc != null, + onTap: () => setState( + () => _selectedDocType = LegalDocType.cgu, + ), + ), + const SizedBox(width: 10), + _tabButton( + context, + label: 'Confidentialité', + selected: _selectedDocType == LegalDocType.privacy, + enabled: !_loadingLegalDocs && _privacyDoc != null, + onTap: () => setState( + () => _selectedDocType = LegalDocType.privacy, + ), + ), + ], + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 4, 12, 8), + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _ModaleCouleur.turquoiseFonce.withValues( + alpha: 0.35, + ), + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: _loadingLegalDocs + ? const Center( + child: CircularProgressIndicator( + color: _ModaleCouleur.turquoiseFonce, + strokeWidth: 3, + ), + ) + : _currentPdfDocumentFuture != null + ? _buildPdfBody() + : Center( + child: Text( + 'Document non disponible.', + style: GoogleFonts.merienda( + color: _ModaleCouleur.encre + .withValues(alpha: 0.75), + ), + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 520), + child: FilledButton( + onPressed: _canValidate + ? () => Navigator.of(context).pop(true) + : null, + style: FilledButton.styleFrom( + backgroundColor: _ModaleCouleur.turquoiseFonce, + foregroundColor: Colors.white, + disabledBackgroundColor: + _ModaleCouleur.mentheClair.withValues(alpha: 0.7), + disabledForegroundColor: + _ModaleCouleur.encre.withValues(alpha: 0.45), + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + ), + child: Text( + 'Valider les CGU et la Politique ' + 'de Confidentialité', + textAlign: TextAlign.center, + style: GoogleFonts.merienda( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + _ModaleCouleur.mentheTresClair, + _ModaleCouleur.mentheClair, + ], + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + _dialogTitle, + style: GoogleFonts.merienda( + fontSize: 17, + fontWeight: FontWeight.w600, + color: _ModaleCouleur.encre, + height: 1.25, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + IconButton( + tooltip: 'Fermer', + icon: Icon( + Icons.close, + color: _ModaleCouleur.encre.withValues(alpha: 0.55), + ), + onPressed: () => Navigator.of(context).pop(false), + ), + ], + ), + ), + ); + } + + Widget _tabButton( + BuildContext context, { + required String label, + required bool selected, + required bool enabled, + required VoidCallback onTap, + }) { + return OutlinedButton( + onPressed: enabled ? onTap : null, + style: OutlinedButton.styleFrom( + foregroundColor: _ModaleCouleur.encre, + backgroundColor: selected + ? _ModaleCouleur.turquoise + : _ModaleCouleur.mentheTresClair, + side: BorderSide( + color: _ModaleCouleur.turquoiseFonce.withValues(alpha: 0.55), + width: 1.5, + ), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), + ), + child: Text( + label, + style: GoogleFonts.merienda( + fontSize: 13, + fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + ), + ), + ); + } +} + +/// [PdfViewPinch] s’appuie sur un [InteractiveViewer], sans vrai [Scrollable] : +/// barre verticale reliée à [PdfControllerPinch.documentProgress] pour que le +/// « chariot » reste visible (surtout sur le web où la barre OS est souvent +/// masquée). +class _PdfPaneWithScrollbar extends StatefulWidget { + const _PdfPaneWithScrollbar({ + super.key, + required this.documentFuture, + }); + + final Future documentFuture; + + @override + State<_PdfPaneWithScrollbar> createState() => _PdfPaneWithScrollbarState(); +} + +class _PdfPaneWithScrollbarState extends State<_PdfPaneWithScrollbar> { + static const double _barW = 14; + static const double _thumbW = 9; + static const double _minThumbH = 44; + + late final PdfControllerPinch _ctrl; + + @override + void initState() { + super.initState(); + _ctrl = PdfControllerPinch(document: widget.documentFuture); + _ctrl.addListener(_onCtrl); + } + + void _onCtrl() { + // [PdfViewPinch] met à jour documentProgress dans son propre listener : + // replanifier pour lire la valeur après ce tour de notifications. + Future.microtask(() { + if (mounted) setState(() {}); + }); + } + + @override + void dispose() { + _ctrl.removeListener(_onCtrl); + _ctrl.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final raw = _ctrl.documentProgress; + final safeP = (!raw.isFinite || raw.isNaN) ? 0.0 : raw.clamp(0.0, 1.0); + + return Stack( + clipBehavior: Clip.hardEdge, + fit: StackFit.expand, + children: [ + Padding( + padding: const EdgeInsets.only(right: _barW), + child: PdfViewPinch( + padding: 10, + scrollDirection: Axis.vertical, + minScale: 1, + maxScale: 4, + controller: _ctrl, + backgroundDecoration: const BoxDecoration(color: Colors.white), + ), + ), + Positioned( + top: 6, + right: 1, + bottom: 6, + width: _barW, + child: LayoutBuilder( + builder: (context, c) { + final trackH = c.maxHeight; + final thumbH = (trackH * 0.2).clamp(_minThumbH, trackH * 0.5); + final maxTop = (trackH - thumbH).clamp(0.0, double.infinity); + final top = safeP * maxTop; + + return Semantics( + label: 'Position dans le document', + child: Stack( + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + color: _ModaleCouleur.turquoise.withValues( + alpha: 0.35, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + ), + Positioned( + top: top, + left: (_barW - _thumbW) / 2, + child: Container( + width: _thumbW, + height: thumbH, + decoration: BoxDecoration( + color: _ModaleCouleur.turquoiseFonce, + borderRadius: BorderRadius.circular(_thumbW / 2), + boxShadow: const [ + BoxShadow( + color: Color(0x40000000), + blurRadius: 3, + offset: Offset(0, 1), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/frontend/lib/widgets/presentation_form_screen.dart b/frontend/lib/widgets/presentation_form_screen.dart index ace8df1..fd2d310 100644 --- a/frontend/lib/widgets/presentation_form_screen.dart +++ b/frontend/lib/widgets/presentation_form_screen.dart @@ -1,7 +1,8 @@ +import 'dart:math' as math; + 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'; @@ -9,6 +10,7 @@ import 'custom_navigation_button.dart'; import 'hover_relief_widget.dart'; import '../models/card_assets.dart'; import '../config/display_config.dart'; +import 'cgu_privacy_validation_dialog.dart'; /// Widget générique pour le formulaire de présentation avec texte libre + CGU /// Supporte mode éditable et readonly, responsive mobile/desktop @@ -75,6 +77,52 @@ class _PresentationFormScreenState extends State { widget.onSubmit(_textController.text, _cguAccepted); } + Future _openCguValidationDialog() async { + final validated = await showDialog( + context: context, + builder: (context) => const CguPrivacyValidationDialog(), + ); + + if (validated == true && mounted) { + setState(() => _cguAccepted = true); + } + } + + Widget _buildCguValidationControl({ + required double fontSize, + bool compact = false, + }) { + final label = compact + ? 'J’ai validé les CGU et la\nPolitique de confidentialité' + : 'J’ai validé les Conditions Générales\nd’Utilisation et la Politique de confidentialité'; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: _cguAccepted, + onChanged: null, + ), + Flexible( + child: Text( + label, + style: GoogleFonts.merienda(fontSize: fontSize), + ), + ), + ], + ), + const SizedBox(height: 6), + TextButton.icon( + onPressed: _openCguValidationDialog, + icon: const Icon(Icons.open_in_new, size: 18), + label: const Text('Consulter et valider les CGU'), + ), + ], + ); + } + @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; @@ -451,11 +499,13 @@ class _PresentationFormScreenState extends State { ), ), const SizedBox(height: 20), - AppCustomCheckbox( - label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', - value: _cguAccepted, - onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false), - ), + config.isReadonly + ? AppCustomCheckbox( + label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', + value: _cguAccepted, + onChanged: (v) {}, + ) + : _buildCguValidationControl(fontSize: 16), ], ), ), @@ -507,12 +557,17 @@ class _PresentationFormScreenState extends State { const SizedBox(height: 16), // Checkbox en bas Transform.scale( - scale: 0.85, - child: AppCustomCheckbox( - label: 'J\'accepte les CGU et la\nPolitique de confidentialité', - value: _cguAccepted, - onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false), - ), + scale: 0.95, + child: config.isReadonly + ? AppCustomCheckbox( + label: 'J\'accepte les CGU et la\nPolitique de confidentialité', + value: _cguAccepted, + onChanged: (v) {}, + ) + : _buildCguValidationControl( + fontSize: 14, + compact: true, + ), ), ], ), @@ -549,13 +604,19 @@ class _PresentationFormScreenState extends State { const SizedBox(width: 16), Expanded( child: HoverReliefWidget( - child: CustomNavigationButton( - text: 'Suivant', - style: NavigationButtonStyle.green, - onPressed: _handleSubmit, - width: double.infinity, - height: 50, - fontSize: 16, + child: Opacity( + opacity: _cguAccepted ? 1 : 0.55, + child: AbsorbPointer( + absorbing: !_cguAccepted, + child: CustomNavigationButton( + text: 'Suivant', + style: NavigationButtonStyle.green, + onPressed: _handleSubmit, + width: double.infinity, + height: 50, + fontSize: 16, + ), + ), ), ), ), diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 8d88817..48350ec 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + extension: + dependency: transitive + description: + name: extension + sha256: be3a6b7f8adad2f6e2e8c63c895d19811fcf203e23466c6296267941d0ff4f24 + url: "https://pub.dev" + source: hosted + version: "0.6.0" fake_async: dependency: transitive description: @@ -113,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -397,6 +413,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdfx: + dependency: "direct main" + description: + name: pdfx + sha256: "29db9b71d46bf2335e001f91693f2c3fbbf0760e4c2eb596bf4bafab211471c1" + url: "https://pub.dev" + source: hosted + version: "2.9.2" + photo_view: + dependency: transitive + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.dev" + source: hosted + version: "0.15.0" platform: dependency: transitive description: @@ -514,6 +546,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://pub.dev" + source: hosted + version: "3.3.1" term_glyph: dependency: transitive description: @@ -538,6 +578,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + universal_platform: + dependency: "direct main" + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" url_launcher: dependency: "direct main" description: @@ -602,6 +650,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 369295c..d9f9b57 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: url_launcher: ^6.2.4 http: ^1.2.2 # flutter_secure_storage: ^9.0.0 + pdfx: ^2.5.0 + universal_platform: ^1.1.0 dev_dependencies: flutter_test: @@ -32,6 +34,7 @@ flutter: assets: - assets/images/ # Déclarer le dossier entier - assets/cards/ # Nouveau dossier de cartes + - assets/documents/ fonts: - family: Merienda diff --git a/frontend/web/index.html b/frontend/web/index.html index 9b2a41d..da896cd 100644 --- a/frontend/web/index.html +++ b/frontend/web/index.html @@ -47,6 +47,16 @@ + +