23 KiB
🔧 Documentation Technique - Configuration Système On-Premise
Version : 1.0
Date : 25 Novembre 2025
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 : Formulaire de configuration
📊 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 SA as Super Admin
participant App as Application
participant Guard as SetupGuard
participant API as ConfigAPI
participant DB as PostgreSQL
participant SMTP as Serveur SMTP
SA->>App: Première connexion
App->>Guard: Vérifier setup_completed
Guard->>DB: SELECT valeur FROM configuration<br/>WHERE cle='setup_completed'
DB-->>Guard: 'false'
Guard-->>App: Redirection forcée vers<br/>/admin/setup
SA->>SA: Remplit formulaire config<br/>(SMTP, app, sécurité)
SA->>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->>SA: Envoi email de test
API-->>App: ✅ Test réussi
App-->>SA: Message: "Email de test envoyé"
else Test SMTP KO
SMTP-->>API: ❌ Erreur connexion
API-->>App: ❌ Erreur détaillée
App-->>SA: Message: "Erreur: vérifiez les paramètres"
end
SA->>App: Clic "Sauvegarder"
App->>API: PATCH /api/v1/configuration/bulk<br/>{smtp_host, smtp_port, ...}
API->>DB: BEGIN TRANSACTION
API->>DB: UPDATE configuration SET valeur=...<br/>FOR EACH key
API->>DB: UPDATE configuration<br/>SET valeur='true'<br/>WHERE cle='setup_completed'
API->>DB: COMMIT
API->>API: Recharger cache ConfigService
API-->>App: ✅ Configuration sauvegardée
App-->>SA: Redirection vers /admin/dashboard
SA->>App: Accès complet à l'application
Étapes détaillées
1. Détection configuration incomplète
Guard : SetupGuard (NestJS)
@Injectable()
export class SetupGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const setupCompleted = this.configService.get('setup_completed', false);
// Exemptions
const exemptedRoutes = ['/auth/login', '/admin/setup', '/api/v1/configuration'];
if (exemptedRoutes.some(route => request.url.includes(route))) {
return true;
}
// Si setup non complété, bloquer
if (!setupCompleted) {
throw new HttpException(
'Configuration initiale requise',
HttpStatus.TEMPORARY_REDIRECT,
{ location: '/admin/setup' }
);
}
return true;
}
}
2. Formulaire Setup (Frontend)
3 onglets :
Onglet 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"
Onglet 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 | ❌ |
Onglet 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"
🔌 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
Écran Setup Initial
┌─────────────────────────────────────────────────────────┐
│ 🚀 Configuration Initiale - P'titsPas │
├─────────────────────────────────────────────────────────┤
│ │
│ Bienvenue ! Configurez votre installation P'titsPas │
│ │
│ [ 📧 Email ] [ 🎨 Personnalisation ] [ ⚙️ Avancé ] │
├─────────────────────────────────────────────────────────┤
│ │
│ 📧 Configuration Email (SMTP) │
│ │
│ Serveur SMTP * │
│ [_____________________________________________] │
│ Ex: mail.mairie-bezons.fr, smtp.gmail.com │
│ │
│ Port SMTP * │
│ [_____] 25 (standard), 465 (SSL), 587 (STARTTLS) │
│ │
│ Sécurité * │
│ [ ▼ Aucune ] STARTTLS SSL/TLS │
│ │
│ ☐ Authentification requise │
│ │
│ Utilisateur SMTP │
│ [_____________________________________________] │
│ │
│ Mot de passe SMTP │
│ [_____________________________________________] │
│ │
│ Nom de l'expéditeur * │
│ [_____________________________________________] │
│ Ex: P'titsPas - Mairie de Bezons │
│ │
│ Email expéditeur * │
│ [_____________________________________________] │
│ Ex: noreply@mairie-bezons.fr │
│ │
│ [ 🧪 Tester la connexion SMTP ] │
│ │
│ ───────────────────────────────────────────────── │
│ │
│ [ ← Précédent ] [ Suivant → ] │
│ │
└─────────────────────────────────────────────────────────┘
Écran Paramètres (accès permanent)
Identique au Setup Initial, mais accessible depuis le menu admin :
- Menu Admin → Paramètres → Configuration Système
📋 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 : 25 Novembre 2025
Version : 1.0
Statut : ✅ Document validé