diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 0c95e97..90bf935 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -63,6 +63,6 @@ async def require_executif( Poste.categorie == "executif", ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé à l'exécutif") return membre diff --git a/backend/app/routers/collectes.py b/backend/app/routers/collectes.py index a849b1e..66c643d 100644 --- a/backend/app/routers/collectes.py +++ b/backend/app/routers/collectes.py @@ -23,7 +23,7 @@ async def require_tresorier(membre=Depends(get_current_membre), db: AsyncSession Poste.nom.ilike("%trésorier%"), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au trésorier") return membre diff --git a/backend/app/routers/commandes_jetons.py b/backend/app/routers/commandes_jetons.py index 1e82e71..2f0aff2 100644 --- a/backend/app/routers/commandes_jetons.py +++ b/backend/app/routers/commandes_jetons.py @@ -24,7 +24,7 @@ async def require_resp_jetons(membre=Depends(get_current_membre), db: AsyncSessi Poste.nom.ilike("%jeton%"), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au responsable des jetons") return membre diff --git a/backend/app/routers/contributions.py b/backend/app/routers/contributions.py index 338e14a..1d9a46d 100644 --- a/backend/app/routers/contributions.py +++ b/backend/app/routers/contributions.py @@ -36,7 +36,7 @@ async def require_tresorier(membre=Depends(get_current_membre), db: AsyncSession Poste.nom.ilike("%trésorier%"), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au trésorier") return membre diff --git a/backend/app/routers/depenses.py b/backend/app/routers/depenses.py index 25d24ab..bc72861 100644 --- a/backend/app/routers/depenses.py +++ b/backend/app/routers/depenses.py @@ -25,7 +25,7 @@ async def require_tresorier(membre=Depends(get_current_membre), db: AsyncSession Poste.nom.ilike("%trésorier%"), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au trésorier") return membre diff --git a/backend/app/routers/jetons.py b/backend/app/routers/jetons.py index ac29bcd..bb24a88 100644 --- a/backend/app/routers/jetons.py +++ b/backend/app/routers/jetons.py @@ -23,7 +23,7 @@ async def require_resp_jetons(membre=Depends(get_current_membre), db: AsyncSessi Poste.nom.ilike("%jeton%"), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au responsable des jetons") return membre diff --git a/backend/app/routers/litterature.py b/backend/app/routers/litterature.py index a865960..9038752 100644 --- a/backend/app/routers/litterature.py +++ b/backend/app/routers/litterature.py @@ -22,7 +22,7 @@ async def require_resp_litterature(membre=Depends(get_current_membre), db: Async Poste.nom.ilike("%littérature%"), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au responsable de la littérature") return membre diff --git a/backend/app/routers/presences.py b/backend/app/routers/presences.py index 1cf0df8..a02a330 100644 --- a/backend/app/routers/presences.py +++ b/backend/app/routers/presences.py @@ -26,7 +26,7 @@ async def require_secretaire(membre=Depends(get_current_membre), db=Depends(get_ (Poste.nom.ilike("%secrétaire%") | (Poste.categorie == "executif")), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au secrétaire ou à l'exécutif") return membre diff --git a/backend/app/routers/pv.py b/backend/app/routers/pv.py index b4e0fa8..03713ab 100644 --- a/backend/app/routers/pv.py +++ b/backend/app/routers/pv.py @@ -24,7 +24,7 @@ async def require_secretaire(membre=Depends(get_current_membre), db=Depends(get_ (Poste.nom.ilike("%secrétaire%") | (Poste.categorie == "executif")), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au secrétaire ou à l'exécutif") return membre diff --git a/backend/app/routers/rapports_rsg.py b/backend/app/routers/rapports_rsg.py index 9a206ba..36f7c65 100644 --- a/backend/app/routers/rapports_rsg.py +++ b/backend/app/routers/rapports_rsg.py @@ -23,7 +23,7 @@ async def require_rsg(membre=Depends(get_current_membre), db=Depends(get_db)): (Poste.nom.ilike("%RSG%") | (Poste.categorie == "executif")), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au RSG ou à l'exécutif") return membre diff --git a/backend/app/routers/reunions.py b/backend/app/routers/reunions.py index da37a1d..983e356 100644 --- a/backend/app/routers/reunions.py +++ b/backend/app/routers/reunions.py @@ -23,7 +23,7 @@ async def require_animateur(membre=Depends(get_current_membre), db: AsyncSession (Poste.nom.ilike("%animateur%") | (Poste.categorie == "executif")), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé à l'animateur ou à l'exécutif") return membre diff --git a/backend/app/routers/ventes_litterature.py b/backend/app/routers/ventes_litterature.py index 960845f..63b84d4 100644 --- a/backend/app/routers/ventes_litterature.py +++ b/backend/app/routers/ventes_litterature.py @@ -24,7 +24,7 @@ async def require_resp_litterature(membre=Depends(get_current_membre), db: Async Poste.nom.ilike("%littérature%"), ) ) - if not result.scalar_one_or_none(): + if not result.scalars().first(): raise HTTPException(status_code=403, detail="Réservé au responsable de la littérature") return membre diff --git a/docs/DEPLOIEMENT.md b/docs/DEPLOIEMENT.md index bc5d4b1..2190113 100644 --- a/docs/DEPLOIEMENT.md +++ b/docs/DEPLOIEMENT.md @@ -1,29 +1,41 @@ # Guide de déploiement — Groupe Méditation -## 27 modules · Phases 0 à 9 · Infrastructure Debian + NGINX + PostgreSQL +## 27 modules · Debian Bookworm + NGINX + PostgreSQL 16 + +Testé sur Debian 12 (Bookworm) avec un utilisateur `ansible` ayant accès sudo. --- -## Prérequis - -- Debian 12+ ou Ubuntu 24.04+ -- PostgreSQL 16+ -- Python 3.11+ -- Node.js 20+ (pour le build frontend) -- NGINX 1.24+ -- Certbot (Let's Encrypt) - ---- - -## 1. Installer PostgreSQL +## 0. Prérequis système ```bash +sudo apt update +sudo apt install -y curl ca-certificates gnupg +``` + +--- + +## 1. Installer PostgreSQL 16 + +Debian Bookworm inclut PostgreSQL 15 par défaut. Pour le 16, ajouter le dépôt officiel PGDG : + +```bash +sudo install -d /usr/share/postgresql-common/pgdg +sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \ + --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc +echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" \ + | sudo tee /etc/apt/sources.list.d/pgdg.list + sudo apt update sudo apt install -y postgresql-16 postgresql-client-16 sudo systemctl enable postgresql sudo systemctl start postgresql +``` +Créer l'utilisateur et la base : + +```bash sudo -u postgres psql << 'SQL' CREATE USER grpmed WITH PASSWORD 'CHANGEZ_CE_MOT_DE_PASSE'; CREATE DATABASE groupe_meditation OWNER grpmed; @@ -33,53 +45,104 @@ GRANT ALL ON SCHEMA public TO grpmed; SQL ``` -## 2. Installer Python et les dépendances +Vérifier : + +```bash +sudo -u postgres psql -c "SELECT version();" +``` + +--- + +## 2. Installer Node.js 20 + +Debian Bookworm inclut Node.js 18. Pour le 20 : + +```bash +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt install -y nodejs +``` + +Vérifier : + +```bash +node --version +npm --version +``` + +--- + +## 3. Installer Python et les dépendances système ```bash sudo apt install -y python3 python3-pip python3-venv # Dépendances système pour WeasyPrint (rapports PDF) -sudo apt install -y libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev libcairo2 - -sudo mkdir -p /opt/groupe-meditation -sudo chown $USER:$USER /opt/groupe-meditation - -cp -r backend/ /opt/groupe-meditation/backend/ -cp -r deploy/ /opt/groupe-meditation/deploy/ - -cd /opt/groupe-meditation/backend -python3 -m venv venv -source venv/bin/activate -pip install -r requirements.txt +sudo apt install -y libpango-1.0-0 libpangocairo-1.0-0 \ + libgdk-pixbuf2.0-0 libffi-dev libcairo2 ``` -## 3. Configurer le backend +--- + +## 4. Installer NGINX + +```bash +sudo apt install -y nginx +sudo systemctl enable nginx +sudo systemctl start nginx +``` + +--- + +## 5. Déployer le code source + +```bash +# Copier l'archive sur le serveur, puis : +cd ~ +tar xzf groupe-meditation-complet.tar.gz + +# Backend +sudo mkdir -p /opt/groupe-meditation +sudo cp -r ~/groupe-meditation/backend/ /opt/groupe-meditation/backend/ +sudo cp -r ~/groupe-meditation/deploy/ /opt/groupe-meditation/deploy/ + +# Environnement virtuel Python +cd /opt/groupe-meditation/backend +sudo python3 -m venv venv +sudo /opt/groupe-meditation/backend/venv/bin/pip install -r requirements.txt +``` + +--- + +## 6. Configurer le backend ```bash cd /opt/groupe-meditation/backend -cp .env.example .env -nano .env +sudo cp .env.example .env +sudo nano .env ``` Modifier `.env` : + ``` DATABASE_URL=postgresql+asyncpg://grpmed:VOTRE_MOT_DE_PASSE@localhost:5432/groupe_meditation -JWT_SECRET=GENEREZ_AVEC_openssl_rand_base64_48 +JWT_SECRET=COLLEZ_ICI_LA_SORTIE_DE_LA_COMMANDE_SUIVANTE CORS_ORIGINS=["https://groupe-meditation.chezlepro.ca"] INVITATION_EXPIRE_HOURS=48 ``` Générer le secret JWT : + ```bash openssl rand -base64 48 ``` -## 4. Initialiser la base de données (23 tables) +--- + +## 7. Initialiser la base de données (23 tables) ```bash cd /opt/groupe-meditation -source backend/venv/bin/activate -python deploy/seed.py +sudo /opt/groupe-meditation/backend/venv/bin/python deploy/seed.py ``` Le script crée **interactivement** : @@ -87,26 +150,36 @@ Le script crée **interactivement** : - Les 10 postes prédéfinis - La répartition par défaut des contributions (30/30/30/10) - La littérature de base (5 titres) -- Le premier groupe et le premier membre avec son poste +- Le premier groupe et le premier membre - Un code d'invitation valide 7 jours -## 5. Créer le service systemd +--- + +## 8. Créer le service systemd ```bash -sudo cp deploy/groupe-meditation.service /etc/systemd/system/ +sudo cp /opt/groupe-meditation/deploy/groupe-meditation.service /etc/systemd/system/ sudo chown -R www-data:www-data /opt/groupe-meditation sudo systemctl daemon-reload sudo systemctl enable groupe-meditation sudo systemctl start groupe-meditation - -# Vérifier -curl http://127.0.0.1:8000/api/health ``` -## 6. Construire le frontend +Vérifier : ```bash -cd frontend/ +sudo systemctl status groupe-meditation +curl -s http://127.0.0.1:8000/api/health +``` + +Doit retourner `{"status":"ok","app":"Groupe Méditation","version":"0.1.0"}` + +--- + +## 9. Construire et déployer le frontend + +```bash +cd ~/groupe-meditation/frontend npm install npm run build @@ -115,45 +188,56 @@ sudo cp -r dist/* /var/www/groupe-meditation/ sudo chown -R www-data:www-data /var/www/groupe-meditation ``` -## 7. Configurer NGINX +--- -NGINX fait tout : servir les fichiers statiques du frontend ET proxy vers FastAPI. Pas besoin d'Apache. +## 10. Configurer NGINX + +NGINX fait tout : fichiers statiques du frontend + reverse proxy vers FastAPI. ```bash -sudo cp deploy/nginx-groupe-meditation.conf /etc/nginx/sites-available/groupe-meditation -sudo ln -s /etc/nginx/sites-available/groupe-meditation /etc/nginx/sites-enabled/ +sudo cp ~/groupe-meditation/deploy/nginx-groupe-meditation.conf \ + /etc/nginx/sites-available/groupe-meditation +sudo ln -sf /etc/nginx/sites-available/groupe-meditation /etc/nginx/sites-enabled/ + +# Retirer le site par défaut si présent +sudo rm -f /etc/nginx/sites-enabled/default sudo nginx -t sudo systemctl reload nginx +``` -# Certificat TLS +Tester en HTTP : + +```bash +curl -s http://groupe-meditation.chezlepro.ca/api/health +``` + +--- + +## 11. Certificat TLS (Let's Encrypt) + +```bash +sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d groupe-meditation.chezlepro.ca ``` -NGINX sert : -- `/api/*` → reverse proxy vers FastAPI (Uvicorn port 8000) -- `/*` → fichiers statiques du build React (SPA routing via `try_files`) -- Cache 1 an sur les assets (JS, CSS, images), pas de cache sur index.html +Certbot modifie automatiquement la config NGINX pour ajouter HTTPS et la redirection 80→443. -## 8. Sauvegarde automatique +--- + +## 12. Sauvegarde automatique ```bash -sudo cp deploy/backup.sh /etc/cron.daily/groupe-meditation-backup +sudo cp ~/groupe-meditation/deploy/backup.sh /etc/cron.daily/groupe-meditation-backup sudo chmod +x /etc/cron.daily/groupe-meditation-backup ``` -- pg_dump quotidien en format custom (-Fc) -- Rétention 30 jours -- **Test de restauration mensuel recommandé** : +--- + +## 13. Vérification finale ```bash -pg_restore -U grpmed -d groupe_meditation_test /var/backups/groupe-meditation/grpmed_DERNIER.dump -``` - -## 9. Vérification finale - -```bash -# API +# API en HTTPS curl -s https://groupe-meditation.chezlepro.ca/api/health | python3 -m json.tool # Documentation OpenAPI @@ -172,27 +256,24 @@ curl -s https://groupe-meditation.chezlepro.ca/api/health | python3 -m json.tool |----------|----------| | API ne démarre pas | `sudo journalctl -u groupe-meditation -f` | | Erreur base de données | `sudo -u postgres psql -d groupe_meditation -c "SELECT count(*) FROM groupes;"` | -| NGINX 502 | `sudo systemctl status groupe-meditation` (vérifier que FastAPI tourne) | +| NGINX 502 | `sudo systemctl status groupe-meditation` | | WeasyPrint échoue | `sudo apt install libpango-1.0-0 libpangocairo-1.0-0` | -| Mise à jour frontend | `cd frontend && npm run build && sudo cp -r dist/* /var/www/groupe-meditation/` | -| Mise à jour backend | `sudo systemctl restart groupe-meditation` | +| seed.py ImportError | Vérifier les noms de classes dans `backend/app/models/` | ## Mise à jour de l'application ```bash # Backend cd /opt/groupe-meditation/backend -source venv/bin/activate -pip install -r requirements.txt +sudo venv/bin/pip install -r requirements.txt sudo systemctl restart groupe-meditation # Frontend -cd frontend -npm install -npm run build +cd ~/groupe-meditation/frontend +npm install && npm run build sudo cp -r dist/* /var/www/groupe-meditation/ ``` --- -**Hébergé au Québec · 100% logiciel libre · Zéro traçage · 23 tables · 25 routers · 27 modules** +**Hébergé au Québec · 100% logiciel libre · Zéro traçage**