diff --git a/DEPLOIEMENT.md b/DEPLOIEMENT.md index a40e2cc..41728cd 100644 --- a/DEPLOIEMENT.md +++ b/DEPLOIEMENT.md @@ -1,5 +1,5 @@ # Guide d'auto-hébergement — La Voix du Peuple -## RockyLinux 9 (ou RHEL 9, AlmaLinux 9) +## Rocky Linux 9 (ou RHEL 9, AlmaLinux 9) --- @@ -18,17 +18,20 @@ Internet - **Frontend** : React + Vite, servi comme fichiers statiques par Nginx - **Backend** : Flask + Gunicorn (4 workers), accessible uniquement via Nginx - **Base de données** : PostgreSQL 15+ -- **IA** : Mistral AI (MISTRAL_API_KEY requis) +- **IA** : Mistral AI (`MISTRAL_API_KEY` requis) ou tout fournisseur compatible OpenAI + +> **Note production :** En déploiement exposé à Internet, placer HAProxy devant Nginx pour la +> terminaison TLS. Voir `ARCHITECTURE.md` pour le schéma complet. Dans cette configuration, +> Nginx écoute sur le port 8080 (fichier `deploy/nginx.conf`) et HAProxy forward sur ce port. +> Pour un déploiement standalone sans HAProxy, changez `listen 8080;` en `listen 80;` dans +> `deploy/nginx.conf` et activez HTTPS avec certbot (section 12). --- ## Prérequis ```bash -# Mettre à jour le système sudo dnf update -y - -# Outils de base sudo dnf install -y git curl wget epel-release ``` @@ -73,21 +76,22 @@ sudo systemctl enable nginx ## 2. Configurer PostgreSQL ```bash -# Connexion en tant que postgres sudo -u postgres psql +``` --- Dans psql : +Dans psql : +```sql CREATE USER voixdupeuple WITH PASSWORD 'CHANGEME_MOT_DE_PASSE_FORT'; CREATE DATABASE voixdupeuple OWNER voixdupeuple; GRANT ALL PRIVILEGES ON DATABASE voixdupeuple TO voixdupeuple; \q ``` -Éditez `/var/lib/pgsql/15/data/pg_hba.conf` pour autoriser la connexion locale par mot de passe : +Éditez `/var/lib/pgsql/15/data/pg_hba.conf` — remplacez `ident` par `scram-sha-256` sur +la ligne `127.0.0.1/32` : ``` -# Remplacez "ident" par "md5" sur la ligne 127.0.0.1/32 : -host all all 127.0.0.1/32 md5 +host all all 127.0.0.1/32 scram-sha-256 ``` ```bash @@ -109,13 +113,12 @@ sudo chown voixdupeuple:voixdupeuple /opt/voix-du-peuple --- -## 4. Cloner le dépôt depuis Gitea +## 4. Cloner le dépôt ```bash -sudo -u voixdupeuple bash -c " - cd /opt - git clone https://votre-gitea.example.com/utilisateur/voix-du-peuple.git voix-du-peuple -" +sudo -u voixdupeuple git clone \ + https://votre-depot.example.com/voix-du-peuple.git \ + /opt/voix-du-peuple ``` --- @@ -123,27 +126,24 @@ sudo -u voixdupeuple bash -c " ## 5. Configurer les variables d'environnement ```bash -sudo -u voixdupeuple bash -c " - cp /opt/voix-du-peuple/.env.example /opt/voix-du-peuple/.env -" - -# Éditer le fichier .env +sudo -u voixdupeuple cp /opt/voix-du-peuple/.env.example /opt/voix-du-peuple/.env sudo nano /opt/voix-du-peuple/.env ``` -Remplissez au minimum : +Valeurs obligatoires à renseigner : ```env +# Base de données DATABASE_URL=postgresql://voixdupeuple:CHANGEME_MOT_DE_PASSE_FORT@127.0.0.1:5432/voixdupeuple -OPENAI_API_KEY=sk-VOTRE_CLE_OPENAI -SESSION_SECRET=GENEREZ_UNE_VALEUR_ALEATOIRE_ICI -PORT=8000 -FLASK_ENV=production -``` -Générez le `SESSION_SECRET` : -```bash -python3.11 -c "import secrets; print(secrets.token_hex(32))" +# IA — Mistral AI (recommandé) +MISTRAL_API_KEY=votre-cle-mistral + +# Sécurité — générez avec : python3 -c "import secrets; print(secrets.token_hex(32))" +SECRET_KEY=VALEUR_ALEATOIRE_LONGUE + +# Panel d'administration +ADMIN_SECRET=MOT_DE_PASSE_ADMIN_FORT ``` Sécurisez le fichier : @@ -169,18 +169,9 @@ sudo -u voixdupeuple bash -c " ## 7. Initialiser la base de données -```bash -sudo -u voixdupeuple bash -c " - cd /opt/voix-du-peuple/artifacts/flask-api - source /opt/voix-du-peuple/.env - export DATABASE_URL OPENAI_API_KEY SESSION_SECRET FLASK_ENV PORT - /opt/voix-du-peuple/.venv/bin/python3 -c ' -from database import init_db -init_db() -print(\"Base de données initialisée.\") -' -" -``` +La base de données est initialisée **automatiquement** au premier démarrage du service +(création des tables et migrations incrémentales idempotentes). Aucune action manuelle n'est +requise. Passez directement à l'étape suivante. --- @@ -190,14 +181,17 @@ print(\"Base de données initialisée.\") sudo -u voixdupeuple bash -c " cd /opt/voix-du-peuple pnpm install --frozen-lockfile - cd artifacts/voix-du-peuple - pnpm exec vite build --config vite.config.selfhost.ts + pnpm build " ``` Les fichiers statiques sont générés dans `artifacts/voix-du-peuple/dist/public/`. +Si vous avez configuré `VITE_APP_URL` (URL publique du site, pour le QR code du flyer) ou +`VITE_HCAPTCHA_SITE_KEY`, ajoutez ces variables dans le `.env` **avant** le build — elles +sont intégrées statiquement dans le bundle. + --- ## 9. Configurer le service systemd (Gunicorn) @@ -229,14 +223,16 @@ curl http://127.0.0.1:8000/api/healthz ## 10. Configurer Nginx +Le fichier `deploy/nginx.conf` configure Nginx en écoute sur le port **8080** (mode +derrière HAProxy). Pour un déploiement standalone sans HAProxy, changez `listen 8080;` +en `listen 80;` avant de copier. + ```bash -# Copier la config Nginx sudo cp /opt/voix-du-peuple/deploy/nginx.conf \ /etc/nginx/conf.d/voix-du-peuple.conf -# Éditez le fichier pour mettre votre nom de domaine -sudo nano /etc/nginx/conf.d/voix-du-peuple.conf -# Remplacez voix-du-peuple.example.com par votre domaine +# Si déploiement standalone : changer le port d'écoute +sudo sed -i 's/listen 8080;/listen 80;/' /etc/nginx/conf.d/voix-du-peuple.conf # Tester la configuration sudo nginx -t @@ -250,32 +246,35 @@ sudo systemctl reload nginx ## 11. Ouvrir le pare-feu ```bash +# Standalone (Nginx expose directement HTTP/HTTPS) sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --permanent --add-service=https sudo firewall-cmd --reload ``` -SELinux — autoriser Nginx à se connecter à Gunicorn : +SELinux — autoriser Nginx à proxyfier vers Gunicorn : ```bash sudo setsebool -P httpd_can_network_connect 1 ``` --- -## 12. (Recommandé) HTTPS avec Let's Encrypt +## 12. (Recommandé) HTTPS avec Let's Encrypt — déploiement standalone + +> Ne s'applique qu'en déploiement standalone (Nginx exposé directement). Si HAProxy gère +> le TLS, configurez Let's Encrypt sur HAProxy — voir `ARCHITECTURE.md`. ```bash sudo dnf install -y certbot python3-certbot-nginx +# Remplacez voix-du-peuple.example.com par votre domaine sudo certbot --nginx -d voix-du-peuple.example.com \ - --email admin@example.com --agree-tos --no-eff-email + --email contact@example.com --agree-tos --no-eff-email -# Renouvellement automatique (déjà configuré par certbot) +# Vérifier le renouvellement automatique sudo systemctl status certbot-renew.timer ``` -Puis décommentez le bloc HTTPS dans `/etc/nginx/conf.d/voix-du-peuple.conf`. - --- ## Structure du projet @@ -283,28 +282,31 @@ Puis décommentez le bloc HTTPS dans `/etc/nginx/conf.d/voix-du-peuple.conf`. ``` voix-du-peuple/ ├── artifacts/ -│ ├── flask-api/ # Backend Python Flask -│ │ ├── app.py # Application principale + routes -│ │ ├── ai_agent.py # Agent IA (filtrage + synthèse) -│ │ ├── legal_framework.py # Prompts ancrés dans le droit international -│ │ ├── database.py # Accès PostgreSQL (psycopg2) -│ │ ├── requirements.txt # Dépendances Python -│ │ └── start.sh # Démarrage développement -│ └── voix-du-peuple/ # Frontend React + Vite +│ ├── flask-api/ # Backend Python Flask +│ │ ├── app.py # Application principale + routes API +│ │ ├── ai_agent.py # Agent IA (filtrage, synthèse, check-law) +│ │ ├── legal_framework.py # Prompts ancrés dans le droit (16 sources) +│ │ ├── database.py # Accès PostgreSQL (psycopg2, sans ORM) +│ │ ├── requirements.txt # Dépendances Python +│ │ └── start.sh # Démarrage dev direct (python app.py) +│ └── voix-du-peuple/ # Frontend React + Vite 7 │ ├── src/ -│ │ ├── pages/ # home.tsx, about.tsx -│ │ ├── components/ # Composants UI (shadcn/ui + radix) -│ │ └── App.tsx # Routing principal -│ └── vite.config.ts # Config Vite (développement + production) +│ │ ├── pages/ # home, about, transparence, contributions-brutes, +│ │ │ # consultations, legal-notice, privacy-policy, admin… +│ │ ├── components/ # UI (shadcn/ui + Radix), consent-dialog, navbar… +│ │ ├── lib/ # prompts.ts (verbatim IA), hooks, utils +│ │ └── App.tsx # Routing principal (Wouter) +│ └── vite.config.ts # Config Vite (dev + production) ├── lib/ -│ ├── api-spec/ # Spécification OpenAPI -│ ├── api-client-react/ # Hooks React Query générés -│ └── db/ # Schéma Drizzle (référence) +│ ├── api-spec/ # Spécification OpenAPI + config Orval +│ ├── api-client-react/ # Hooks React Query générés +│ └── api-zod/ # Schémas Zod générés ├── deploy/ │ ├── voix-du-peuple-api.service # Service systemd Gunicorn -│ └── nginx.conf # Configuration Nginx -├── .env.example # Variables d'environnement (modèle) -└── DEPLOIEMENT.md # Ce fichier +│ └── nginx.conf # Configuration Nginx (port 8080, derrière HAProxy) +├── docs/ # Documentation technique (RGPD, sécurité, prompts…) +├── .env.example # Variables d'environnement (modèle commenté) +└── DEPLOIEMENT.md # Ce fichier ``` --- @@ -313,32 +315,36 @@ voix-du-peuple/ | Variable | Obligatoire | Description | |----------|-------------|-------------| -| `DATABASE_URL` | Oui | URL PostgreSQL complète | -| `OPENAI_API_KEY` | Oui | Clé API OpenAI (sk-...) | -| `OPENAI_BASE_URL` | Non | Proxy OpenAI compatible (Ollama, Azure, etc.) | -| `OPENAI_FILTER_MODEL` | Non | Modèle de filtrage (défaut : `gpt-4o-mini`) | -| `OPENAI_SYNTHESIS_MODEL` | Non | Modèle de synthèse (défaut : `gpt-4o`) | -| `SESSION_SECRET` | Oui | Clé secrète Flask (min 32 caractères aléatoires) | -| `PORT` | Non | Port Gunicorn (défaut : 8000) | -| `FLASK_ENV` | Non | `production` ou `development` | +| `DATABASE_URL` | Oui | URL PostgreSQL — `postgresql://user:pass@host:5432/db` | +| `MISTRAL_API_KEY` | Oui* | Clé API Mistral AI (fournisseur par défaut) | +| `OPENAI_API_KEY` | Oui* | Alternative si Mistral non utilisé (compatible OpenAI) | +| `OPENAI_BASE_URL` | Non | Base URL pour fournisseur tiers (Ollama, proxy…) | +| `FILTER_MODEL` | Non | Modèle de filtrage (défaut : `mistral-small-latest`) | +| `SYNTHESIS_MODEL` | Non | Modèle de synthèse (défaut : `mistral-large-latest`) | +| `SECRET_KEY` | Recommandé | Active le cooldown cookie et le consentement RGPD signé HMAC-SHA256 | +| `ADMIN_SECRET` | Recommandé | Mot de passe Bearer token pour le panel `/admin` | +| `REDIS_URL` | Non | Rate limiting persistant entre workers — `redis://localhost:6379/0` | +| `RATE_LIMIT_CONTRIBUTIONS` | Non | Seuil de soumissions (défaut : `5 per minute;3 per hour`) | +| `CONTRIBUTION_COOLDOWN_SECONDS` | Non | Délai inter-soumissions par session (défaut : `240`) | +| `FLOOD_THRESHOLD` | Non | Seuil d'alerte flood en 5 min (défaut : `10`) | +| `IP_BLACKLIST_DAYS` | Non | Durée du blacklist IP (défaut : `30`) | +| `HCAPTCHA_SECRET_KEY` | Non | Active la vérification hCaptcha côté serveur | +| `VITE_HCAPTCHA_SITE_KEY` | Non | Clé publique hCaptcha côté frontend (rebuild requis) | +| `VITE_APP_URL` | Non | URL publique du site — utilisée par le QR code du flyer (rebuild requis) | + +*`MISTRAL_API_KEY` est prioritaire. Si absent, `OPENAI_API_KEY` est utilisé. L'un des deux est obligatoire. --- -## Modèles IA utilisés +## Modèles IA -| Fonction | Modèle par défaut | Variable d'environnement | -|----------|-------------------|--------------------------| -| Filtrage des idées | `mistral-small-latest` | `FILTER_MODEL` | +| Rôle | Modèle par défaut | Variable de contrôle | +|------|-------------------|----------------------| +| Filtrage des contributions | `mistral-small-latest` | `FILTER_MODEL` | | Synthèse collective | `mistral-large-latest` | `SYNTHESIS_MODEL` | +| Vérification cadre légal | même que `FILTER_MODEL` | `FILTER_MODEL` | -Pour changer les modèles, éditez `artifacts/flask-api/ai_agent.py` : -```python -# Filtrage (ligne ~56) -model="gpt-4o-mini", # ou tout modèle OpenAI compatible - -# Synthèse (ligne ~104) -model="gpt-4o", # ou tout modèle OpenAI compatible -``` +Pour changer de modèle, définissez la variable dans `.env` — aucune modification du code n'est nécessaire. --- @@ -349,16 +355,15 @@ sudo -u voixdupeuple bash -c " cd /opt/voix-du-peuple git pull origin main - # Mise à jour des dépendances Python si nécessaire + # Dépendances Python .venv/bin/pip install -r artifacts/flask-api/requirements.txt - # Mise à jour des dépendances Node et rebuild frontend + # Rebuild frontend pnpm install --frozen-lockfile - cd artifacts/voix-du-peuple - pnpm exec vite build --config vite.config.selfhost.ts + cd artifacts/voix-du-peuple && pnpm build " -# Redémarrer le service +# Redémarrer le service (les migrations DB s'appliquent automatiquement) sudo systemctl restart voix-du-peuple-api sudo systemctl reload nginx ``` @@ -368,10 +373,10 @@ sudo systemctl reload nginx ## Logs et supervision ```bash -# Logs API Flask +# Logs API Flask (journald + fichiers) sudo journalctl -u voix-du-peuple-api -f -tail -f /var/log/voix-du-peuple/api-error.log -tail -f /var/log/voix-du-peuple/api-access.log +sudo tail -f /var/log/voix-du-peuple/api-error.log +sudo tail -f /var/log/voix-du-peuple/api-access.log # Logs Nginx sudo tail -f /var/log/nginx/error.log @@ -391,31 +396,44 @@ sudo systemctl status postgresql-15 ```bash # Vérifier que Gunicorn tourne sudo systemctl status voix-du-peuple-api + # Vérifier SELinux sudo setsebool -P httpd_can_network_connect 1 + +# Tester Gunicorn directement +curl http://127.0.0.1:8000/api/healthz ``` ### Erreur "could not connect to server" (PostgreSQL) ```bash -# Vérifier que PostgreSQL tourne sudo systemctl status postgresql-15 -# Vérifier l'URL de connexion dans .env -psql -U voixdupeuple -h 127.0.0.1 -d voixdupeuple +psql -U voixdupeuple -h 127.0.0.1 -d voixdupeuple -c "\dt" +# Vérifier DATABASE_URL dans .env ``` -### Erreur OpenAI API +### Erreur Mistral AI API ```bash # Vérifier la clé dans .env -grep OPENAI_API_KEY /opt/voix-du-peuple/.env -# Tester directement -curl https://api.openai.com/v1/models \ - -H "Authorization: Bearer $OPENAI_API_KEY" +grep MISTRAL_API_KEY /opt/voix-du-peuple/.env + +# Tester l'accès à l'API Mistral +curl https://api.mistral.ai/v1/models \ + -H "Authorization: Bearer $(grep MISTRAL_API_KEY /opt/voix-du-peuple/.env | cut -d= -f2)" ``` ### Le frontend affiche une page blanche ```bash # Vérifier que le build existe ls /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/public/ + # Vérifier les droits -sudo chown -R voixdupeuple:voixdupeuple /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/ +sudo chown -R voixdupeuple:voixdupeuple \ + /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/ +``` + +### Les variables VITE_* ne sont pas prises en compte +Les variables préfixées `VITE_` sont intégrées au moment du build, pas à l'exécution. +Après toute modification de `VITE_APP_URL` ou `VITE_HCAPTCHA_SITE_KEY`, relancez le build : +```bash +cd /opt/voix-du-peuple/artifacts/voix-du-peuple && pnpm build ```