Merge ARCHITECTURE.md into README, expand Légifrance section on transparence
- README: add sommaire, integrate full architecture content (diagram, components, routes, flows, security, decisions, scalability), add Légifrance articles table and PISTE setup instructions - ARCHITECTURE.md: removed (content fully merged into README) - transparence.tsx: expand Légifrance block with full table of 11 tracked articles (priority, description), cache/resilience/monitoring details Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,11 +11,24 @@ La Voix du Peuple est un projet personnel open-source. Si vous l'utilisez et vou
|
||||
|
||||
---
|
||||
|
||||
## Sommaire
|
||||
|
||||
1. [Fonctionnalités](#fonctionnalités)
|
||||
2. [Architecture](#architecture)
|
||||
3. [Stack technique](#stack-technique)
|
||||
4. [Installation et développement local](#installation-et-développement-local)
|
||||
5. [Déploiement en production](#déploiement-en-production)
|
||||
6. [Contribuer](#contribuer)
|
||||
7. [Licence](#licence)
|
||||
|
||||
---
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
**Contribution citoyenne**
|
||||
- Formulaire texte libre (10 à 1 000 caractères), pseudonyme optionnel
|
||||
- Filtrage IA automatique à la soumission selon quinze textes de référence : DUDH, PIDCP, CEDH, Code pénal français, Loi du 29 juillet 1881, LCEN, SREN 2024, RGPD, et autres
|
||||
- Injection dynamique des textes légaux consolidés depuis l'API officielle Légifrance (PISTE) — le modèle travaille sur la version du jour, pas sur sa mémoire d'entraînement
|
||||
- Motif de rejet et base légale retournés au contributeur si la contribution est refusée
|
||||
- Dialogue de recueil du consentement RGPD avant soumission
|
||||
|
||||
@@ -65,6 +78,462 @@ La Voix du Peuple est un projet personnel open-source. Si vous l'utilisez et vou
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Vue d'ensemble
|
||||
|
||||
```
|
||||
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│ │ Mistral AI API │
|
||||
│(local) │ │ (externe, UE) │
|
||||
│- ideas │ │ mistral-small │
|
||||
│- synth. │ │ mistral-large │
|
||||
│- consult.│ │ (ou OpenAI-compat│
|
||||
│- consents│ │ si configuré) │
|
||||
│- ip_abuse│ └───────────────────┘
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
### HAProxy
|
||||
|
||||
| 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 de référence :
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 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`) |
|
||||
|
||||
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) :
|
||||
|
||||
```nginx
|
||||
set_real_ip_from <IP_HAPROXY>;
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
```
|
||||
|
||||
### 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 | Auth |
|
||||
|---------|-------|-------------|------|
|
||||
| GET | `/api/healthz` | Santé du service | — |
|
||||
| GET | `/api/ideas` | Idées globales acceptées | — |
|
||||
| POST | `/api/ideas` | Soumettre une idée globale | — |
|
||||
| GET | `/api/ideas/stats` | Statistiques globales (admin) | — |
|
||||
| GET | `/api/synthesis` | Synthèse collective globale | — |
|
||||
| POST | `/api/consent` | Enregistrer le consentement RGPD | — |
|
||||
| GET | `/api/stats/public` | Statistiques publiques globales | — |
|
||||
| GET | `/api/contributions` | Contributions paginées (vue publique) | — |
|
||||
| GET | `/api/contributions/export/json` | Export JSON contributions globales | — |
|
||||
| GET | `/api/contributions/export/csv` | Export CSV contributions globales | — |
|
||||
| POST | `/api/ideas/:id/flag` | Signaler une contribution | — |
|
||||
| POST | `/api/check-law` | Vérifier si une proposition est couverte par la loi | — |
|
||||
| GET | `/api/consultations` | Liste des consultations | — |
|
||||
| GET | `/api/consultations/:slug` | Détails d'une consultation | — |
|
||||
| GET | `/api/consultations/:slug/synthesis` | Synthèse d'une consultation | — |
|
||||
| GET | `/api/consultations/:slug/contributions` | Contributions paginées d'une consultation | — |
|
||||
| POST | `/api/consultations/:slug/ideas` | Soumettre une idée dans une consultation | — |
|
||||
| GET | `/api/consultations/:slug/export/print` | Rendu HTML pour impression | — |
|
||||
| POST | `/api/admin/login` | Authentification admin | — |
|
||||
| GET | `/api/admin/stats` | Statistiques détaillées | Admin |
|
||||
| GET | `/api/admin/ideas` | Liste toutes les contributions (filtres, pagination) | Admin |
|
||||
| DELETE | `/api/admin/ideas/:id` | Supprimer une contribution | Admin |
|
||||
| POST | `/api/admin/ideas/bulk-delete` | Suppression en masse | Admin |
|
||||
| POST | `/api/admin/ideas/:id/override` | Modifier le statut d'une contribution | Admin |
|
||||
| POST | `/api/admin/ideas/:id/unflag` | Retirer un signalement | Admin |
|
||||
| POST | `/api/admin/synthesis/regenerate` | Régénérer la synthèse globale | Admin |
|
||||
| GET | `/api/admin/export/csv` | Export CSV complet (tous champs) | Admin |
|
||||
| GET | `/api/admin/consultations` | Liste toutes les consultations | Admin |
|
||||
| POST | `/api/admin/consultations` | Créer une consultation | Admin |
|
||||
| POST | `/api/admin/consultations/:slug/close` | Fermer une consultation | Admin |
|
||||
| DELETE | `/api/admin/consultations/:slug` | Supprimer une consultation | Admin |
|
||||
| GET | `/api/admin/ip-blacklist` | Liste des IPs blacklistées | Admin |
|
||||
| DELETE | `/api/admin/ip-blacklist/:hash` | Lever un blacklist IP | Admin |
|
||||
|
||||
**Sécurité applicative :**
|
||||
- Rate limiting par IP réelle ou fingerprint FingerprintJS (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
|
||||
|
||||
### Agent IA
|
||||
|
||||
Trois agents distincts, tous via le client OpenAI-compatible (Mistral AI par défaut).
|
||||
|
||||
#### Agent de filtrage
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| Modèle par défaut | `mistral-small-latest` |
|
||||
| Variable d'env | `FILTER_MODEL` |
|
||||
| Tokens max | 300 |
|
||||
| Format de sortie | JSON strict (`response_format: json_object`) |
|
||||
| Déclenchement | À chaque soumission d'idée |
|
||||
|
||||
Logique de décision :
|
||||
1. Récupération des textes légaux consolidés depuis Légifrance (PISTE) — cache 24 h, fallback statique si API indisponible
|
||||
2. Appel API avec le prompt légal complet (16 sources + textes injectés)
|
||||
3. Parse JSON `{"accepted": bool, "reason"?: str, "legal_basis"?: str}`
|
||||
4. Si l'API retourne une erreur de filtre de contenu → rejet automatique avec citation DUDH/PIDCP/CEDH
|
||||
5. Résultat persisté en base avec l'idée
|
||||
|
||||
#### Agent de synthèse
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| Modèle par défaut | `mistral-large-latest` |
|
||||
| Variable d'env | `SYNTHESIS_MODEL` |
|
||||
| Tokens max | 1200 |
|
||||
| Déclenchement | En arrière-plan après chaque acceptation, ou à la suppression |
|
||||
|
||||
La synthèse est non-bloquante : l'API répond immédiatement à l'utilisateur, la synthèse s'effectue dans un thread daemon.
|
||||
|
||||
#### Agent de vérification légale
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| Modèle | même que `FILTER_MODEL` |
|
||||
| Tokens max | 200 |
|
||||
| Déclenchement | À la demande via `POST /api/check-law` |
|
||||
| Rôle | Informer le contributeur si sa proposition est déjà couverte par une loi — non bloquant |
|
||||
|
||||
#### Base légale du filtre (16 sources)
|
||||
|
||||
| # | Texte |
|
||||
|---|-------|
|
||||
| 1 | Déclaration universelle des droits de l'homme (DUDH, ONU 1948) |
|
||||
| 2 | Pacte international relatif aux droits civils et politiques (PIDCP, ONU 1966) |
|
||||
| 3 | Convention européenne des droits de l'homme (CEDH, 1950) |
|
||||
| 4 | Charte des droits fondamentaux de l'UE (2000/2009) |
|
||||
| 5 | Convention pour la prévention du génocide (ONU 1948) |
|
||||
| 6 | Statut de Rome / CPI (1998) |
|
||||
| 7 | Convention sur la discrimination raciale (CERD, ONU 1965) |
|
||||
| 8-13 | Code pénal français — Livres I à VI |
|
||||
| 14 | Loi sur la liberté de la presse du 29 juillet 1881 |
|
||||
| 15 | LCEN (2004) · Loi SREN (2024) · RGPD · Code de la santé publique |
|
||||
| 16 | Principes constitutionnels (CC 94-343/344 DC) · Code civil Art. 9, 1240 |
|
||||
|
||||
#### Intégration Légifrance (PISTE)
|
||||
|
||||
Les textes des articles les plus sensibles sont récupérés à chaque filtrage depuis l'API officielle Légifrance (PISTE) et injectés dans le prompt avant les critères d'analyse. Le modèle travaille ainsi sur les textes consolidés du jour.
|
||||
|
||||
Articles surveillés (vérifiés et mis à jour via `scripts/check_legal_refs.py`) :
|
||||
|
||||
| Clé | Référence | Description | Priorité |
|
||||
|-----|-----------|-------------|----------|
|
||||
| CP-225-1 | Code pénal Art. 225-1 | Discrimination | Critique |
|
||||
| L1881-24 | Loi 1881 Art. 24 | Provocation à la haine | Critique |
|
||||
| CP-222-33 | Code pénal Art. 222-33 | Harcèlement sexuel | Haute |
|
||||
| CP-222-33-2-2 | Code pénal Art. 222-33-2-2 | Cyberharcèlement en meute | Haute |
|
||||
| CP-421-2-5 | Code pénal Art. 421-2-5 | Apologie du terrorisme | Haute |
|
||||
| CP-211-1 | Code pénal Art. 211-1 | Génocide | Haute |
|
||||
| L1881-24bis | Loi 1881 Art. 24 bis | Négationnisme | Haute |
|
||||
| CP-226-4-1 | Code pénal Art. 226-4-1 | Usurpation d'identité numérique | Haute |
|
||||
| CP-212-1 | Code pénal Art. 212-1 | Crimes contre l'humanité | Moyenne |
|
||||
| CP-223-13 | Code pénal Art. 223-13 | Provocation au suicide | Moyenne |
|
||||
| L1881-29 | Loi 1881 Art. 29 | Diffamation | Moyenne |
|
||||
|
||||
Les articles de priorité **critique** et **haute** sont injectés dans le prompt. Les articles de priorité **moyenne** sont suivis pour détecter les changements législatifs mais ne sont pas injectés (le cadre légal statique les couvre déjà).
|
||||
|
||||
Le script `scripts/check_legal_refs.py --check` compare les textes courants avec la baseline stockée dans `artifacts/flask-api/legal_refs.yaml`. Exit code 2 si des modifications sont détectées — intégrable dans un cron ou une pipeline CI.
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| Version | 15+ |
|
||||
| Accès | Localhost uniquement (127.0.0.1) |
|
||||
| Connexion Flask | Via `DATABASE_URL` (psycopg2-binary) |
|
||||
| Authentification | scram-sha-256 |
|
||||
| Migrations | Incrémentales et idempotentes — appliquées à chaque démarrage |
|
||||
|
||||
**Schéma de la base de données :**
|
||||
|
||||
```sql
|
||||
CREATE TABLE consultations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
intro_message TEXT,
|
||||
organizer_name VARCHAR(200),
|
||||
organizer_logo_url TEXT,
|
||||
starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ends_at TIMESTAMPTZ,
|
||||
closed_at TIMESTAMPTZ,
|
||||
webhook_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
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,
|
||||
flagged BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
flag_count INTEGER NOT NULL DEFAULT 0,
|
||||
admin_note TEXT,
|
||||
fingerprint_hash VARCHAR(64),
|
||||
consultation_id INTEGER REFERENCES consultations(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
-- consultation_id IS NULL → contribution globale (page d'accueil)
|
||||
);
|
||||
|
||||
CREATE TABLE synthesis (
|
||||
id SERIAL PRIMARY KEY,
|
||||
text TEXT NOT NULL,
|
||||
idea_count INTEGER NOT NULL DEFAULT 0,
|
||||
consultation_id INTEGER REFERENCES consultations(id) ON DELETE CASCADE,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
-- consultation_id IS NULL → synthèse globale
|
||||
);
|
||||
|
||||
CREATE TABLE consents (
|
||||
id SERIAL PRIMARY KEY,
|
||||
fingerprint_hash VARCHAR(64) NOT NULL,
|
||||
consent_version VARCHAR(20) NOT NULL,
|
||||
consented_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE ip_abuse (
|
||||
ip_hash TEXT PRIMARY KEY, -- SHA-256 de l'IP (RGPD)
|
||||
rejection_count INTEGER NOT NULL DEFAULT 1,
|
||||
first_rejected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_rejected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_idea_id INTEGER,
|
||||
blacklisted_at TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ
|
||||
-- blacklist déclenché à rejection_count >= 2
|
||||
);
|
||||
```
|
||||
|
||||
### Frontend React
|
||||
|
||||
| Propriété | Valeur |
|
||||
|-----------|--------|
|
||||
| Framework | React 18 + Vite 7 |
|
||||
| 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 : toutes les requêtes passent par `/api/` (même origine). La synthèse est rafraîchie automatiquement toutes les 15 secondes.
|
||||
|
||||
**Pages publiques :**
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `/` | Formulaire de soumission + synthèse |
|
||||
| `/about` | Présentation du projet |
|
||||
| `/transparence` | Fonctionnement, critères IA, prompts verbatim |
|
||||
| `/contributions-brutes` | Fil paginé des contributions acceptées |
|
||||
| `/consultations` | Index des consultations |
|
||||
| `/consultation/:slug` | Page d'une consultation ciblée |
|
||||
| `/flyer` | Flyer QR code imprimable |
|
||||
| `/mentions-legales` | Mentions légales (LCEN) |
|
||||
| `/politique-confidentialite` | Politique de confidentialité RGPD |
|
||||
| `/admin` | Panel d'administration (protégé) |
|
||||
|
||||
### Flux de données
|
||||
|
||||
#### Soumission d'une idée
|
||||
|
||||
```
|
||||
Citoyen
|
||||
│
|
||||
│ POST /api/ideas {"content": "...", "author": "..."}
|
||||
▼
|
||||
HAProxy ──[TLS]──► Nginx ──► Gunicorn/Flask
|
||||
│
|
||||
├─ Vérification blacklist IP
|
||||
├─ Honeypot / hCaptcha
|
||||
├─ Consentement RGPD (cookie _ct)
|
||||
├─ Cooldown session (cookie _cv)
|
||||
├─ Validation entrées (longueur, XSS)
|
||||
├─ Rate limiting (fingerprint ou IP)
|
||||
├─ Flood detection
|
||||
│
|
||||
├─ Fetch textes Légifrance (cache 24h)
|
||||
├─ Agent filtrage ──► Mistral AI API
|
||||
│ │
|
||||
│ JSON {"accepted": bool, ...}
|
||||
│
|
||||
├─ INSERT INTO ideas (...)
|
||||
│
|
||||
├─ Si refusé :
|
||||
│ record_ip_rejection() → blacklist si >= 2 rejets
|
||||
│
|
||||
├─ Si accepté :
|
||||
│ Thread daemon ──► Agent synthèse ──► Mistral AI
|
||||
│ │
|
||||
│ UPSERT synthesis
|
||||
│
|
||||
◄── HTTP 201 {"accepted": bool, "reason": "...", "idea": {...}} ──────────┘
|
||||
```
|
||||
|
||||
#### Lecture de la synthèse
|
||||
|
||||
```
|
||||
Citoyen (polling 15s)
|
||||
│
|
||||
│ GET /api/synthesis
|
||||
▼
|
||||
HAProxy ──► Nginx ──► Flask ──► SELECT FROM synthesis ──► {"text": "...", "ideaCount": N}
|
||||
```
|
||||
|
||||
#### Fermeture automatique des consultations
|
||||
|
||||
```
|
||||
Thread daemon (toutes les 60s, par worker)
|
||||
│
|
||||
├─ pg_try_advisory_lock() — un seul worker exécute le check
|
||||
├─ SELECT consultations WHERE ends_at < NOW() AND closed_at IS NULL
|
||||
├─ UPDATE consultations SET closed_at = NOW()
|
||||
└─ Thread ──► webhook (non-bloquant)
|
||||
```
|
||||
|
||||
### Sécurité
|
||||
|
||||
#### 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é |
|
||||
| Anti-abus | Honeypot · hCaptcha (optionnel) · cooldown cookie · flood detection · blacklist IP (SHA-256, 30 jours) |
|
||||
| IA | Filtre de contenu Mistral AI + filtre légal interne (16 sources + Légifrance live) |
|
||||
| Base de données | Requêtes paramétrées, accès localhost uniquement |
|
||||
| Système | Utilisateur dédié non-root, systemd sandboxing |
|
||||
|
||||
#### 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 |
|
||||
|
||||
#### 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
|
||||
Pragma: no-cache
|
||||
```
|
||||
|
||||
Exception : `/api/consultations/:slug/export/print` autorise `script-src 'unsafe-inline'` pour `window.print()`.
|
||||
|
||||
### 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 |
|
||||
| Fournisseur IA | Mistral AI (défaut) | Hébergement UE, conformité RGPD, API compatible OpenAI |
|
||||
| Stockage IP | SHA-256 (32 car.) | Anti-abus sans stocker l'IP brute — conformité RGPD art. 6(1)(f) |
|
||||
| Textes légaux | Légifrance PISTE live | Textes consolidés du jour injectés dans le prompt, fallback statique si indisponible |
|
||||
|
||||
### 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 (`REDIS_URL=redis://...`)
|
||||
- **Base de données** : index existants sur `ideas.consultation_id`, `synthesis.consultation_id`, `consents.fingerprint_hash`, `ip_abuse.expires_at` ; ajouter un index sur `ideas.accepted` si la table grossit
|
||||
- **Cache synthèse** : la synthèse est unique en base par contexte, naturellement partagée entre workers
|
||||
|
||||
---
|
||||
|
||||
## Stack technique
|
||||
|
||||
| Couche | Technologie |
|
||||
@@ -73,12 +542,13 @@ La Voix du Peuple est un projet personnel open-source. Si vous l'utilisez et vou
|
||||
| Backend | Python 3.11+ · Flask 3 · Gunicorn |
|
||||
| Base de données | PostgreSQL 15+ (psycopg2, sans ORM) |
|
||||
| IA | Mistral AI (défaut) ou tout fournisseur compatible API OpenAI |
|
||||
| Textes légaux | API Légifrance PISTE — injection temps réel dans le prompt de filtrage |
|
||||
| Sécurité | flask-limiter · bleach · FingerprintJS · Redis (optionnel) · hCaptcha (optionnel) |
|
||||
| Déploiement | Nginx · systemd |
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
## Installation et développement local
|
||||
|
||||
### Prérequis
|
||||
|
||||
@@ -114,6 +584,21 @@ chmod 600 .env
|
||||
| `HCAPTCHA_SECRET_KEY` | Active la vérification hCaptcha côté serveur |
|
||||
| `VITE_HCAPTCHA_SITE_KEY` | Clé publique hCaptcha côté frontend (nécessite un rebuild) |
|
||||
|
||||
**Intégration Légifrance (optionnelle — recommandée) :**
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `PISTE_CLIENT_ID` | Client ID OAuth de l'application PISTE |
|
||||
| `PISTE_CLIENT_SECRET` | Client Secret OAuth de l'application PISTE |
|
||||
| `LEGIFRANCE_CACHE_TTL` | Durée de cache des textes en secondes (défaut : 86400 = 24h) |
|
||||
|
||||
Inscription gratuite sur [piste.gouv.fr](https://piste.gouv.fr) — créer une application et activer l'API Légifrance. Après configuration, initialiser les identifiants d'articles :
|
||||
|
||||
```bash
|
||||
python scripts/check_legal_refs.py --init
|
||||
python scripts/check_legal_refs.py --update
|
||||
```
|
||||
|
||||
Les modèles IA sont configurables via `FILTER_MODEL` (défaut : `mistral-small-latest`) et `SYNTHESIS_MODEL` (défaut : `mistral-large-latest`).
|
||||
|
||||
### Développement local
|
||||
@@ -147,9 +632,15 @@ Pour configurer l'URL du QR code avant le build :
|
||||
bash scripts/set-domain.sh https://votredomaine.fr
|
||||
```
|
||||
|
||||
### Déploiement en production
|
||||
---
|
||||
|
||||
La procédure complète (PostgreSQL, Gunicorn, Nginx, systemd) est documentée dans `docs/INSTALL_ROCKY.md` pour Rocky Linux 9 / AlmaLinux 9 / RHEL 9. Les fichiers de configuration prêts à l'emploi se trouvent dans `deploy/`.
|
||||
## Déploiement en production
|
||||
|
||||
La procédure complète (PostgreSQL, Gunicorn, Nginx, systemd, certificat TLS) est documentée dans [`DEPLOIEMENT.md`](DEPLOIEMENT.md).
|
||||
|
||||
Les fichiers de configuration prêts à l'emploi se trouvent dans `deploy/` :
|
||||
- `deploy/nginx.conf` — configuration Nginx
|
||||
- `deploy/voix-du-peuple-api.service` — unité systemd
|
||||
|
||||
La base de données est initialisée automatiquement au premier démarrage du backend. Les migrations sont incrémentales et idempotentes — un redémarrage après mise à jour suffit.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user