petitspas/docs/21_CONFIGURATION-SYSTEME.md
Julien Martin be8b1f23ed docs: concept v1.3 config (panneau Paramètres 3 sections, numéros = Gitea)
- 21_CONFIGURATION-SYSTEME: workflow sans /admin/setup, 3 sections, panneau unique
- 23_LISTE-TICKETS: numéros de section = numéros Gitea, tickets #14/#15 alignés
- 24_DECISIONS-PROJET: config initiale = panneau + navigation bloquée
- BRIEFING-FRONTEND: tickets #12/#13 remplacés par panneau Paramètres
- Suppression login_screen.dart.bak

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 00:19:35 +01:00

668 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🔧 Documentation Technique - Configuration Système On-Premise
**Version** : 1.1
**Date** : 9 Février 2026
**Auteur** : Équipe PtitsPas
**Référence** : Architecture On-Premise
---
## 📖 Table des matières
1. [Vue d'ensemble](#vue-densemble)
2. [Architecture de configuration](#architecture-de-configuration)
3. [Table configuration](#table-configuration)
4. [Service Configuration](#service-configuration)
5. [Workflow Setup Initial](#workflow-setup-initial)
6. [APIs Configuration](#apis-configuration)
7. [Interface Admin](#interface-admin)
8. [Exemples de configuration](#exemples-de-configuration)
---
## 🎯 Vue d'ensemble
### Problématique
L'application P'titsPas est déployée **on-premise** chez différentes collectivités. Chaque collectivité a :
- Son propre serveur SMTP
- Ses propres ports et configurations réseau
- Son propre nom de domaine
- Sa propre charte graphique
**Solution** : Configuration dynamique stockée en base de données, modifiable via interface web.
### Principes
1.**Pas de hardcoding** : Aucune valeur en dur dans le code
2.**Pas de redéploiement** : Modification sans rebuild Docker
3.**Sécurité** : Mots de passe chiffrés en AES-256
4.**Traçabilité** : Qui a modifié quoi et quand
5.**Setup wizard** : Configuration guidée à la première connexion
6.**Validation** : Test SMTP avant sauvegarde
---
## 🏗️ Architecture de configuration
### Flux de données
```
┌─────────────────────────────────────────────────────────┐
│ Application │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Frontend │─────▶│ Backend │ │
│ │ (Admin) │ │ ConfigAPI │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ ConfigService│ │
│ │ (Cache) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ PostgreSQL │ │
│ │ configuration│ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
```
### Composants
1. **Table `configuration`** : Stockage clé/valeur en BDD
2. **ConfigService** : Cache en mémoire + chiffrement
3. **ConfigAPI** : Endpoints REST pour CRUD
4. **Guard Setup** : Redirection forcée si config incomplète
5. **Interface Admin** : Panneau Paramètres (3 sections) dans le dashboard, première config + accès permanent
---
## 📊 Table configuration
### Schéma SQL
```sql
-- Table de configuration système (clé/valeur)
CREATE TABLE configuration (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cle VARCHAR(100) UNIQUE NOT NULL, -- Clé unique (ex: 'smtp_host')
valeur TEXT, -- Valeur (peut être NULL)
type VARCHAR(50) NOT NULL, -- Type: 'string', 'number', 'boolean', 'json', 'encrypted'
categorie VARCHAR(50), -- Catégorie: 'email', 'app', 'security'
description TEXT, -- Description pour l'interface admin
modifie_le TIMESTAMPTZ DEFAULT now(), -- Date dernière modification
modifie_par UUID REFERENCES utilisateurs(id) -- Qui a modifié (traçabilité)
);
-- Index pour performance
CREATE INDEX idx_configuration_cle ON configuration(cle);
CREATE INDEX idx_configuration_categorie ON configuration(categorie);
```
### Seed initial
```sql
INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
-- === Configuration Email (SMTP) ===
('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'),
('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'),
('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'),
('smtp_auth_required', 'false', 'boolean', 'email', 'Authentification SMTP requise'),
('smtp_user', '', 'string', 'email', 'Utilisateur SMTP (si authentification requise)'),
('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'),
('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'),
('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'),
-- === Configuration Application ===
('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'),
('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'),
('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'),
('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'),
-- === Configuration Sécurité ===
('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'),
('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'),
('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'),
('bcrypt_rounds', '12', 'number', 'security', 'Nombre de rounds bcrypt pour le hachage des mots de passe');
```
### Types de données
| Type | Description | Exemple |
|------|-------------|---------|
| `string` | Chaîne de caractères | `"mail.example.com"` |
| `number` | Nombre entier ou décimal | `587` |
| `boolean` | Booléen | `true` / `false` |
| `json` | Objet JSON | `{"key": "value"}` |
| `encrypted` | Chaîne chiffrée AES-256 | `"a3f8b2..."` (hash) |
---
## 🔧 Service Configuration
### Responsabilités
1. **Cache en mémoire** : Chargement au démarrage
2. **Lecture** : `get(key, defaultValue)`
3. **Écriture** : `set(key, value, userId)`
4. **Chiffrement** : AES-256 pour type `encrypted`
5. **Conversion de types** : string → number/boolean/json
6. **Test SMTP** : Validation connexion
### Implémentation (TypeScript)
```typescript
// backend/src/config/config.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Configuration } from './entities/configuration.entity';
import * as crypto from 'crypto';
@Injectable()
export class ConfigService {
private cache: Map<string, any> = new Map();
private readonly ENCRYPTION_KEY = process.env.CONFIG_ENCRYPTION_KEY;
constructor(
@InjectRepository(Configuration)
private configRepo: Repository<Configuration>,
) {
this.loadCache();
}
// Chargement du cache au démarrage
async loadCache() {
const configs = await this.configRepo.find();
configs.forEach(config => {
let value = config.valeur;
// Déchiffrement si nécessaire
if (config.type === 'encrypted' && value) {
value = this.decrypt(value);
}
// Conversion de type
value = this.convertType(value, config.type);
this.cache.set(config.cle, value);
});
}
// Récupération d'une valeur
get(key: string, defaultValue?: any): any {
return this.cache.has(key) ? this.cache.get(key) : defaultValue;
}
// Mise à jour d'une valeur
async set(key: string, value: any, userId: string): Promise<void> {
const config = await this.configRepo.findOne({ where: { cle: key } });
if (!config) {
throw new Error(`Configuration key '${key}' not found`);
}
let valueToStore = String(value);
// Chiffrement si nécessaire
if (config.type === 'encrypted') {
valueToStore = this.encrypt(valueToStore);
}
config.valeur = valueToStore;
config.modifie_par = userId;
config.modifie_le = new Date();
await this.configRepo.save(config);
// Mise à jour du cache
this.cache.set(key, value);
}
// Récupération de toutes les configs par catégorie
async getByCategory(category: string): Promise<any> {
const configs = await this.configRepo.find({ where: { categorie: category } });
return configs.reduce((acc, config) => {
let value = config.valeur;
if (config.type === 'encrypted') {
value = '***********'; // Masquer les mots de passe
} else {
value = this.convertType(value, config.type);
}
acc[config.cle] = {
value,
description: config.description,
type: config.type,
};
return acc;
}, {});
}
// Test de connexion SMTP
async testSmtpConnection(): Promise<boolean> {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: this.get('smtp_host'),
port: this.get('smtp_port'),
secure: this.get('smtp_secure'),
auth: this.get('smtp_auth_required') ? {
user: this.get('smtp_user'),
pass: this.get('smtp_password'),
} : undefined,
});
try {
await transporter.verify();
return true;
} catch (error) {
console.error('SMTP test failed:', error);
return false;
}
}
// Utilitaires
private convertType(value: string, type: string): any {
switch (type) {
case 'number': return Number(value);
case 'boolean': return value === 'true';
case 'json': return JSON.parse(value);
default: return value;
}
}
private encrypt(text: string): string {
const cipher = crypto.createCipher('aes-256-cbc', this.ENCRYPTION_KEY);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
private decrypt(text: string): string {
const decipher = crypto.createDecipher('aes-256-cbc', this.ENCRYPTION_KEY);
let decrypted = decipher.update(text, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
```
---
## 🔄 Workflow Setup Initial
### Diagramme de séquence
```mermaid
sequenceDiagram
participant Op as Opérateur
participant App as Application (Dashboard)
participant API as ConfigAPI
participant DB as PostgreSQL
participant SMTP as Serveur SMTP
Op->>App: Première connexion (admin)
App->>API: GET /configuration/setup/status
API->>DB: setup_completed ?
DB-->>API: false
API-->>App: setupCompleted: false
App->>App: Affiche panneau Configuration<br/>et bloque les autres onglets
Op->>Op: Remplit les 3 sections<br/>(Email, Personnalisation, Avancé)
Op->>App: Clic "Tester la connexion SMTP"
App->>API: POST /api/v1/configuration/test-smtp
API->>SMTP: Test connexion
alt Test SMTP OK
SMTP-->>API: ✅ Connexion réussie
API->>Op: Envoi email de test
API-->>App: ✅ Test réussi
App-->>Op: Message: "Email de test envoyé"
else Test SMTP KO
SMTP-->>API: ❌ Erreur connexion
API-->>App: ❌ Erreur détaillée
App-->>Op: Message: "Erreur: vérifiez les paramètres"
end
Op->>App: Clic "Sauvegarder et terminer la configuration"
App->>API: PATCH /api/v1/configuration/bulk<br/>{smtp_host, smtp_port, ...}
API->>DB: UPDATE configuration SET valeur=...
API-->>App: OK
App->>API: POST /api/v1/configuration/setup/complete
API->>DB: SET setup_completed = true
API->>API: Recharger cache ConfigService
API-->>App: OK
App->>App: Débloque la navigation<br/>Message succès
Op->>App: Accès complet au dashboard
```
### Étapes détaillées
#### 1. Détection configuration incomplète
**Backend** : Le `SetupGuard` (NestJS) vérifie `setup_completed`. Si false, il autorise laccès au dashboard et aux APIs configuration (pas de redirection vers une page dédiée). Le **frontend** appelle `GET /configuration/setup/status` au chargement du dashboard admin ; si `setupCompleted === false`, il affiche directement le **panneau Paramètres** et désactive les autres onglets jusquà sauvegarde.
**Guard** (exemple) : exemption des routes login + dashboard + APIs configuration.
```typescript
// Exemptions : /auth/login, /api/v1/configuration, routes dashboard admin
// Si setup non complété : pas de redirection HTTP ; le frontend gère laffichage du panneau Config et le blocage des onglets.
```
#### 2. Panneau Paramètres (Frontend)
**Une seule page avec 3 sections** (blocs successifs, pas donglets dans le formulaire) :
##### Section 1 : Configuration Email 📧
| Champ | Type | Valeur par défaut | Obligatoire |
|-------|------|-------------------|-------------|
| Serveur SMTP | Text | `localhost` | ✅ |
| Port SMTP | Number | `25` | ✅ |
| Sécurité | Select | `Aucune` / `STARTTLS` / `SSL/TLS` | ✅ |
| Authentification requise | Checkbox | `false` | - |
| Utilisateur SMTP | Text | - | Si auth |
| Mot de passe SMTP | Password | - | Si auth |
| Nom expéditeur | Text | `P'titsPas` | ✅ |
| Email expéditeur | Email | `no-reply@ptits-pas.fr` | ✅ |
**Bouton** : "🧪 Tester la connexion SMTP"
##### Section 2 : Personnalisation 🎨
| Champ | Type | Valeur par défaut | Obligatoire |
|-------|------|-------------------|-------------|
| Nom de l'application | Text | `P'titsPas` | ✅ |
| URL de l'application | URL | `https://app.ptits-pas.fr` | ✅ |
| Logo | File (PNG/JPG) | Logo par défaut | ❌ |
##### Section 3 : Paramètres avancés ⚙️
| Champ | Type | Valeur par défaut | Obligatoire |
|-------|------|-------------------|-------------|
| Durée validité token MDP (jours) | Number | `7` | ✅ |
| Durée session JWT (heures) | Number | `24` | ✅ |
| Taille max upload (MB) | Number | `5` | ✅ |
**Bouton** : "💾 Sauvegarder et terminer la configuration" (première config) ou "💾 Enregistrer" (accès permanent).
---
## 🔌 APIs Configuration
### Endpoint 1 : Récupérer config par catégorie
```http
GET /api/v1/configuration/:category
Authorization: Bearer <super_admin_token>
```
**Paramètres** :
- `category` : `email` | `app` | `security`
**Réponse 200** :
```json
{
"smtp_host": {
"value": "localhost",
"description": "Serveur SMTP",
"type": "string"
},
"smtp_port": {
"value": 25,
"description": "Port SMTP",
"type": "number"
},
"smtp_password": {
"value": "***********",
"description": "Mot de passe SMTP",
"type": "encrypted"
}
}
```
---
### Endpoint 2 : Mise à jour multiple
```http
PATCH /api/v1/configuration/bulk
Authorization: Bearer <super_admin_token>
Content-Type: application/json
```
**Body** :
```json
{
"smtp_host": "mail.mairie-bezons.fr",
"smtp_port": 587,
"smtp_secure": false,
"smtp_auth_required": true,
"smtp_user": "noreply@mairie-bezons.fr",
"smtp_password": "SecretPassword123",
"email_from_name": "P'titsPas - Mairie de Bezons",
"email_from_address": "noreply@mairie-bezons.fr",
"app_name": "P'titsPas Bezons",
"app_url": "https://ptitspas.mairie-bezons.fr"
}
```
**Réponse 200** :
```json
{
"message": "Configuration mise à jour avec succès",
"updated": 10
}
```
---
### Endpoint 3 : Test connexion SMTP
```http
POST /api/v1/configuration/test-smtp
Authorization: Bearer <super_admin_token>
Content-Type: application/json
```
**Body** :
```json
{
"smtp_host": "mail.mairie-bezons.fr",
"smtp_port": 587,
"smtp_secure": false,
"smtp_auth_required": true,
"smtp_user": "noreply@mairie-bezons.fr",
"smtp_password": "SecretPassword123",
"test_email": "admin@mairie-bezons.fr"
}
```
**Réponse 200** :
```json
{
"success": true,
"message": "Connexion SMTP réussie. Email de test envoyé à admin@mairie-bezons.fr"
}
```
**Réponse 400** :
```json
{
"success": false,
"message": "Erreur de connexion SMTP",
"error": "ECONNREFUSED: Connection refused"
}
```
---
## 💻 Interface Admin
### Panneau Paramètres / Configuration (unique)
Un **seul panneau** dans le dashboard admin, avec **3 sections** affichées sur une même page (défilement si besoin). Pas donglets dans le formulaire.
- **Première configuration** (au déploiement, `setup_completed === false`) : lopérateur arrive sur le dashboard ; le panneau Configuration est affiché par défaut et les **autres onglets sont bloqués** jusquà clic sur « Sauvegarder et terminer la configuration » (PATCH bulk + POST setup/complete).
- **Accès permanent** : même panneau accessible via longlet « Configuration » / « Paramètres » du dashboard ; pas de blocage, simple modification et enregistrement (PATCH bulk).
```
┌─────────────────────────────────────────────────────────┐
│ Dashboard Admin [ Gestionnaires ] [ Parents ] ... │
│ [ Configuration ] ← onglet actif │
├─────────────────────────────────────────────────────────┤
│ │
│ 📧 Configuration Email (SMTP) │
│ Serveur SMTP * [_________________________________] │
│ Port * [____] Sécurité [▼] ☐ Auth requise │
│ Utilisateur [__________] Mot de passe [__________] │
│ Nom expéditeur * [__________] Email * [__________] │
│ [ 🧪 Tester la connexion SMTP ] │
│ │
│ ───────────────────────────────────────────────── │
│ 🎨 Personnalisation │
│ Nom application * [__________] URL * [__________] │
│ Logo [ Choisir un fichier ] │
│ ───────────────────────────────────────────────── │
│ ⚙️ Paramètres avancés │
│ Durée token MDP (jours) [__] JWT (h) [__] Upload MB [__] │
│ │
│ [ 💾 Sauvegarder et terminer la configuration ] │
│ (ou « Enregistrer » si config déjà complétée) │
└─────────────────────────────────────────────────────────┘
```
---
## 📋 Exemples de configuration
### Configuration 1 : Mairie (serveur local)
```json
{
"smtp_host": "mail.mairie-bezons.fr",
"smtp_port": 25,
"smtp_secure": false,
"smtp_auth_required": false,
"email_from_name": "P'titsPas - Mairie de Bezons",
"email_from_address": "noreply@mairie-bezons.fr",
"app_name": "P'titsPas Bezons",
"app_url": "https://ptitspas.mairie-bezons.fr"
}
```
### Configuration 2 : Gmail (pour tests)
```json
{
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_secure": false,
"smtp_auth_required": true,
"smtp_user": "contact@ptits-pas.fr",
"smtp_password": "abcd efgh ijkl mnop",
"email_from_name": "P'titsPas",
"email_from_address": "contact@ptits-pas.fr",
"app_name": "P'titsPas",
"app_url": "https://app.ptits-pas.fr"
}
```
**Note** : Pour Gmail, utiliser un "Mot de passe d'application" (App Password)
### Configuration 3 : Office 365
```json
{
"smtp_host": "smtp.office365.com",
"smtp_port": 587,
"smtp_secure": false,
"smtp_auth_required": true,
"smtp_user": "noreply@collectivite.fr",
"smtp_password": "MotDePasseSecurise123",
"email_from_name": "P'titsPas - Collectivité",
"email_from_address": "noreply@collectivite.fr",
"app_name": "P'titsPas",
"app_url": "https://ptitspas.collectivite.fr"
}
```
---
## 🔒 Sécurité
### Chiffrement des mots de passe
**Algorithme** : AES-256-CBC
**Clé** : Variable d'environnement `CONFIG_ENCRYPTION_KEY` (32 caractères)
**Génération de la clé** :
```bash
# Linux/Mac
openssl rand -hex 32
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```
**Fichier `.env`** :
```env
CONFIG_ENCRYPTION_KEY=a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2
```
### Variables d'environnement critiques
**Fichier `.env`** (à créer lors de l'installation) :
```env
# Base de données
DATABASE_URL=postgresql://app_user:password@ptitspas-postgres:5432/ptitpas_db
# JWT
JWT_SECRET=VotreSecretJWTTresLongEtAleatoire123456789
JWT_EXPIRY=24h
# Configuration
CONFIG_ENCRYPTION_KEY=VotreCleDeChiffrementAES256TresLongue32Caracteres
# Application
NODE_ENV=production
PORT=3000
```
---
## 📚 Références
### Documentation interne
- [01_CAHIER-DES-CHARGES.md](./01_CAHIER-DES-CHARGES.md)
- [02_ARCHITECTURE.md](./02_ARCHITECTURE.md)
- [03_DEPLOYMENT.md](./03_DEPLOYMENT.md)
- [10_DATABASE.md](./10_DATABASE.md)
- [11_API.md](./11_API.md)
### Documentation externe
- [NestJS Configuration](https://docs.nestjs.com/techniques/configuration)
- [Nodemailer SMTP](https://nodemailer.com/smtp/)
- [Node.js Crypto](https://nodejs.org/api/crypto.html)
---
**Dernière mise à jour** : 9 Février 2026
**Version** : 1.1
**Statut** : ✅ Document validé