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

22 KiB
Raw Blame History

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

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

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

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

{
  "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é