merge(master): intégration develop — ticket #50, documents légaux et validation CGU

Squash merge de la branche develop dans master : un seul commit sur master pour regrouper la livraison liée au ticket #50 (affichage dynamique des CGU et de la politique de confidentialité à l'inscription) ainsi que la documentation et les ajustements associés.

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

Documentation: jeux de fichiers juridiques (markdown et PDF de référence), réorganisation (dossier docs/juridique, archives, renommage du briefing), mises à jour index, liste de tickets et décisions projet. Scripts et métadonnées mineures (Gitea, pubspec, index web).
Made-with: Cursor
This commit is contained in:
MARTIN Julien 2026-04-17 17:33:47 +02:00
parent 241ce36b85
commit d91caef0c5
34 changed files with 2169 additions and 912 deletions

View File

@ -1,7 +1,7 @@
/** /**
* Commentaire de clôture + fermeture issue Gitea #120. * Commentaire de clôture + fermeture issue Gitea #120.
* Usage: node backend/scripts/close-gitea-issue-120.js * 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 https = require('https');
const fs = require('fs'); const fs = require('fs');
@ -45,7 +45,10 @@ if (!token) {
} }
if (!token) { if (!token) {
try { 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]+)/); const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/);
if (m) token = m[1].trim(); if (m) token = m[1].trim();
} catch (_) {} } catch (_) {}

View File

@ -1,7 +1,8 @@
/** /**
* Crée l'issue Gitea "[Frontend] Inscription Parent Branchement soumission formulaire à l'API" * Crée l'issue Gitea "[Frontend] Inscription Parent Branchement soumission formulaire à l'API"
* Usage: node backend/scripts/create-gitea-issue-parent-api.js * 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 https = require('https');
const fs = require('fs'); const fs = require('fs');
@ -19,13 +20,18 @@ if (!token) {
} }
if (!token) { if (!token) {
try { 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]+)/); const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/);
if (m) token = m[1].trim(); if (m) token = m[1].trim();
} catch (_) {} } catch (_) {}
} }
if (!token) { 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); process.exit(1);
} }

View File

@ -1,6 +1,6 @@
/** /**
* Liste toutes les issues Gitea (ouvertes + fermées) pour jmartin/petitspas. * 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 https = require('https');
const fs = require('fs'); const fs = require('fs');
@ -16,7 +16,10 @@ if (!token) {
} }
if (!token) { if (!token) {
try { 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]+)/); const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/);
if (m) token = m[1].trim(); if (m) token = m[1].trim();
} catch (_) {} } catch (_) {}

View File

@ -1,7 +1,7 @@
/** /**
* Met à jour l'issue Gitea #119 : endpoint unifié GET /dossiers/:numeroDossier (option A) * Met à jour l'issue Gitea #119 : endpoint unifié GET /dossiers/:numeroDossier (option A)
* Usage: node backend/scripts/update-gitea-issue-119-dossiers.js * 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 https = require('https');
const fs = require('fs'); const fs = require('fs');
@ -17,7 +17,10 @@ if (!token) {
} }
if (!token) { if (!token) {
try { 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]+)/); const m = briefing.match(/Token:\s*(giteabu_[a-f0-9]+)/);
if (m) token = m[1].trim(); if (m) token = m[1].trim();
} catch (_) {} } catch (_) {}

View File

@ -19,15 +19,39 @@ Ce fichier sert d'index pour naviguer dans toute la documentation du projet.
### Développement ### Développement
- [**10 - Database Schema**](./10_DATABASE.md) - Schéma de la base de données et modèles - [**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 - [**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 ### Workflows Fonctionnels
- [**20 - Workflow Création de Compte**](./20_WORKFLOW-CREATION-COMPTE.md) - Workflow complet de création et validation des comptes utilisateurs - [**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 - [**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 - [**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 - [**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 - [**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) - [**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) ### Administration (À créer)
- [**30 - Guide d'administration**](./30_ADMIN.md) - Gestion des utilisateurs, accès PgAdmin, logs - [**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) # Fichier déplacé
**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é
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é | | 44 | [Frontend] Dashboard Gestionnaire - Structure | ✅ Terminé |
| 45 | [Frontend] Dashboard Gestionnaire - Liste Parents | ✅ Fermé (obsolète, couvert #103-#111) | | 45 | [Frontend] Dashboard Gestionnaire - Liste Parents | ✅ Fermé (obsolète, couvert #103-#111) |
| 46 | [Frontend] Dashboard Gestionnaire - Liste AM | ✅ 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 | | 48 | [Frontend] Gestion Erreurs & Messages | Ouvert |
| 49 | [Frontend] Écran Gestion Documents Légaux (Admin) | Ouvert | | 49 | [Frontend] Écran Gestion Documents Légaux (Admin) | Ouvert |
| 50 | [Frontend] Affichage dynamique CGU lors inscription | 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 - [ ] Créer migration Prisma
- [ ] Tester migration - [ ] 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()` - [ ] Méthode `listerVersions()`
- [ ] Tests unitaires - [ ] 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 - [ ] Guards + validation
- [ ] Tests unitaires - [ ] 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) - [ ] API `GET /api/v1/users/:id/acceptations` (admin)
- [ ] Tests unitaires - [ ] 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 - [ ] Activation version
- [ ] Historique versions - [ ] 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). **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 - Activation manuelle après prévisualisation
- Documents génériques v1 fournis par défaut - 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 ├── 11_API.md
├── 20_WORKFLOW-CREATION-COMPTE.md ├── 20_WORKFLOW-CREATION-COMPTE.md
├── 21_CONFIGURATION-SYSTEME.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 ├── 23_LISTE-TICKETS.md
├── 24_DECISIONS-PROJET.md (ce document) ├── 24_DECISIONS-PROJET.md (ce document)
├── 90_AUDIT.md ├── 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 **[26_GITEA-API.md](./26_GITEA-API.md)**
- **API de base** : `https://git.ptits-pas.fr/api/v1`
- **Projet P'titsPas** : dépôt `jmartin/petitspas` (owner = `jmartin`, repo = `petitspas`)
## 2. Authentification Lancienne copie `PROCEDURE-API-GITEA.md` est archivée dans
`docs/archive/obsolete/` (doublon).
### 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.

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 # 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 ## 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 # 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 **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 | | `11_API.md` | Endpoints API |
| `20_WORKFLOW-CREATION-COMPTE.md` | Workflow création compte | | `20_WORKFLOW-CREATION-COMPTE.md` | Workflow création compte |
| `23_LISTE-TICKETS.md` | Liste des tickets | | `23_LISTE-TICKETS.md` | Liste des tickets |
| `BRIEFING-FRONTEND.md` | Brief frontend, accès Git, tickets prioritaires | | `27_BRIEFING-FRONTEND.md` | Brief frontend, accès Git, tickets prioritaires |
| `PROCEDURE-API-GITEA.md` | Utilisation API Gitea (issues, PR, token) | | `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 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) # 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é. 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; return u;
} }
final base = baseUrl.replaceAll(RegExp(r'/+$'), ''); 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'; 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/api_config.dart';
import 'package:p_tits_pas/services/api/tokenService.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 { class UserService {
static Future<Map<String, String>> _headers() async { static Future<Map<String, String>> _headers() async {
final token = await TokenService.getToken(); final token = await TokenService.getToken();
@ -34,6 +67,46 @@ class UserService {
return _toStr(err) ?? 'Erreur inconnue'; 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. /// Utilisateurs en attente de validation (GET /users/pending). Ticket #107.
static Future<List<AppUser>> getPendingUsers({String? role}) async { static Future<List<AppUser>> getPendingUsers({String? role}) async {
final query = role != null ? '?role=$role' : ''; 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:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'dart:math' as math;
import 'custom_decorated_text_field.dart'; import 'custom_decorated_text_field.dart';
import 'app_custom_checkbox.dart'; import 'app_custom_checkbox.dart';
@ -9,6 +10,7 @@ import 'custom_navigation_button.dart';
import 'hover_relief_widget.dart'; import 'hover_relief_widget.dart';
import '../models/card_assets.dart'; import '../models/card_assets.dart';
import '../config/display_config.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 /// Widget générique pour le formulaire de présentation avec texte libre + CGU
/// Supporte mode éditable et readonly, responsive mobile/desktop /// Supporte mode éditable et readonly, responsive mobile/desktop
@ -75,6 +77,52 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
widget.onSubmit(_textController.text, _cguAccepted); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
@ -451,11 +499,13 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
AppCustomCheckbox( config.isReadonly
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité', ? AppCustomCheckbox(
value: _cguAccepted, label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité',
onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false), value: _cguAccepted,
), onChanged: (v) {},
)
: _buildCguValidationControl(fontSize: 16),
], ],
), ),
), ),
@ -507,12 +557,17 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
const SizedBox(height: 16), const SizedBox(height: 16),
// Checkbox en bas // Checkbox en bas
Transform.scale( Transform.scale(
scale: 0.85, scale: 0.95,
child: AppCustomCheckbox( child: config.isReadonly
label: 'J\'accepte les CGU et la\nPolitique de confidentialité', ? AppCustomCheckbox(
value: _cguAccepted, label: 'J\'accepte les CGU et la\nPolitique de confidentialité',
onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false), value: _cguAccepted,
), onChanged: (v) {},
)
: _buildCguValidationControl(
fontSize: 14,
compact: true,
),
), ),
], ],
), ),
@ -549,13 +604,19 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: HoverReliefWidget( child: HoverReliefWidget(
child: CustomNavigationButton( child: Opacity(
text: 'Suivant', opacity: _cguAccepted ? 1 : 0.55,
style: NavigationButtonStyle.green, child: AbsorbPointer(
onPressed: _handleSubmit, absorbing: !_cguAccepted,
width: double.infinity, child: CustomNavigationButton(
height: 50, text: 'Suivant',
fontSize: 16, style: NavigationButtonStyle.green,
onPressed: _handleSubmit,
width: double.infinity,
height: 50,
fontSize: 16,
),
),
), ),
), ),
), ),

View File

@ -57,6 +57,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.0.6"
extension:
dependency: transitive
description:
name: extension
sha256: be3a6b7f8adad2f6e2e8c63c895d19811fcf203e23466c6296267941d0ff4f24
url: "https://pub.dev"
source: hosted
version: "0.6.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -113,6 +121,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.9.3+4" version: "0.9.3+4"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -397,6 +413,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" 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: platform:
dependency: transitive dependency: transitive
description: description:
@ -514,6 +546,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
url: "https://pub.dev"
source: hosted
version: "3.3.1"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -538,6 +578,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" 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: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@ -602,6 +650,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.4" version: "3.1.4"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -20,6 +20,8 @@ dependencies:
url_launcher: ^6.2.4 url_launcher: ^6.2.4
http: ^1.2.2 http: ^1.2.2
# flutter_secure_storage: ^9.0.0 # flutter_secure_storage: ^9.0.0
pdfx: ^2.5.0
universal_platform: ^1.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -32,6 +34,7 @@ flutter:
assets: assets:
- assets/images/ # Déclarer le dossier entier - assets/images/ # Déclarer le dossier entier
- assets/cards/ # Nouveau dossier de cartes - assets/cards/ # Nouveau dossier de cartes
- assets/documents/
fonts: fonts:
- family: Merienda - family: Merienda

View File

@ -47,6 +47,16 @@
<script src="flutter.js" defer></script> <script src="flutter.js" defer></script>
</head> </head>
<body> <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> <script>
window.addEventListener('load', function(ev) { window.addEventListener('load', function(ev) {
// Download main.dart.js // Download main.dart.js

View File

@ -18,15 +18,15 @@ fi
if [ -z "$GITEA_TOKEN" ] && [ -f ~/.bashrc ]; then if [ -z "$GITEA_TOKEN" ] && [ -f ~/.bashrc ]; then
eval "$(grep '^export GITEA_TOKEN=' ~/.bashrc 2>/dev/null)" || true eval "$(grep '^export GITEA_TOKEN=' ~/.bashrc 2>/dev/null)" || true
fi fi
if [ -z "$GITEA_TOKEN" ] && [ -f docs/BRIEFING-FRONTEND.md ]; then 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/BRIEFING-FRONTEND.md 2>/dev/null | head -1) 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 if [ -n "$token_from_briefing" ]; then
GITEA_TOKEN="$token_from_briefing" GITEA_TOKEN="$token_from_briefing"
fi fi
fi fi
if [ -z "$GITEA_TOKEN" ]; then 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 exit 1
fi fi