- 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>
22 KiB
🔧 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
- Vue d'ensemble
- Architecture de configuration
- Table configuration
- Service Configuration
- Workflow Setup Initial
- APIs Configuration
- Interface Admin
- 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
- ✅ Pas de hardcoding : Aucune valeur en dur dans le code
- ✅ Pas de redéploiement : Modification sans rebuild Docker
- ✅ Sécurité : Mots de passe chiffrés en AES-256
- ✅ Traçabilité : Qui a modifié quoi et quand
- ✅ Setup wizard : Configuration guidée à la première connexion
- ✅ Validation : Test SMTP avant sauvegarde
🏗️ Architecture de configuration
Flux de données
┌─────────────────────────────────────────────────────────┐
│ Application │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Frontend │─────▶│ Backend │ │
│ │ (Admin) │ │ ConfigAPI │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ ConfigService│ │
│ │ (Cache) │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ PostgreSQL │ │
│ │ configuration│ │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Composants
- Table
configuration: Stockage clé/valeur en BDD - ConfigService : Cache en mémoire + chiffrement
- ConfigAPI : Endpoints REST pour CRUD
- Guard Setup : Redirection forcée si config incomplète
- Interface Admin : Panneau Paramètres (3 sections) dans le dashboard, première config + accès permanent
📊 Table configuration
Schéma 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
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
- Cache en mémoire : Chargement au démarrage
- Lecture :
get(key, defaultValue) - Écriture :
set(key, value, userId) - Chiffrement : AES-256 pour type
encrypted - Conversion de types : string → number/boolean/json
- Test SMTP : Validation connexion
Implémentation (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
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 l’accè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.
// Exemptions : /auth/login, /api/v1/configuration, routes dashboard admin
// Si setup non complété : pas de redirection HTTP ; le frontend gère l’affichage du panneau Config et le blocage des onglets.
2. Panneau Paramètres (Frontend)
Une seule page avec 3 sections (blocs successifs, pas d’onglets 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 | 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
GET /api/v1/configuration/:category
Authorization: Bearer <super_admin_token>
Paramètres :
category:email|app|security
Réponse 200 :
{
"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
PATCH /api/v1/configuration/bulk
Authorization: Bearer <super_admin_token>
Content-Type: application/json
Body :
{
"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 :
{
"message": "Configuration mise à jour avec succès",
"updated": 10
}
Endpoint 3 : Test connexion SMTP
POST /api/v1/configuration/test-smtp
Authorization: Bearer <super_admin_token>
Content-Type: application/json
Body :
{
"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 :
{
"success": true,
"message": "Connexion SMTP réussie. Email de test envoyé à admin@mairie-bezons.fr"
}
Réponse 400 :
{
"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 d’onglets dans le formulaire.
- Première configuration (au déploiement,
setup_completed === false) : l’opé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 l’onglet « 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)
{
"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)
{
"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
{
"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é :
# Linux/Mac
openssl rand -hex 32
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Fichier .env :
CONFIG_ENCRYPTION_KEY=a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2
Variables d'environnement critiques
Fichier .env (à créer lors de l'installation) :
# 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
Documentation externe
Dernière mise à jour : 9 Février 2026
Version : 1.1
Statut : ✅ Document validé