petitspas/docs/21_CONFIGURATION-SYSTEME.md

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

  1. Vue d'ensemble
  2. Architecture de configuration
  3. Table configuration
  4. Service Configuration
  5. Workflow Setup Initial
  6. APIs Configuration
  7. Interface Admin
  8. 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

-- 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

  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)

// 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 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

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é