petitspas/docs/22_DOCUMENTS-LEGAUX.md

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é