Files
AdamThomasandClaude Sonnet 5 82088f3b0c Étape 21 : formulaire public d'inscription des bénévoles
Retire « constructeur de formulaire » des exclusions du CLAUDE.md (§ 5) —
décision explicite prise en conversation.

Un formulaire par édition (COORDINATION choisit les champs actifs parmi
neuf : email, téléphone, vœux de poste — max 2, vœux d'artiste — un par
soir de festival, vœu de compagnon en texte libre, disponibilité/
cotisation déclaratives, photo, commentaire ; nom/prénom toujours
collectés) ouvert/fermé à volonté, avec un lien à partager
(/inscription, aucun compte requis).

Backend : FormTemplate/FormField (migration 0013), quatre nouveaux champs
sur Volunteer (photo, companion_wish_note, self_declared_available,
willing_to_pay_fee), app/api/forms.py — les deux seuls endpoints publics
de toute l'API (GET .../form/public, POST .../form/public/submit),
protégés par hCaptcha (app/forms/captcha.py, désactivé sans
HCAPTCHA_SECRET_KEY) et un filet de sécurité global par édition
(form_submission_limiter). resolve_owned_ids/set_artists/
set_wished_positions déplacés vers app/api/_common.py, partagés avec
volunteers.py.

Frontend : écran de configuration (/inscriptions) avec lien copiable et
bascule ouvert/fermé, formulaire public (/inscription, hors RequireAuth)
avec widget hCaptcha chargé dynamiquement, capture photo native
(<input capture>), vœux de poste/artiste avec les mêmes règles que le
back-end. Réponses du formulaire visibles et éditables depuis la fiche
bénévole existante (ConfigVolunteers.tsx).

Vérifié : pytest (273 passés), ruff, alembic check, tsc --noEmit, eslint,
vite build, smoke (rendu SSR de chaque écran + flux de bout en bout du
formulaire en mode démonstration) — tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 20:24:32 +02:00
..

Déploiement

Le pipeline .gitea/workflows/ci-cd.yml (Gitea Actions) se déclenche sur chaque push :

  1. tests (backend : ruff + pytest + alembic check ; frontend : typecheck + lint + build + smoke) ;
  2. sur main seulement : build des images backend et frontend, poussées sur le registre de conteneurs intégré à Gitea ;
  3. copie de deploy/docker-compose.prod.yml sur le VPS par SSH ;
  4. docker compose pull, migration Alembic, up -d.

Nécessite un runner Gitea Actions (act_runner, exécuteur Docker) activé sur l'instance et attaché au dépôt.

Le VPS n'expose rien directement : un Caddy déjà installé sur le serveur (service système, hors de ce dépôt) reçoit le trafic public sur 443 et reverse-proxy vers les deux conteneurs, qui n'écoutent que sur 127.0.0.1.

Avant d'ouvrir au public — check-list sécurité

  • JWT_SECRET : une vraie valeur générée (openssl rand -hex 32), jamais la valeur par défaut de .env.example. Le backend refuse de démarrer en ENVIRONMENT=production sinon (app/config.py:check_production_secrets).
  • EMAIL_ENABLED=true avec un SMTP qui fonctionne (docker compose run --rm backend uv run python -m app.email.check <destinataire> pour vérifier) — sinon les liens magiques sont journalisés en clair au lieu d'être envoyés (le backend avertit au démarrage tant que c'est le cas, voir plus bas).
  • Édition créée via python -m app.bootstrap (étape 3), jamais via python -m app.seed : ce dernier crée un compte de démonstration (coordination@lucioles.test) avec un mot de passe versionné dans le dépôt — un vrai identifiant de connexion COORDINATION public si la base de démo devient la base de production.
  • CORS_ORIGINS : sans objet ici tant que le frontend appelle l'API en même origine via /api/* (voir le vhost Caddy, étape 5) — à ne renseigner que si un autre client (domaine séparé, appli mobile) doit appeler l'API directement.
  • /auth/login et /auth/magic-link sont limités en tentatives par email (5/min et 3/5 min) — voir app/auth/rate_limit.py. Limité par email et non par IP (le VPS ne transmet pas encore l'IP réelle du client à travers Caddy) : suffisant contre la force brute sur un compte connu, pas contre une attaque distribuée sur de nombreux emails différents.

Étape 0 — pousser le dépôt sur Gitea

# Créer d'abord un dépôt vide sur Gitea (sans README ni .gitignore), puis :
git remote add origin git@gitea.mon-domaine.fr:<namespace>/<projet>.git
git push -u origin main

Secrets et variables à définir (Paramètres du dépôt → Actions)

Secrets (Paramètres → Actions → Secrets) :

Secret Description
REGISTRY_TOKEN Jeton d'accès (scope write:package) pour pousser sur le registre de conteneurs de l'instance Gitea
SSH_KEY Clé privée SSH (format OpenSSH)

Variables (Paramètres → Actions → Variables) :

Variable Description
REGISTRY_HOST Hôte du registre (ex. gitea.mon-domaine.fr)
REGISTRY_NAMESPACE Espace du registre (ex. <organisation>/<depot>)
REGISTRY_USER Utilisateur associé à REGISTRY_TOKEN
SSH_HOST Hôte du VPS
SSH_USER Utilisateur SSH
DEPLOY_PATH Dossier du projet sur le VPS (ex. /opt/benevoles)
VITE_EDITION_ID UUID de l'édition de production — à ajouter après l'étape 3
VITE_HCAPTCHA_SITE_KEY Clé de site hCaptcha (publique) — voir étape 1, formulaire public d'inscription. Optionnelle : vide ⇒ formulaire sans vérification anti-robot

La clé publique correspondant à SSH_KEY doit être dans le ~/.ssh/authorized_keys de SSH_USER sur le VPS.

Étape 1 — préparation du VPS (une fois)

mkdir -p /opt/benevoles
cd /opt/benevoles
cat > .env <<'EOF'
POSTGRES_USER=benevoles
POSTGRES_PASSWORD=<mot de passe fort>
POSTGRES_DB=benevoles
JWT_SECRET=<secret fort ; openssl rand -hex 32>
PUBLIC_BASE_URL=https://benevoles.mon-domaine.fr

# Emails (récapitulatifs de planning + liens de connexion). Laisser
# EMAIL_ENABLED=false tant que le SMTP n'est pas prêt : les emails sont alors
# seulement journalisés — un jeton de lien magique en clair dans les logs du
# conteneur permet de se connecter à la place de n'importe qui, donc à
# repasser à true avant l'ouverture au public (le backend affiche un
# avertissement au démarrage tant que c'est le cas).
EMAIL_ENABLED=true
SMTP_HOST=smtp.mon-domaine.fr
SMTP_PORT=587
SMTP_USER=no-reply@mon-domaine.fr
SMTP_PASSWORD=<mot de passe SMTP>
SMTP_STARTTLS=true
SMTP_SSL=false
SMTP_FROM=Festival <no-reply@mon-domaine.fr>

# Formulaire public d'inscription (/inscription). Vide ⇒ aucune vérification
# anti-robot (déconseillé en public) — créer un compte gratuit sur
# https://hcaptcha.com, puis renseigner ici le secret du couple site/secret
# (la clé de site va, elle, dans la variable Gitea VITE_HCAPTCHA_SITE_KEY —
# voir tableau ci-dessus — car figée au build du frontend, pas modifiable ici).
HCAPTCHA_SECRET_KEY=
EOF

Le pipeline dépose docker-compose.prod.yml dans ce dossier et l'utilise tel quel — il n'y a rien d'autre à y copier.

Étape 2 — premier déploiement (backend + base seuls)

Le frontend a besoin de l'UUID d'une édition pour être construit (voir plus bas) : le tout premier déploiement se fait donc en deux temps.

  1. Sur Gitea, lancer le pipeline sur main normalement (Push → Actions, ou un simple commit). Le job build-frontend échouera ou produira une image sans édition tant que VITE_EDITION_ID n'existe pas : c'est attendu, on l'ignore pour l'instant.
  2. Une fois build-backend et deploy passés, db et backend tournent sur le VPS.

Étape 3 — créer l'édition de production

ssh SSH_USER@SSH_HOST
cd /opt/benevoles
docker compose -f docker-compose.prod.yml run --rm backend \
  uv run python -m app.bootstrap \
  --name "Festival 2026" --start 2026-07-01 --end 2026-07-04 \
  --email coordination@mon-domaine.fr --password <mot-de-passe-solide>

La commande affiche l'UUID de l'édition créée. Récupérer aussi celui-ci à tout moment avec :

docker compose -f docker-compose.prod.yml exec -T db \
  psql -U benevoles -tAc "select id from edition order by created_at desc limit 1"

Étape 4 — brancher le frontend sur cette édition

Dans Gitea, ajouter la variable VITE_EDITION_ID (Paramètres → Actions → Variables) avec cet UUID, puis relancer le pipeline sur main (bouton Re-run sur le dernier run, ou un nouveau commit). Cette fois build-frontend construit une image qui pointe sur la bonne édition, et deploy démarre le service frontend.

VITE_EDITION_ID ne change plus ensuite : elle ne se touche à nouveau que pour une nouvelle édition (festival de l'année suivante) qu'on voudrait publier sur ce même domaine.

Étape 5 — le vhost Caddy

Caddy tourne déjà sur ce serveur (service système) : ajouter le bloc deploy/Caddyfile.benevoles à son Caddyfile (ou au fichier qu'il importe pour les sites), avec le vrai nom de domaine, puis :

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Le DNS du domaine doit déjà pointer (enregistrement A/AAAA) vers ce VPS — Caddy obtient et renouvelle seul le certificat HTTPS (Let's Encrypt) au premier accès.

Vérifier ensuite que PUBLIC_BASE_URL dans le .env du VPS (étape 1) correspond bien à https://<ce domaine> — c'est cette valeur qui est utilisée dans les liens des emails envoyés aux bénévoles.

Déploiements suivants

Un simple push sur main suffit : tests → build → déploiement → migration → redémarrage, sans aucune intervention sur le VPS ni sur Caddy.