petitspas/docs/juridique/22_DOCUMENTS-LEGAUX.md
Julien Martin d91caef0c5 merge(master): intégration develop — ticket #50, documents légaux et validation CGU
Squash merge de la branche develop dans master : un seul commit sur master pour regrouper la livraison liée au ticket #50 (affichage dynamique des CGU et de la politique de confidentialité à l'inscription) ainsi que la documentation et les ajustements associés.

Frontend (P'titsPas) : modale de validation bloquante avec onglets CGU / confidentialité, chargement des PDF depuis l'API des documents légaux actifs, affichage via pdfx (PdfViewPinch et barre de progression latérale sur le web, repli PdfView sous Windows). Branchement dans le flux d'inscription (écran de présentation), enrichissement du UserService et correction des chemins média pour les URL absolues.

Documentation: jeux de fichiers juridiques (markdown et PDF de référence), réorganisation (dossier docs/juridique, archives, renommage du briefing), mises à jour index, liste de tickets et décisions projet. Scripts et métadonnées mineures (Gitea, pubspec, index web).
Made-with: Cursor
2026-04-17 17:33:47 +02:00

23 KiB

📄 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
  2. Architecture
  3. Tables BDD
  4. Service Documents Légaux
  5. Workflow Upload & Activation
  6. Workflow Acceptation Utilisateur
  7. APIs
  8. Interface Admin
  9. 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_<timestamp>.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

-- 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

-- 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
  • : ip_address
  • Comment : user_agent

Modification table utilisateurs

-- 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

-- 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)

// 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<DocumentLegal>,
  ) {}

  // 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<DocumentLegal> {
    // 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<void> {
    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<DocumentLegal[]> {
    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<boolean> {
    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

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<br/>{type: 'cgu', file: PDF}
    
    API->>API: Validation fichier<br/>(PDF, max 10MB)
    API->>API: Calcul hash SHA-256
    
    API->>DB: SELECT MAX(version)<br/>WHERE type='cgu'
    DB-->>API: version = 3
    
    API->>API: Nouvelle version = 4
    
    API->>FS: Enregistrer fichier<br/>/documents/legaux/cgu_v4_<timestamp>.pdf
    FS-->>API: ✅ Fichier sauvegardé
    
    API->>DB: INSERT INTO documents_legaux<br/>(type, version=4, actif=false)
    DB-->>API: ✅ Document créé
    
    API-->>A: 201 Created<br/>{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<br/>SET actif=false WHERE type='cgu'
    API->>DB: UPDATE documents_legaux<br/>SET actif=true, active_le=now()<br/>WHERE id={id}
    API->>DB: COMMIT
    
    API-->>A: ✅ CGU v4 activées

📥 Workflow Acceptation Utilisateur

Diagramme de séquence

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<br/>WHERE actif=true
    DB-->>API: {cgu: v4, privacy: v2}
    API-->>App: {cgu: {version: 4, url: '...'}, privacy: {...}}
    
    App->>App: Afficher liens PDF<br/>"CGU v4" et "Privacy v2"
    
    U->>U: Lit les documents
    U->>U: Coche "J'accepte"
    
    App->>API: POST /api/v1/auth/register<br/>{..., cgu_version: 4, privacy_version: 2, ip, user_agent}
    
    API->>DB: BEGIN TRANSACTION
    
    API->>DB: INSERT INTO utilisateurs<br/>(..., cgu_version_acceptee=4, privacy_version_acceptee=2)
    DB-->>API: id_utilisateur
    
    API->>DB: INSERT INTO acceptations_documents<br/>(id_utilisateur, type='cgu', version=4, ip, user_agent)
    API->>DB: INSERT INTO acceptations_documents<br/>(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)

GET /api/v1/documents-legaux/actifs

Réponse 200 :

{
  "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)

GET /api/v1/documents-legaux/:type/versions
Authorization: Bearer <super_admin_token>

Paramètres :

  • type : cgu | privacy

Réponse 200 :

[
  {
    "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)

POST /api/v1/documents-legaux
Authorization: Bearer <super_admin_token>
Content-Type: multipart/form-data

Body :

type: cgu
file: <fichier PDF>

Réponse 201 :

{
  "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)

PATCH /api/v1/documents-legaux/:id/activer
Authorization: Bearer <super_admin_token>

Réponse 200 :

{
  "message": "Document activé avec succès",
  "documentId": "uuid-nouveau-doc",
  "type": "cgu",
  "version": 5
}

API 5 : Télécharger document (Public)

GET /api/v1/documents-legaux/:id/download

Réponse 200 :

Content-Type: application/pdf
Content-Disposition: attachment; filename="CGU_v5.pdf"

<binary PDF data>

API 6 : Historique acceptations utilisateur (Admin)

GET /api/v1/users/:userId/acceptations
Authorization: Bearer <super_admin_token>

Réponse 200 :

[
  {
    "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 :

-- Anonymisation (pas suppression totale)
UPDATE acceptations_documents
SET ip_address = NULL,
    user_agent = NULL
WHERE id_utilisateur = '<uuid>';

-- Puis suppression utilisateur
DELETE FROM utilisateurs WHERE id = '<uuid>';

📚 Références

Documentation interne

Documentation externe


Dernière mise à jour : 25 Novembre 2025
Version : 1.0
Statut : Document validé