Files
pironantoine 432509b2d3 Add technical and operational guides for the platform
Add ARCHITECTURE.md and EXPLOITATION.md files detailing the system's technical design and operational procedures.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9fd0e274-629e-4dae-8960-adf4c556797f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/VnHW0bR
Replit-Helium-Checkpoint-Created: true
2026-04-03 17:09:34 +00:00

13 KiB
Raw Permalink Blame History

Architecture Technique — La Voix du Peuple

Version : 1.0
Date : Avril 2026
Statut : Production


1. Vue d'ensemble

La Voix du Peuple est une plateforme démocratique citoyenne permettant la soumission d'idées politiques, leur filtrage automatique par un agent IA ancré dans le droit international des droits humains, et leur synthèse en un texte collectif vivant.


2. Architecture générale

                         RÉSEAU EXTERNE (Internet)
                                   │
                                   ▼
                        ┌─────────────────────┐
                        │       HAProxy        │
                        │  (réseau séparé)     │
                        │  - Terminaison TLS   │
                        │  - Load balancing    │
                        │  - ACL / filtrage IP │
                        └──────────┬──────────┘
                                   │  HTTP (port 80, réseau interne)
                                   ▼
                        ┌─────────────────────┐
                        │        Nginx         │
                        │  (reverse proxy)     │
                        │  - Routage /api/*    │
                        │  - Serve SPA React   │
                        │  - Cache statiques   │
                        └────────┬────────────┘
                                 │
               ┌─────────────────┴──────────────────┐
               │                                     │
               ▼                                     ▼
   ┌───────────────────────┐           ┌───────────────────────┐
   │   Gunicorn + Flask    │           │   Fichiers statiques   │
   │   (127.0.0.1:8000)   │           │   React SPA (dist/)    │
   │   - API REST /api/*   │           │   (servis par Nginx)   │
   │   - Filtrage IA       │           └───────────────────────┘
   │   - Rate limiting     │
   └──────────┬────────────┘
              │
    ┌─────────┴──────────┐
    │                    │
    ▼                    ▼
┌──────────┐     ┌───────────────┐
│PostgreSQL│     │  OpenAI API   │
│(local)   │     │  (externe)    │
│- ideas   │     │  gpt-4o-mini  │
│- synthesis│    │  gpt-4o       │
└──────────┘     └───────────────┘

3. Composants

3.1 HAProxy (réseau séparé)

Propriété Valeur
Rôle Point d'entrée unique, terminaison TLS, load balancing
Réseau DMZ / réseau séparé dédié
Protocoles entrants HTTPS/443, HTTP/80 (redirect)
Protocoles sortants HTTP/80 vers Nginx (réseau interne)
TLS Terminaison SSL/TLS sur HAProxy, communication interne en HTTP clair

Configuration recommandée dans HAProxy :

frontend voix_du_peuple_https
    bind *:443 ssl crt /etc/ssl/certs/voix-du-peuple.pem
    mode http
    option forwardfor
    http-request set-header X-Forwarded-Proto https
    default_backend voix_du_peuple_backend

frontend voix_du_peuple_http
    bind *:80
    mode http
    redirect scheme https code 301 if !{ ssl_fc }

backend voix_du_peuple_backend
    mode http
    balance roundrobin
    option httpchk GET /api/healthz
    http-check expect status 200
    server app01 <IP_NGINX>:80 check inter 10s

3.2 Nginx

Propriété Valeur
Rôle Reverse proxy applicatif, service des fichiers statiques
Écoute 0.0.0.0:80 (réseau interne uniquement)
Vers Flask 127.0.0.1:8000 pour toute requête /api/*
Vers SPA dist/public/ pour tout le reste (try_files)

Responsabilités :

  • Routage : /api/* → Gunicorn, /* → SPA React
  • Cache : assets JS/CSS/images avec Cache-Control: immutable
  • Headers de sécurité : X-Frame-Options, X-Content-Type-Options, Referrer-Policy
  • Logging : access.log et error.log structurés

Note HAProxy : Nginx doit faire confiance à l'en-tête X-Forwarded-For positionné par HAProxy pour que Flask voie la vraie IP cliente (rate limiting par IP).

Configuration Nginx requise :

set_real_ip_from  <IP_HAPROXY>;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;

3.3 Gunicorn + Flask (Backend API)

Propriété Valeur
Serveur WSGI Gunicorn 23+
Workers 4 (synchrones, ajustable)
Bind 127.0.0.1:8000 (non exposé directement)
Framework Flask 3.1+
Python 3.11+

Routes exposées :

Méthode Route Description Rate limit
GET /api/healthz Santé du service Aucun
GET /api/ideas Liste des idées acceptées 120/min
POST /api/ideas Soumettre une idée 5/min, 20/h par IP
GET /api/ideas/stats Statistiques 120/min
GET /api/synthesis Texte synthétisé collectif 120/min

Sécurité applicative :

  • Rate limiting par IP réelle (via flask-limiter)
  • Assainissement XSS : bleach.clean() sur toutes les entrées
  • Requêtes SQL paramétrées (psycopg2) — pas de concaténation de chaînes
  • En-têtes HTTP de sécurité sur toutes les réponses
  • Aucun secret exposé dans les messages d'erreur

3.4 Agent IA

Deux agents distincts, chacun configuré avec son propre modèle :

Agent de filtrage

Propriété Valeur
Modèle par défaut gpt-4o-mini
Variable d'env OPENAI_FILTER_MODEL
Tokens max 300
Format de sortie JSON strict (response_format: json_object)
Déclenchement À chaque soumission d'idée
Temps de réponse ~3-6 secondes

Logique de décision :

  1. Appel OpenAI avec le prompt légal complet
  2. Parse JSON {"accepted": bool, "reason"?: str, "legal_basis"?: str}
  3. Si l'API retourne une erreur de filtre de contenu → rejet automatique avec citation DUDH/PIDCP/CEDH
  4. Résultat persisté en base avec l'idée

Agent de synthèse

Propriété Valeur
Modèle par défaut gpt-4o
Variable d'env OPENAI_SYNTHESIS_MODEL
Tokens max 1200
Déclenchement En arrière-plan après chaque acceptation
Temps de réponse ~8-15 secondes

La synthèse est non-bloquante : l'API répond immédiatement à l'utilisateur, la synthèse s'effectue dans un thread daemon.

Base légale du filtre

Le prompt de filtrage intègre textuellement les articles pertinents de :

  • Déclaration universelle des droits de l'homme (DUDH, ONU 1948)
  • Pacte international relatif aux droits civils et politiques (PIDCP, ONU 1966)
  • Convention européenne des droits de l'homme (CEDH, 1950)
  • Charte des droits fondamentaux de l'UE (2000/2009)
  • Convention pour la prévention du génocide (ONU 1948)
  • Statut de Rome / CPI (1998)
  • Convention sur la discrimination raciale (CERD, ONU 1965)

3.5 PostgreSQL

Propriété Valeur
Version 15+
Accès Localhost uniquement (127.0.0.1)
Connexion Flask Via DATABASE_URL (psycopg2-binary)
Authentification md5 / scram-sha-256

Schéma de la base de données :

CREATE TABLE ideas (
    id              SERIAL PRIMARY KEY,
    content         TEXT        NOT NULL,
    author          VARCHAR(100),
    accepted        BOOLEAN     NOT NULL DEFAULT FALSE,
    rejection_reason TEXT,
    legal_basis     TEXT,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE synthesis (
    id          SERIAL  PRIMARY KEY,
    text        TEXT    NOT NULL,
    idea_count  INTEGER NOT NULL DEFAULT 0,
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

3.6 Frontend React

Propriété Valeur
Framework React 18 + Vite 6
Styles Tailwind CSS v4 + shadcn/ui + Radix UI
Routing Wouter (léger, côté client)
Requêtes API TanStack React Query
Build Sortie statique dans dist/public/

Communication avec le backend :

  • Hooks générés depuis la spécification OpenAPI (lib/api-spec/openapi.yaml)
  • Codegen via Orval → lib/api-client-react/src/generated/
  • Toutes les requêtes passent par /api/ (même origine, pas de CORS cross-origin en production)
  • Rafraîchissement automatique de la synthèse toutes les 15 secondes

4. Flux de données

4.1 Soumission d'une idée

Citoyen
  │
  │  POST /api/ideas {"content": "...", "author": "..."}
  ▼
HAProxy ──[TLS]──► Nginx ──► Gunicorn/Flask
                               │
                               ├─ Validation entrées (longueur, XSS)
                               ├─ Rate limiting (5/min par IP)
                               │
                               ├─ Agent filtrage ──► OpenAI API
                               │                        │
                               │                   JSON {"accepted": bool, ...}
                               │
                               ├─ INSERT INTO ideas (...)
                               │
                               ├─ Si accepté :
                               │    Thread daemon ──► Agent synthèse ──► OpenAI
                               │                                             │
                               │                                        UPDATE synthesis
                               │
  ◄── HTTP 201 {"accepted": bool, "reason": "...", "idea": {...}} ──────────┘

4.2 Lecture de la synthèse

Citoyen (polling 15s)
  │
  │  GET /api/synthesis
  ▼
HAProxy ──► Nginx ──► Flask ──► SELECT FROM synthesis ──► {"text": "...", "ideaCount": N}

5. Sécurité

5.1 Couches de défense

Couche Mécanisme
Réseau HAProxy sur réseau séparé, Nginx non exposé directement
Transport TLS 1.2/1.3 terminé sur HAProxy
Applicative Flask Rate limiting, validation, assainissement XSS, headers sécurité
IA (double filtre) Filtre de contenu Azure/OpenAI + filtre légal interne
Base de données Requêtes paramétrées, accès localhost uniquement
Système Utilisateur dédié non-root, systemd sandboxing

5.2 Périmètre des ports réseau

Port Interface Exposé à Rôle
443 HAProxy Internet HTTPS public
80 HAProxy Internet Redirect HTTPS
80 Nginx Réseau interne HTTP depuis HAProxy
8000 Gunicorn 127.0.0.1 API Flask
5432 PostgreSQL 127.0.0.1 Base de données

5.3 En-têtes HTTP de sécurité (Flask)

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; script-src 'none'; object-src 'none'
Cache-Control: no-store

6. Décisions d'architecture

Décision Choix Justification
ORM vs SQL direct psycopg2 direct Transparence, simplicité, facilité d'audit
Sync vs Async Flask Synchrone (Gunicorn) Complexité réduite, suffisant pour la charge attendue
Synthèse bloquante vs thread Thread daemon Latence utilisateur réduite, synthèse non-critique au retour
TLS sur HAProxy vs Nginx HAProxy TLS centralisé sur le composant réseau dédié à cet usage
SPA vs SSR SPA statique Déploiement simple, aucune dépendance Node en production
Rate limiting en mémoire vs Redis Mémoire (memory://) Installation simple ; Redis optionnel pour cluster multi-instances

7. Scalabilité

L'architecture actuelle est dimensionnée pour une instance unique. En cas de montée en charge :

  • Gunicorn workers : augmenter via --workers (règle : 2 × CPU + 1)
  • Multi-instances : ajouter des backends dans HAProxy + basculer le rate limiting sur Redis (RATELIMIT_STORAGE_URL=redis://...)
  • Base de données : ajouter des index sur ideas.accepted et ideas.created_at
  • Cache synthèse : la synthèse est unique en base, naturellement partagée entre workers