# 📄 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é