Renommer l'application en app

This commit is contained in:
Daniel Allaire 2026-06-01 09:16:37 -04:00
parent 7fc75eacde
commit ddb391c673
33 changed files with 166 additions and 83 deletions

View file

@ -6,7 +6,7 @@
tasks: tasks:
- name: Lancer le script de sauvegarde - name: Lancer le script de sauvegarde
ansible.builtin.command: /etc/cron.daily/groupe-meditation-backup ansible.builtin.command: /etc/cron.daily/app-backup
changed_when: true changed_when: true
- name: Lister les dernieres sauvegardes - name: Lister les dernieres sauvegardes

View file

@ -3,16 +3,16 @@
groupe_meditation_domain: app.87-16.org groupe_meditation_domain: app.87-16.org
# Chemins de déploiement. # Chemins de déploiement.
groupe_meditation_app_dir: /opt/groupe-meditation groupe_meditation_app_dir: /opt/app
groupe_meditation_backend_dir: "{{ groupe_meditation_app_dir }}/backend" groupe_meditation_backend_dir: "{{ groupe_meditation_app_dir }}/backend"
groupe_meditation_deploy_dir: "{{ groupe_meditation_app_dir }}/deploy" groupe_meditation_deploy_dir: "{{ groupe_meditation_app_dir }}/deploy"
groupe_meditation_data_dir: "{{ groupe_meditation_app_dir }}/data" groupe_meditation_data_dir: "{{ groupe_meditation_app_dir }}/data"
groupe_meditation_frontend_dir: "{{ groupe_meditation_app_dir }}/frontend" groupe_meditation_frontend_dir: "{{ groupe_meditation_app_dir }}/frontend"
groupe_meditation_web_root: /var/www/groupe-meditation groupe_meditation_web_root: /var/www/app
groupe_meditation_upload_dir: "{{ groupe_meditation_app_dir }}/uploads/preuves" groupe_meditation_upload_dir: "{{ groupe_meditation_app_dir }}/uploads/preuves"
# Service applicatif. # Service applicatif.
groupe_meditation_service_name: groupe-meditation groupe_meditation_service_name: app
groupe_meditation_service_user: www-data groupe_meditation_service_user: www-data
groupe_meditation_service_group: www-data groupe_meditation_service_group: www-data
groupe_meditation_api_host: 127.0.0.1 groupe_meditation_api_host: 127.0.0.1
@ -54,5 +54,5 @@ groupe_meditation_enable_tls: false
groupe_meditation_certbot_email: admin@example.com groupe_meditation_certbot_email: admin@example.com
# Sauvegardes. # Sauvegardes.
groupe_meditation_backup_dir: /var/backups/groupe-meditation groupe_meditation_backup_dir: /var/backups/app
groupe_meditation_backup_retention_days: 30 groupe_meditation_backup_retention_days: 30

View file

@ -3,7 +3,7 @@ all:
children: children:
groupe_meditation: groupe_meditation:
hosts: hosts:
groupe-meditation-prod: app-prod:
ansible_host: 192.168.12.46 ansible_host: 192.168.12.46
ansible_user: ansible ansible_user: ansible
ansible_ssh_private_key_file: ~/.ssh/id_ed25519_ansible_chezlepro ansible_ssh_private_key_file: ~/.ssh/id_ed25519_ansible_chezlepro

View file

@ -3,7 +3,7 @@
ansible.builtin.systemd: ansible.builtin.systemd:
daemon_reload: true daemon_reload: true
- name: Redemarrer groupe-meditation - name: Redemarrer app
ansible.builtin.service: ansible.builtin.service:
name: "{{ groupe_meditation_service_name }}" name: "{{ groupe_meditation_service_name }}"
state: restarted state: restarted

View file

@ -39,21 +39,21 @@
- "--exclude=.env" - "--exclude=.env"
- "--exclude=__pycache__/" - "--exclude=__pycache__/"
- "--exclude=*.pyc" - "--exclude=*.pyc"
notify: Redemarrer groupe-meditation notify: Redemarrer app
- name: Synchroniser les fichiers de deploiement - name: Synchroniser les fichiers de deploiement
ansible.posix.synchronize: ansible.posix.synchronize:
src: "{{ playbook_dir }}/../deploy/" src: "{{ playbook_dir }}/../deploy/"
dest: "{{ groupe_meditation_deploy_dir }}/" dest: "{{ groupe_meditation_deploy_dir }}/"
delete: true delete: true
notify: Redemarrer groupe-meditation notify: Redemarrer app
- name: Synchroniser les donnees d'import - name: Synchroniser les donnees d'import
ansible.posix.synchronize: ansible.posix.synchronize:
src: "{{ playbook_dir }}/../data/" src: "{{ playbook_dir }}/../data/"
dest: "{{ groupe_meditation_data_dir }}/" dest: "{{ groupe_meditation_data_dir }}/"
delete: true delete: true
notify: Redemarrer groupe-meditation notify: Redemarrer app
- name: Synchroniser le frontend - name: Synchroniser le frontend
ansible.posix.synchronize: ansible.posix.synchronize:
@ -73,7 +73,7 @@
ansible.builtin.pip: ansible.builtin.pip:
requirements: "{{ groupe_meditation_backend_dir }}/requirements.txt" requirements: "{{ groupe_meditation_backend_dir }}/requirements.txt"
virtualenv: "{{ groupe_meditation_backend_dir }}/venv" virtualenv: "{{ groupe_meditation_backend_dir }}/venv"
notify: Redemarrer groupe-meditation notify: Redemarrer app
- name: Deployer le fichier d'environnement backend - name: Deployer le fichier d'environnement backend
ansible.builtin.template: ansible.builtin.template:
@ -83,7 +83,7 @@
group: "{{ groupe_meditation_service_group }}" group: "{{ groupe_meditation_service_group }}"
mode: "0640" mode: "0640"
no_log: true no_log: true
notify: Redemarrer groupe-meditation notify: Redemarrer app
- name: Installer les dependances frontend sur le poste de controle avec npm ci - name: Installer les dependances frontend sur le poste de controle avec npm ci
ansible.builtin.command: ansible.builtin.command:
@ -130,16 +130,31 @@
- "{{ groupe_meditation_app_dir }}" - "{{ groupe_meditation_app_dir }}"
- "{{ groupe_meditation_web_root }}" - "{{ groupe_meditation_web_root }}"
- name: Arreter l'ancien service applicatif si present
ansible.builtin.service:
name: groupe-meditation
state: stopped
enabled: false
failed_when: false
when: groupe_meditation_service_name != "groupe-meditation"
- name: Retirer l'ancienne unite systemd
ansible.builtin.file:
path: /etc/systemd/system/groupe-meditation.service
state: absent
when: groupe_meditation_service_name != "groupe-meditation"
notify: Recharger systemd
- name: Installer l'unite systemd - name: Installer l'unite systemd
ansible.builtin.template: ansible.builtin.template:
src: groupe-meditation.service.j2 src: app.service.j2
dest: "/etc/systemd/system/{{ groupe_meditation_service_name }}.service" dest: "/etc/systemd/system/{{ groupe_meditation_service_name }}.service"
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
notify: notify:
- Recharger systemd - Recharger systemd
- Redemarrer groupe-meditation - Redemarrer app
- name: Compter les groupes existants - name: Compter les groupes existants
community.postgresql.postgresql_query: community.postgresql.postgresql_query:
@ -185,7 +200,7 @@
- groupe_meditation_run_seed | bool - groupe_meditation_run_seed | bool
- groupes_table.query_result[0].count | int == 0 or (groupes_count.query_result[0].count | default(0) | int == 0) - groupes_table.query_result[0].count | int == 0 or (groupes_count.query_result[0].count | default(0) | int == 0)
no_log: true no_log: true
notify: Redemarrer groupe-meditation notify: Redemarrer app
- name: Appliquer les handlers apres le seed - name: Appliquer les handlers apres le seed
ansible.builtin.meta: flush_handlers ansible.builtin.meta: flush_handlers

View file

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Groupe Meditation API Description=App API
After=network.target postgresql.service After=network.target postgresql.service
Requires=postgresql.service Requires=postgresql.service
@ -15,4 +15,3 @@ RestartSec=5
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View file

@ -9,9 +9,14 @@
- name: Installer la sauvegarde quotidienne - name: Installer la sauvegarde quotidienne
ansible.builtin.template: ansible.builtin.template:
src: groupe-meditation-backup.j2 src: app-backup.j2
dest: /etc/cron.daily/groupe-meditation-backup dest: /etc/cron.daily/app-backup
owner: root owner: root
group: root group: root
mode: "0750" mode: "0750"
no_log: true no_log: true
- name: Retirer l'ancienne sauvegarde quotidienne
ansible.builtin.file:
path: /etc/cron.daily/groupe-meditation-backup
state: absent

View file

@ -16,12 +16,13 @@ cd "{{ groupe_meditation_backend_dir }}"
"{{ groupe_meditation_backend_dir }}/venv/bin/python" "{{ groupe_meditation_deploy_dir }}/tenant_schemas.py" "{{ groupe_meditation_backend_dir }}/venv/bin/python" "{{ groupe_meditation_deploy_dir }}/tenant_schemas.py"
"{{ groupe_meditation_backend_dir }}/venv/bin/python" "{{ groupe_meditation_deploy_dir }}/group_backup.py" all "{{ groupe_meditation_backend_dir }}/venv/bin/python" "{{ groupe_meditation_deploy_dir }}/group_backup.py" all
pg_dump -U "$DB_USER" -h "$DB_HOST" -Fc "$DB_NAME" > "$BACKUP_DIR/grpmed_${DATE}.dump" pg_dump -U "$DB_USER" -h "$DB_HOST" -Fc "$DB_NAME" > "$BACKUP_DIR/app_${DATE}.dump"
for schema in $(PGPASSWORD="$PGPASSWORD" psql -U "$DB_USER" -h "$DB_HOST" -d "$DB_NAME" -Atc "SELECT tenant_schema FROM groupes WHERE tenant_schema IS NOT NULL ORDER BY tenant_schema"); do for schema in $(PGPASSWORD="$PGPASSWORD" psql -U "$DB_USER" -h "$DB_HOST" -d "$DB_NAME" -Atc "SELECT tenant_schema FROM groupes WHERE tenant_schema IS NOT NULL ORDER BY tenant_schema"); do
pg_dump -U "$DB_USER" -h "$DB_HOST" -n "$schema" -Fc "$DB_NAME" > "$BACKUP_DIR/${schema}_${DATE}.dump" pg_dump -U "$DB_USER" -h "$DB_HOST" -n "$schema" -Fc "$DB_NAME" > "$BACKUP_DIR/${schema}_${DATE}.dump"
done done
find "$BACKUP_DIR" -name "grpmed_*.dump" -mtime +"$RETENTION_DAYS" -delete find "$BACKUP_DIR" -name "grpmed_*.dump" -mtime +"$RETENTION_DAYS" -delete
find "$BACKUP_DIR" -name "app_*.dump" -mtime +"$RETENTION_DAYS" -delete
find "$BACKUP_DIR" -name "group_*.json" -mtime +"$RETENTION_DAYS" -delete find "$BACKUP_DIR" -name "group_*.json" -mtime +"$RETENTION_DAYS" -delete
find "$BACKUP_DIR" -name "grp_*.dump" -mtime +"$RETENTION_DAYS" -delete find "$BACKUP_DIR" -name "grp_*.dump" -mtime +"$RETENTION_DAYS" -delete
echo "[$(date)] Sauvegarde terminee: grpmed_${DATE}.dump" echo "[$(date)] Sauvegarde terminee: app_${DATE}.dump"

View file

@ -6,8 +6,8 @@
- name: Installer le site NGINX - name: Installer le site NGINX
ansible.builtin.template: ansible.builtin.template:
src: groupe-meditation.conf.j2 src: app.conf.j2
dest: /etc/nginx/sites-available/groupe-meditation dest: /etc/nginx/sites-available/app
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
@ -15,12 +15,21 @@
- name: Activer le site NGINX - name: Activer le site NGINX
ansible.builtin.file: ansible.builtin.file:
src: /etc/nginx/sites-available/groupe-meditation src: /etc/nginx/sites-available/app
dest: /etc/nginx/sites-enabled/groupe-meditation dest: /etc/nginx/sites-enabled/app
state: link state: link
force: true force: true
notify: Recharger nginx notify: Recharger nginx
- name: Desactiver l'ancien site NGINX
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/nginx/sites-enabled/groupe-meditation
- /etc/nginx/sites-available/groupe-meditation
notify: Recharger nginx
- name: Desactiver le site NGINX par defaut - name: Desactiver le site NGINX par defaut
ansible.builtin.file: ansible.builtin.file:
path: /etc/nginx/sites-enabled/default path: /etc/nginx/sites-enabled/default
@ -36,4 +45,3 @@
name: nginx name: nginx
state: started state: started
enabled: true enabled: true

View file

@ -1,5 +1,5 @@
--- ---
- name: Installer Groupe Meditation - name: Installer App
hosts: groupe_meditation hosts: groupe_meditation
become: true become: true
gather_facts: true gather_facts: true

View file

@ -1,5 +1,5 @@
--- ---
- name: Verifier Groupe Meditation - name: Verifier App
hosts: groupe_meditation hosts: groupe_meditation
become: true become: true
gather_facts: false gather_facts: false
@ -39,4 +39,3 @@
that: that:
- frontend_index.stat.exists - frontend_index.stat.exists
fail_msg: "Le fichier {{ groupe_meditation_web_root }}/index.html est absent." fail_msg: "Le fichier {{ groupe_meditation_web_root }}/index.html est absent."

View file

@ -20,7 +20,7 @@ class Settings(BaseSettings):
VAPID_PUBLIC_KEY: str | None = None VAPID_PUBLIC_KEY: str | None = None
VAPID_PRIVATE_KEY_B64: str | None = None VAPID_PRIVATE_KEY_B64: str | None = None
VAPID_SUBJECT: str = "mailto:admin@app.87-16.org" VAPID_SUBJECT: str = "mailto:admin@app.87-16.org"
GROUP_BACKUP_DIR: str = "/var/backups/groupe-meditation" GROUP_BACKUP_DIR: str = "/var/backups/app"
class Config: class Config:
env_file = str(_ENV_FILE) env_file = str(_ENV_FILE)

View file

@ -1,5 +1,5 @@
""" """
Groupe Méditation — API Backend App — API Backend
District 87-16, Région 87 District 87-16, Région 87
100% logiciel libre · Hébergé au Québec 100% logiciel libre · Hébergé au Québec
""" """
@ -130,8 +130,8 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title="Groupe Méditation API", title="App API",
description="API du Groupe Méditation — District 87-16, Région 87", description="API du App — District 87-16, Région 87",
version="0.1.0", version="0.1.0",
lifespan=lifespan, lifespan=lifespan,
) )
@ -178,4 +178,4 @@ app.include_router(journal.router, prefix="/api")
@app.get("/api/health") @app.get("/api/health")
async def root_health(): async def root_health():
return {"status": "ok", "app": "Groupe Méditation", "version": "0.1.0"} return {"status": "ok", "app": "App", "version": "0.1.0"}

View file

@ -5,7 +5,7 @@ from sqlalchemy import func, select
from pydantic import BaseModel from pydantic import BaseModel
from app.core.database import get_db from app.core.database import get_db
from app.core.security import create_token, get_current_membre, is_sysadmin, verify_pin from app.core.security import create_token, get_current_membre, hash_pin, is_sysadmin, verify_pin
from app.services.admin_database import ( from app.services.admin_database import (
assurer_sysadmin_instance, assurer_sysadmin_instance,
creer_groupe, creer_groupe,
@ -48,6 +48,11 @@ class RestoreGroupeRequest(BaseModel):
fichier: str fichier: str
class ChangePinRequest(BaseModel):
pin_actuel: str
nouveau_pin: str
async def require_sysadmin(membre=Depends(get_current_membre)): async def require_sysadmin(membre=Depends(get_current_membre)):
if not is_sysadmin(membre): if not is_sysadmin(membre):
raise HTTPException(status_code=403, detail="Réservé au sysadmin") raise HTTPException(status_code=403, detail="Réservé au sysadmin")
@ -75,6 +80,21 @@ async def sysadmin_login(req: SysadminLoginRequest, db: AsyncSession = Depends(g
raise HTTPException(status_code=401, detail="PIN Sysadmin invalide") raise HTTPException(status_code=401, detail="PIN Sysadmin invalide")
@router.post("/sysadmin-pin")
async def changer_pin_sysadmin(
req: ChangePinRequest,
sysadmin=Depends(require_sysadmin),
):
if getattr(sysadmin, "groupe_id", None) is not None:
raise HTTPException(status_code=403, detail="Réservé au sysadmin d'instance")
if not req.nouveau_pin.isdigit() or len(req.nouveau_pin) != 4:
raise HTTPException(status_code=400, detail="Le nouveau PIN doit contenir 4 chiffres")
if not verify_pin(req.pin_actuel, sysadmin.pin_hash):
raise HTTPException(status_code=401, detail="PIN actuel invalide")
sysadmin.pin_hash = hash_pin(req.nouveau_pin)
return {"message": "PIN sysadmin mis à jour"}
@router.post("/demo") @router.post("/demo")
async def demo_database( async def demo_database(
_sysadmin=Depends(require_sysadmin), _sysadmin=Depends(require_sysadmin),

View file

@ -17,7 +17,7 @@ from app.services.historique import enregistrer_historique, snapshot, verifier_v
router = APIRouter(prefix="/depenses", tags=["S5 — Dépenses"]) router = APIRouter(prefix="/depenses", tags=["S5 — Dépenses"])
METHODES_VALIDES = {"VIREMENT", "CHEQUE", "COLLECTE"} METHODES_VALIDES = {"VIREMENT", "CHEQUE", "COLLECTE"}
UPLOAD_DIR = "/opt/groupe-meditation/uploads/preuves" UPLOAD_DIR = "/opt/app/uploads/preuves"
async def _to_out(d: Depense, db: AsyncSession) -> DepenseOut: async def _to_out(d: Depense, db: AsyncSession) -> DepenseOut:

17
deploy/app.service Normal file
View file

@ -0,0 +1,17 @@
[Unit]
Description=App API
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/app/backend
Environment=PATH=/opt/app/backend/venv/bin
ExecStart=/opt/app/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View file

@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# Sauvegarde quotidienne PostgreSQL — Groupe Méditation # Sauvegarde quotidienne PostgreSQL — App
# À placer dans /etc/cron.daily/ ou via crontab # À placer dans /etc/cron.daily/ ou via crontab
BACKUP_DIR="/var/backups/groupe-meditation" BACKUP_DIR="/var/backups/app"
RETENTION_DAYS=30 RETENTION_DAYS=30
DB_NAME="groupe_meditation" DB_NAME="groupe_meditation"
DB_USER="grpmed" DB_USER="grpmed"
@ -11,9 +11,10 @@ DATE=$(date +%Y-%m-%d_%H%M)
mkdir -p "$BACKUP_DIR" mkdir -p "$BACKUP_DIR"
# Dump # Dump
pg_dump -U "$DB_USER" -h 127.0.0.1 -Fc "$DB_NAME" > "$BACKUP_DIR/grpmed_${DATE}.dump" pg_dump -U "$DB_USER" -h 127.0.0.1 -Fc "$DB_NAME" > "$BACKUP_DIR/app_${DATE}.dump"
# Nettoyage des anciennes sauvegardes # Nettoyage des anciennes sauvegardes
find "$BACKUP_DIR" -name "grpmed_*.dump" -mtime +$RETENTION_DAYS -delete find "$BACKUP_DIR" -name "grpmed_*.dump" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "app_*.dump" -mtime +$RETENTION_DAYS -delete
echo "[$(date)] Sauvegarde terminée: grpmed_${DATE}.dump" echo "[$(date)] Sauvegarde terminée: app_${DATE}.dump"

View file

@ -2,7 +2,7 @@
"""Sauvegardes applicatives par groupe. """Sauvegardes applicatives par groupe.
Usage: Usage:
cd /opt/groupe-meditation/backend cd /opt/app/backend
venv/bin/python ../deploy/group_backup.py all venv/bin/python ../deploy/group_backup.py all
""" """
import asyncio import asyncio

View file

@ -1,17 +0,0 @@
[Unit]
Description=Groupe Méditation API
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/groupe-meditation/backend
Environment=PATH=/opt/groupe-meditation/backend/venv/bin
ExecStart=/opt/groupe-meditation/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View file

@ -1,9 +1,9 @@
# Groupe Méditation — NGINX (seul, sans Apache) # App — NGINX (seul, sans Apache)
# Sert les fichiers statiques + reverse proxy vers FastAPI # Sert les fichiers statiques + reverse proxy vers FastAPI
# #
# Installation : # Installation :
# sudo cp deploy/nginx-groupe-meditation.conf /etc/nginx/sites-available/groupe-meditation # sudo cp deploy/nginx-app.conf /etc/nginx/sites-available/app
# sudo ln -s /etc/nginx/sites-available/groupe-meditation /etc/nginx/sites-enabled/ # sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
# sudo nginx -t && sudo systemctl reload nginx # sudo nginx -t && sudo systemctl reload nginx
# sudo certbot --nginx -d app.87-16.org # sudo certbot --nginx -d app.87-16.org
@ -32,7 +32,7 @@ server {
} }
# ── Frontend — fichiers statiques (React build) ── # ── Frontend — fichiers statiques (React build) ──
root /var/www/groupe-meditation; root /var/www/app;
index index.html; index index.html;
# Cache longue durée sur les assets (JS, CSS, images) # Cache longue durée sur les assets (JS, CSS, images)

View file

@ -1,7 +1,7 @@
""" """
Script d'initialisation. Script d'initialisation.
Crée le schéma et le groupe de démonstration initial. Crée le schéma et le groupe de démonstration initial.
Usage: cd /opt/groupe-meditation/backend && venv/bin/python ../deploy/seed.py Usage: cd /opt/app/backend && venv/bin/python ../deploy/seed.py
""" """
import asyncio import asyncio
import sys import sys

View file

@ -9,7 +9,7 @@ Dernière révision: 2026-06-01
Sur la VM: Sur la VM:
```bash ```bash
systemctl status groupe-meditation systemctl status app
systemctl status nginx systemctl status nginx
``` ```
@ -28,20 +28,20 @@ curl http://127.0.0.1:8000/api/health
Réponse attendue: Réponse attendue:
```json ```json
{"status":"ok","app":"Groupe Méditation","version":"0.1.0"} {"status":"ok","app":"App","version":"0.1.0"}
``` ```
## Redémarrer ## Redémarrer
```bash ```bash
sudo systemctl restart groupe-meditation sudo systemctl restart app
sudo systemctl reload nginx sudo systemctl reload nginx
``` ```
## Lire les journaux système ## Lire les journaux système
```bash ```bash
journalctl -u groupe-meditation -n 200 --no-pager journalctl -u app -n 200 --no-pager
journalctl -u nginx -n 100 --no-pager journalctl -u nginx -n 100 --no-pager
``` ```

View file

@ -105,13 +105,13 @@ ansible-playbook ansible/verify.yml --ask-vault-pass
Vérification directe de l'API: Vérification directe de l'API:
```bash ```bash
ansible groupe-meditation-prod -m uri -a 'url=http://127.0.0.1:8000/api/health return_content=true status_code=200' -o ansible app-prod -m uri -a 'url=http://127.0.0.1:8000/api/health return_content=true status_code=200' -o
``` ```
Vérification des groupes publics: Vérification des groupes publics:
```bash ```bash
ansible groupe-meditation-prod -m uri -a 'url=http://127.0.0.1:8000/api/auth/groupes return_content=true status_code=200' -o ansible app-prod -m uri -a 'url=http://127.0.0.1:8000/api/auth/groupes return_content=true status_code=200' -o
``` ```
## Initialisation de la base ## Initialisation de la base

View file

@ -15,7 +15,7 @@ Le groupe courant est déterminé par le membre authentifié et le `groupe_id` d
Chaque groupe reçoit aussi un schéma PostgreSQL dédié, nommé à partir de son identifiant (`grp_<uuid>`). Les tables du groupe peuvent être rafraîchies dans ce schéma avec le script: Chaque groupe reçoit aussi un schéma PostgreSQL dédié, nommé à partir de son identifiant (`grp_<uuid>`). Les tables du groupe peuvent être rafraîchies dans ce schéma avec le script:
```bash ```bash
cd /opt/groupe-meditation/backend cd /opt/app/backend
venv/bin/python ../deploy/tenant_schemas.py venv/bin/python ../deploy/tenant_schemas.py
``` ```

View file

@ -8,7 +8,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Groupe Méditation</title> <title>App</title>
</head> </head>
<body class="bg-gray-50"> <body class="bg-gray-50">
<div id="root"></div> <div id="root"></div>

View file

@ -1,11 +1,11 @@
{ {
"name": "groupe-meditation", "name": "app",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "groupe-meditation", "name": "app",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"react": "^18.3.1", "react": "^18.3.1",

View file

@ -1,8 +1,8 @@
{ {
"name": "groupe-meditation", "name": "app",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"description": "Application du Groupe Méditation — District 87-16", "description": "App — District 87-16",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View file

@ -4,10 +4,10 @@ self.addEventListener('push', (event) => {
try { try {
data = event.data.json(); data = event.data.json();
} catch { } catch {
data = { title: 'Groupe Méditation', body: event.data.text() }; data = { title: 'App', body: event.data.text() };
} }
} }
const title = data.title || 'Groupe Méditation'; const title = data.title || 'App';
const options = { const options = {
body: data.body || 'Nouvelle notification', body: data.body || 'Nouvelle notification',
icon: '/icon-192.png', icon: '/icon-192.png',

View file

@ -25,7 +25,6 @@ export default function GestionGroupes() {
try { try {
const data = await api.get('/admin/groupes'); const data = await api.get('/admin/groupes');
setGroupes(data); setGroupes(data);
if (!ouvertId && data.length) setOuvertId(data[0].id);
} catch (e) { } catch (e) {
setError(e.message); setError(e.message);
} finally { } finally {
@ -168,6 +167,36 @@ export default function GestionGroupes() {
} }
}; };
const changerPinSysadmin = async () => {
const pinActuel = window.prompt('PIN actuel');
if (!pinActuel) return;
const nouveauPin = window.prompt('Nouveau PIN sysadmin (4 chiffres)');
if (!nouveauPin) return;
if (!/^[0-9]{4}$/.test(nouveauPin)) {
setError('Le nouveau PIN doit contenir 4 chiffres');
return;
}
const confirmation = window.prompt('Confirmer le nouveau PIN');
if (confirmation !== nouveauPin) {
setError('La confirmation ne correspond pas');
return;
}
setBusy(true);
setError('');
setMsg('');
try {
const res = await api.post('/admin/sysadmin-pin', {
pin_actuel: pinActuel,
nouveau_pin: nouveauPin,
});
setMsg(res.message || 'PIN sysadmin mis à jour');
} catch (e) {
setError(e.message);
} finally {
setBusy(false);
}
};
return ( return (
<div className="min-h-screen bg-gray-100"> <div className="min-h-screen bg-gray-100">
<PageHeader titre="Gestion des groupes" sousTitre="Groupes, démo et opérations sysadmin" /> <PageHeader titre="Gestion des groupes" sousTitre="Groupes, démo et opérations sysadmin" />
@ -184,6 +213,13 @@ export default function GestionGroupes() {
> >
Créer un groupe Créer un groupe
</button> </button>
<button
onClick={changerPinSysadmin}
disabled={busy}
className="w-full min-h-[48px] p-3 bg-gray-800 text-white rounded-xl text-sm font-bold disabled:opacity-40"
>
Changer le PIN sysadmin
</button>
</div> </div>
{form && ( {form && (

View file

@ -12,7 +12,7 @@ export default function Login() {
const [courriel, setCourriel] = useState(localStorage.getItem('gm_courriel') || ''); const [courriel, setCourriel] = useState(localStorage.getItem('gm_courriel') || '');
const [pin, setPin] = useState(''); const [pin, setPin] = useState('');
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || ''); const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
const [groupeOuvert, setGroupeOuvert] = useState(localStorage.getItem('gm_groupe_id') || ''); const [groupeOuvert, setGroupeOuvert] = useState('');
const [seSouvenir, setSeSouvenir] = useState(!!localStorage.getItem('gm_courriel')); const [seSouvenir, setSeSouvenir] = useState(!!localStorage.getItem('gm_courriel'));
useEffect(() => { useEffect(() => {
@ -25,7 +25,6 @@ export default function Login() {
if (groupeId && groupeExiste) return; if (groupeId && groupeExiste) return;
const prochainGroupeId = groupes.length === 1 ? groupes[0].id : ''; const prochainGroupeId = groupes.length === 1 ? groupes[0].id : '';
setGroupeId(prochainGroupeId); setGroupeId(prochainGroupeId);
setGroupeOuvert(prochainGroupeId);
if (prochainGroupeId) { if (prochainGroupeId) {
localStorage.setItem('gm_groupe_id', prochainGroupeId); localStorage.setItem('gm_groupe_id', prochainGroupeId);
} else { } else {

View file

@ -1,5 +1,5 @@
/** /**
* Client API — Groupe Méditation * Client API — App
*/ */
const BASE = '/api'; const BASE = '/api';

View file

@ -9,9 +9,9 @@ export default defineConfig({
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['favicon.svg'], includeAssets: ['favicon.svg'],
manifest: { manifest: {
name: 'Groupe Méditation', name: 'App',
short_name: 'Méditation', short_name: 'Méditation',
description: 'Application du Groupe Méditation — District 87-16', description: 'App — District 87-16',
theme_color: '#1a5276', theme_color: '#1a5276',
background_color: '#f8f9fa', background_color: '#f8f9fa',
display: 'standalone', display: 'standalone',