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
This commit is contained in:
pironantoine
2026-04-03 17:09:34 +00:00
parent c0322d5c8e
commit 432509b2d3
2 changed files with 831 additions and 0 deletions
+335
View File
@@ -0,0 +1,335 @@
# 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 :
```nginx
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 :**
```sql
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