# 🔧 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 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** : Formulaire de configuration --- ## 📊 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 = new Map(); private readonly ENCRYPTION_KEY = process.env.CONFIG_ENCRYPTION_KEY; constructor( @InjectRepository(Configuration) private configRepo: Repository, ) { 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 { 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 { 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 { 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 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
WHERE cle='setup_completed' DB-->>Guard: 'false' Guard-->>App: Redirection forcée vers
/admin/setup SA->>SA: Remplit formulaire config
(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
{smtp_host, smtp_port, ...} API->>DB: BEGIN TRANSACTION API->>DB: UPDATE configuration SET valeur=...
FOR EACH key API->>DB: UPDATE configuration
SET valeur='true'
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) ```typescript @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 | Email | `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 ```http GET /api/v1/configuration/:category Authorization: Bearer ``` **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 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 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 ### É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) ```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** : 25 Novembre 2025 **Version** : 1.0 **Statut** : ✅ Document validé