feat(frontend): #50 — validation CGU et politique de confidentialité

- Modale de validation avec chargement des PDF distants, PdfViewPinch et barre de progression latérale (repli PdfView sous Windows).

- Service documents légaux actifs, correction des URLs média, intégration au formulaire de présentation.

- Documentation juridique et réorganisation (archive, index, tickets).

Made-with: Cursor
This commit is contained in:
MARTIN Julien 2026-04-17 17:22:45 +02:00
parent 241ce36b85
commit 7381ce356d
34 changed files with 2169 additions and 912 deletions

View File

@ -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 (_) {}

View File

@ -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);
}

View File

@ -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 (_) {}

View File

@ -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 (_) {}

View File

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

View File

@ -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_<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`
```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<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
```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<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
```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<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)
```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 <super_admin_token>
```
**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 <super_admin_token>
Content-Type: multipart/form-data
```
**Body** :
```
type: cgu
file: <fichier PDF>
```
**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 <super_admin_token>
```
**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"
<binary PDF data>
```
---
### API 6 : Historique acceptations utilisateur (Admin)
```http
GET /api/v1/users/:userId/acceptations
Authorization: Bearer <super_admin_token>
```
**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 = '<uuid>';
-- Puis suppression utilisateur
DELETE FROM utilisateurs WHERE id = '<uuid>';
```
---
## 📚 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.

View File

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

View File

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

View File

@ -1,176 +1,8 @@
# Procédure Utilisation de lAPI 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 lenvironnement (ex. `~/.bashrc`) :
```bash
export GITEA_TOKEN="<votre_token>"
```
Pour lutiliser 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 dune 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 lissue #47
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
# Fermer lissue #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 dune 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 len-tête `Authorization: token <TOKEN>`.
- **404** : vérifier owner/repo et lURL (sensible à la casse).
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.
Lancienne copie `PROCEDURE-API-GITEA.md` est archivée dans
`docs/archive/obsolete/` (doublon).

30
docs/archive/README.md Normal file
View File

@ -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_` sapplique surtout aux
fichiers **directement** sous `docs/`.

View File

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

View File

@ -0,0 +1,180 @@
# Procédure Utilisation de lAPI 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 lenvironnement (ex. `~/.bashrc`) :
```bash
export GITEA_TOKEN="<votre_token>"
```
Pour lutiliser 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 dune 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 lissue #47
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues/47" | jq .
# Fermer lissue #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 dune 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 len-tête `Authorization: token <TOKEN>`.
- **404** : vérifier owner/repo et lURL (sensible à la casse).
- **422 / body invalide** : pour POST/PATCH, envoyer `Content-Type: application/json` et un JSON valide.

View File

@ -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 densemble est dans
[**02_ARCHITECTURE.md**](../../02_ARCHITECTURE.md). |
| `STATUS-APPLICATION.md` | Instantané daté ; non tenu comme doc vivante. |
Après vérification quaucun lien externe ne pointe encore vers ces chemins, on
peut **supprimer** ce sous-dossier ou ne garder que des pointeurs minimalistes.

View File

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

View File

@ -1,3 +1,4 @@
# Archivé docs/archive/temporaires/ — export jetable, supprimer si inutile.
Point tickets frontend (API Gitea) - 27/01/2026
================================================

View File

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

View File

@ -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 na été faite dans le chantier backend associé.

View File

@ -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_<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`
```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<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
```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<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
```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<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)
```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 <super_admin_token>
```
**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 <super_admin_token>
Content-Type: multipart/form-data
```
**Body** :
```
type: cgu
file: <fichier PDF>
```
**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 <super_admin_token>
```
**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"
<binary PDF data>
```
---
### API 6 : Historique acceptations utilisateur (Admin)
```http
GET /api/v1/users/:userId/acceptations
Authorization: Bearer <super_admin_token>
```
**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 = '<uuid>';
-- Puis suppression utilisateur
DELETE FROM utilisateurs WHERE id = '<uuid>';
```
---
## 📚 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é

41
docs/juridique/README.md Normal file
View File

@ -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 lapplication : notamment
lécran d**acceptation à linscription** (CGU + politique de confidentialité).
## Documents « application » (à exporter en PDF)
| Fichier | Rôle |
|--------|------|
| [**cgu.md**](./cgu.md) | **Conditions générales dutilisation** 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
dengagement 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).

116
docs/juridique/cgc.md Normal file
View File

@ -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 nest 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 lusage de la
plateforme **P'titsPas** pour la gestion de la garde denfants au sein des
**collectivités** ou structures habilitées.
Elles complètent les [**conditions générales dutilisation**](./cgu.md) (CGU),
qui régissent lusage du **Service numérique** lui-même.
---
## 2. Parties
Selon les cas dusage du déploiement, peuvent notamment être parties :
- la **collectivité** ou l**employeur public** porteur du dispositif ;
- l**Éditeur** ou l**opérateur** assurant lhébergement et la maintenance du
Service ;
- les **Utilisateurs** (parents, assistantes maternelles, relais dassistantes
maternelles, gestionnaires, etc.).
[À compléter : identification précise des personnes morales signataires et
rôles (maîtrise douvrage, 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 dacteur ;
- les **conditions dinscription**, 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 sengagent à :
- respecter le **cadre légal** applicable à la garde denfants et à la
protection de lenfance ;
- ne fournir que des **documents** et **informations** licites et, le cas échéant,
conformes aux demandes des gestionnaires ;
- respecter la **charte dusage** 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** dutilisation 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** dun compte Utilisateur nimplique 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 lexige, 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 laccord, nullité partielle, langue de référence,
date deffet.]
---
*Document généré comme base de travail pour le projet P'titsPas. Il ne
constitue pas un conseil juridique.*

119
docs/juridique/cgu.md Normal file
View File

@ -0,0 +1,119 @@
# Conditions générales dutilisation (CGU)
**Produit** : P'titsPas
**Version document** : 0.2 (brouillon)
**Date** : avril 2026
**Statut** : à valider juridiquement avant publication
---
## 1. Objet et champ dapplication
Les présentes **conditions générales dutilisation** (« CGU ») encadrent
laccès et lusage de la **plateforme logicielle P'titsPas** (ci-après le
« **Service** »), dédiée à lorganisation de la **garde denfants 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 lutilisation.
---
## 2. Définitions
- **Éditeur** : la personne morale ou la collectivité responsable du
déploiement et de ladministration du Service.
- **Utilisateur** : toute personne disposant dun 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** laccè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 sengage à :
- 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 dutiliser 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** à lhé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 lart 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.*

BIN
docs/juridique/cgu.pdf Normal file

Binary file not shown.

View File

@ -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 dutilisation**](./cgu.md)
(CGU), qui régissent lusage 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 à lorganisation de la **garde denfants** (notamment 0 à 3
ans) pour le compte de **collectivités** ou structures habilitées.
Elle sapplique 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 didentité et de contact** : nom, prénom, adresse électronique,
numéro de téléphone, adresse postale, etc.
- **Données dauthentification** : 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 lautorité 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 dun contrat |
| **Amélioration** et statistiques dusage (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 dhébergement.]
---
## 6. Durée de conservation
Les données sont conservées :
- pendant la **durée dutilisation** 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 daccès** et de **rectification** ;
- **droit à leffacement** (« droit à loubli ») ;
- **droit à la limitation** du traitement ;
- **droit à la portabilité** (le cas échéant) ;
- **droit dopposition** (notamment pour prospection, selon contexte) ;
- **droit de retirer votre consentement** lorsque le traitement repose sur le
consentement ;
- **droit dintroduire 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
didentité si exigée.]
---
## 8. Sécurité
LÉditeur met en œuvre des **mesures techniques et organisationnelles**
appropriées (contrôle daccè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 lautorité parentale ou personnes désignées par la
loi.
[À compléter : référence au cadre local (protection de lenfance, 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 linscription, avec **trace** de lacceptation 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.*

Binary file not shown.

Binary file not shown.

View File

@ -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';
}

View File

@ -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<String, dynamic> 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<Map<String, String>> _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<DocumentsActifsInfo> 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<String, dynamic>) {
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<String, dynamic>.from(cgu)),
privacy: DocumentActifInfo.fromJson(Map<String, dynamic>.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<List<AppUser>> getPendingUsers({String? role}) async {
final query = role != null ? '?role=$role' : '';

View File

@ -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<CguPrivacyValidationDialog> createState() =>
_CguPrivacyValidationDialogState();
}
class _CguPrivacyValidationDialogState extends State<CguPrivacyValidationDialog> {
bool _loadingLegalDocs = false;
DocumentActifInfo? _cguDoc;
DocumentActifInfo? _privacyDoc;
LegalDocType _selectedDocType = LegalDocType.cgu;
static const double _cornerRadius = 16;
@override
void initState() {
super.initState();
_loadActiveLegalDocs();
}
Future<void> _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] nest pas implémenté : on charge les
/// octets puis [PdfDocument.openData].
Future<Uint8List> _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<PdfDocument>? 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 dUtilisation';
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] nest 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] sappuie sur un [InteractiveViewer], sans vrai [Scrollable] :
/// barre verticale reliée à [PdfControllerPinch.documentProgress] pour que le
/// « chariot » reste visible (surtout sur le web la barre OS est souvent
/// masquée).
class _PdfPaneWithScrollbar extends StatefulWidget {
const _PdfPaneWithScrollbar({
super.key,
required this.documentFuture,
});
final Future<PdfDocument> 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),
),
],
),
),
),
],
),
);
},
),
),
],
);
}
}

View File

@ -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<PresentationFormScreen> {
widget.onSubmit(_textController.text, _cguAccepted);
}
Future<void> _openCguValidationDialog() async {
final validated = await showDialog<bool>(
context: context,
builder: (context) => const CguPrivacyValidationDialog(),
);
if (validated == true && mounted) {
setState(() => _cguAccepted = true);
}
}
Widget _buildCguValidationControl({
required double fontSize,
bool compact = false,
}) {
final label = compact
? 'Jai validé les CGU et la\nPolitique de confidentialité'
: 'Jai validé les Conditions Générales\ndUtilisation 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<PresentationFormScreen> {
),
),
const SizedBox(height: 20),
AppCustomCheckbox(
config.isReadonly
? 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),
),
onChanged: (v) {},
)
: _buildCguValidationControl(fontSize: 16),
],
),
),
@ -507,11 +557,16 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
const SizedBox(height: 16),
// Checkbox en bas
Transform.scale(
scale: 0.85,
child: AppCustomCheckbox(
scale: 0.95,
child: config.isReadonly
? AppCustomCheckbox(
label: 'J\'accepte les CGU et la\nPolitique de confidentialité',
value: _cguAccepted,
onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false),
onChanged: (v) {},
)
: _buildCguValidationControl(
fontSize: 14,
compact: true,
),
),
],
@ -549,6 +604,10 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
const SizedBox(width: 16),
Expanded(
child: HoverReliefWidget(
child: Opacity(
opacity: _cguAccepted ? 1 : 0.55,
child: AbsorbPointer(
absorbing: !_cguAccepted,
child: CustomNavigationButton(
text: 'Suivant',
style: NavigationButtonStyle.green,
@ -559,6 +618,8 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
),
),
),
),
),
],
),
);

View File

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

View File

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

View File

@ -47,6 +47,16 @@
<script src="flutter.js" defer></script>
</head>
<body>
<script src='https://cdn.jsdelivr.net/npm/pdfjs-dist@4.6.82/build/pdf.min.mjs' type='module'></script>
<script type='module'>
var { pdfjsLib } = globalThis;
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.6.82/build/pdf.worker.mjs';
var pdfRenderOptions = {
cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.6.82/cmaps/',
cMapPacked: true,
}
</script>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js

View File

@ -18,15 +18,15 @@ fi
if [ -z "$GITEA_TOKEN" ] && [ -f ~/.bashrc ]; then
eval "$(grep '^export GITEA_TOKEN=' ~/.bashrc 2>/dev/null)" || true
fi
if [ -z "$GITEA_TOKEN" ] && [ -f docs/BRIEFING-FRONTEND.md ]; then
token_from_briefing=$(sed -n 's/.*Token: *\(giteabu_[a-f0-9]*\).*/\1/p' docs/BRIEFING-FRONTEND.md 2>/dev/null | head -1)
if [ -z "$GITEA_TOKEN" ] && [ -f docs/27_BRIEFING-FRONTEND.md ]; then
token_from_briefing=$(sed -n 's/.*Token: *\(giteabu_[a-f0-9]*\).*/\1/p' docs/27_BRIEFING-FRONTEND.md 2>/dev/null | head -1)
if [ -n "$token_from_briefing" ]; then
GITEA_TOKEN="$token_from_briefing"
fi
fi
if [ -z "$GITEA_TOKEN" ]; then
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea (voir docs/PROCEDURE-API-GITEA.md)."
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea (voir docs/26_GITEA-API.md)."
exit 1
fi