Files
benevoles/CLAUDE.md
T
AdamThomasandClaude Sonnet 5 ced922f785 Étape 7 : authentification par lien magique + JWT + rôles
- app/auth/ : jetons (lien magique aléatoire haché SHA-256, JWT HS256),
  dépendances get_current_account / current_account_in_edition / require_roles
  + require_coordinator / require_staff, et ensure_can_write_position/_shift
- Endpoints app/api/auth.py : POST .../auth/magic-link (202 constant, pas de
  divulgation), POST .../auth/verify (JWT, jeton à usage unique), GET /auth/me
- Envoi d'email stubé : le lien est journalisé (logger app.auth, identifiants
  seulement, aucune donnée nominative)
- app/api/accounts.py : CRUD des comptes réservé à la COORDINATION
- Rôles appliqués sur toute l'API :
  COORDINATION accès complet ; RÉFÉRENT lecture globale + écriture sur ses
  seuls postes/créneaux/affectations ; BÉNÉVOLE sa fiche et ses affectations,
  aucune écriture
- imports.py aligné sur la dépendance get_edition (404 avant 403)
- Secret JWT porté à >= 32 octets (config, .env.example, docker-compose)
- seed : crée un compte VOLUNTEER pour chaque bénévole avec email (104 comptes)
- conftest : fixtures make_account / coordinator / as_account / raw_client ;
  la suite existante s'exécute en COORDINATION
- 13 tests d'authentification + de rôles ; 119 tests au total ; alembic check
  toujours vert (Account / MagicLinkToken déjà dans la migration 0001)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-09 23:22:35 +02:00

304 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — Gestion des bénévoles
Application web de gestion des bénévoles pour un festival associatif (50–200
bénévoles, 4 jours montage/démontage inclus). Interface en français, code en
anglais.
Ce fichier est le contrat de travail. Il est tenu à jour à chaque étape.
---
## 1. Vocabulaire du domaine (glossaire FR ↔ EN)
Le métier parle français, le code et la base parlent anglais. Toute entité,
colonne, route et variable utilise la colonne « Code / base ».
| Métier (brief) | Code / base |
|------------------------------------|-----------------------------------------------|
| Édition | `Edition` |
| Bénévole | `Volunteer` |
| Compétence | `Skill` |
| Poste | `Position` |
| Référent d'un poste | `Position.lead_account_id` → `Account` |
| Créneau | `Shift` |
| Disponibilité | `Availability` |
| Affectation | `Assignment` |
| Compte (utilisateur connecté) | `Account` |
| Découpage de journée (paramétrable)| `DayPart` (`code`, `label`, `start_time`, `end_time`, `order`) |
| Lien magique | `MagicLinkToken` |
Énumérations :
| Métier | Code |
|------------------------------------|-----------------------------------------------|
| Statut d'affectation | `PROPOSED` / `CONFIRMED` / `DECLINED` / `CANCELLED` |
| Origine d'affectation | `SOLVER` / `MANUAL` |
| Verrouillée | `Assignment.locked: bool` |
| Rôle | `COORDINATOR` / `LEAD` / `VOLUNTEER` |
| Disponibilité (tri-état) | `AVAILABLE` / `IF_NEEDED` / `UNAVAILABLE` |
| Catégorie de poste | `BAR` / `TICKETING` / `WELCOME` / `PARKING` / `CLEANING` / `CATERING` / `SETUP` / `STAGE` / `OTHER` |
### Distinction critique (ne jamais confondre, ni en base ni dans l'UI)
- **`Availability`** = une *déclaration* du bénévole : « je peux venir tel
jour, telle demi-journée ».
- **`Assignment`** = une *décision* de l'organisation : « tu tiens le bar
samedi 18 h – 22 h ».
Deux tables distinctes, deux écrans distincts, deux vocabulaires distincts.
---
## 2. Modèle de domaine
Défini dans `backend/app/models/` (`enums.py`, `base.py`, `domain.py`).
Clés primaires `UUID`. Champs d'audit `created_at` / `updated_at` sur toutes
les tables sauf les tables d'association.
- **`Edition`** : `year`, `name` (opt.), `start_date`, `end_date`,
`retention_days` (durée de rétention des données nominatives en jours,
`NULL` = pas de purge).
- **`DayPart`** : découpage d'une journée propre à l'édition (nombre et bornes
libres, paramétrable par la coordination). `code`, `label`, `start_time`,
`end_time` (`end_time < start_time` ⇒ passe minuit), `order`. Unique
(`edition_id`, `code`).
- **`Volunteer`** : `email` (unique **par édition**, nullable pour les
bénévoles créés à la main — plusieurs `NULL` autorisés), `first_name`,
`last_name`, `phone`, `tshirt_size`, `diet`, `comment`. Pas de date de
naissance.
- **`Skill`** : référentiel par édition (`code`, `label`). N-N `Volunteer`
(`volunteer_skill`). N-N `Shift` (`shift_required_skill`).
- **`Position`** : `name`, `location`, `category` (`PositionCategory`),
`lead_account_id` (→ `Account`, `SET NULL` à la suppression du compte).
- **`Shift`** : `position_id`, `starts_at`, `ends_at` (peut finir le
lendemain), `min_staff`, `ideal_staff`, compétences requises (N-N `Skill`).
- **`Availability`** : `volunteer_id`, `date`, `day_part_id`, `status`
(`AvailabilityStatus`). Unique (`volunteer_id`, `date`, `day_part_id`).
- **`Assignment`** : `volunteer_id`, `shift_id`, `status`
(`AssignmentStatus`, défaut `PROPOSED`), `source` (`AssignmentSource`,
défaut `MANUAL`), `locked` (défaut `false`). Unique (`volunteer_id`,
`shift_id`).
- **`Account`** : `email` (unique par édition), `role` (`Role`), `edition_id`,
`volunteer_id` (nullable, unique).
- **`MagicLinkToken`** : `account_id`, `token_hash` (unique), `expires_at`,
`used_at`.
Conventions techniques :
- Multi-édition : toute entité métier porte un `edition_id`.
- Énumérations stockées en `VARCHAR` + `CHECK` (`native_enum=False`), pas de
type ENUM natif PostgreSQL.
- Suppression en cascade au niveau base (`ON DELETE CASCADE`) sur tous les
`edition_id` et sur les liens vers `volunteer`, `shift`, `position`,
`skill`, `account`. La suppression complète d'un bénévole (RGPD) efface donc
ses `Availability`, `Assignment`, liens de compétence et son `Account`.
- Dates/heures **naïves**, heure locale Europe/Paris (`TZ` du conteneur +
`tzdata`). `app.models.local_now()` pour les horodatages.
### Rôles et accès (`app/auth/`)
- **`COORDINATOR`** : accès complet, construit et publie les plannings.
- **`LEAD`** (référent) : lecture sur toute l'édition, écriture limitée aux
postes dont il est `lead_account_id` (et à leurs créneaux et affectations).
- **`VOLUNTEER`** : lecture de sa seule fiche et de ses seules affectations ;
aucune écriture.
Matrice appliquée par les endpoints :
| Ressource | COORDINATOR | LEAD | VOLUNTEER |
|-----------|-------------|------|-----------|
| `volunteers` liste/écriture | ✔ | liste seule | — |
| `volunteers/{id}` lecture | ✔ | ✔ | soi-même |
| `positions`/`shifts` lecture | ✔ | ✔ | — |
| `positions`/`shifts` écriture | ✔ | ses postes | — |
| `assignments` écriture | ✔ | ses postes | — |
| `assignments` lecture | ✔ | ✔ | les siennes |
| `matrix`, `import`, `accounts` | ✔ / ✔ / ✔ | ✔ / — / — | — |
**Authentification** : lien magique + session JWT (`Bearer`), sans mot de passe.
1. `POST /editions/{id}/auth/magic-link` `{email}` → réponse `202` identique
qu'un compte existe ou non. Si oui, un `MagicLinkToken` est créé (jeton
aléatoire, **haché** en base, TTL `magic_link_ttl_minutes`).
2. **Envoi d'email stubé** : le lien est écrit dans les logs (`app.auth`,
niveau INFO, `jeton=…` — identifiants seulement, aucune donnée nominative).
3. `POST /editions/{id}/auth/verify` `{token}` → JWT HS256
(`session_ttl_hours`) + `AccountRead`. Jeton à usage unique.
4. `GET /auth/me` → compte courant. En-tête `Authorization: Bearer <jwt>`.
Un bénévole sans email n'a pas de `Account` et ne se connecte pas ; il est géré
entièrement par la coordination. La coordination crée les comptes via
`POST /editions/{id}/accounts`.
---
## 3. Moteur de validation des affectations — cœur du produit
Module isolé `backend/app/validation/`. **Tests écrits avant l'implémentation**,
un cas passant + un cas déclenchant par règle.
Retourne une liste de violations typées `BLOCKING` ou `WARNING`. Chaque
violation est un **code** + des paramètres structurés, jamais une phrase (la
traduction se fait via `locales/fr.json`).
| Code | Sévérité | Règle |
|---------------------------|------------|-------------------------------------------------------------|
| `SHIFT_OVERLAP` | `BLOCKING` | Deux créneaux d'un même bénévole se chevauchent. |
| `OUTSIDE_AVAILABILITY` | `WARNING` | Un `DayPart` couvert par le créneau est `UNAVAILABLE` ou sans déclaration. |
| `DAILY_HOURS_EXCEEDED` | `WARNING` | > 8 h 00 d'affectations cumulées sur une même journée. |
| `INSUFFICIENT_REST` | `WARNING` | < 8 h 00 entre la fin d'un créneau et le début du suivant. |
| `MISSING_REQUIRED_SKILL` | `WARNING` | Le bénévole n'a pas une compétence requise par le créneau. |
Sémantique figée :
- Statuts pris en compte pour chevauchement / cumul / repos :
`PROPOSED` + `CONFIRMED`. `DECLINED` et `CANCELLED` sont ignorés.
- « Journée » (cumul 8 h) : du début du premier `DayPart` du jour au début du
premier `DayPart` du lendemain.
- Cumul : `> 8 h 00` strict (8 h 00 pile passe).
- Repos : `< 8 h 00` strict.
- Créneaux jointifs (`end_at == start_at`) : **pas** un chevauchement.
- Une violation `BLOCKING` empêche l'enregistrement, sans dérogation.
- Une violation `WARNING` est retournée à l'appelant mais n'empêche rien
(l'équipe connaît des exceptions légitimes).
---
## 4. Périmètre
### Premier incrément (en cours)
1. Modèle de données complet + migrations Alembic.
2. Import CSV/Excel (un seul fichier bénévoles + disponibilités), en deux
temps : analyse (colonnes détectées, aperçu) puis exécution avec mapping
validé. Rapport ligne par ligne. Détection des doublons : upsert sur
`email` ; sans email, détection sur (`last_name`, `first_name`) signalée
sans upsert automatique. Mode simulation qui ne persiste rien.
3. API CRUD sur `Position`, `Shift`, `Volunteer`, `Assignment`.
4. Moteur de validation (section 3).
5. Endpoint matrice d'une édition :
`[{ position, shifts: [{ shift, min_staff, ideal_staff, confirmed, proposed }] }]`.
6. Jeu de données de démonstration : 120 bénévoles, 15 postes, 4 jours,
quelques trous d'effectif, 2–3 avertissements actifs.
### Hors périmètre (le modèle doit juste pouvoir les accueillir sans refonte)
Solveur OR-Tools · envoi d'emails / SMS · constructeur de formulaire ·
pointage sur site · fonctions d'IA.
---
## 5. Arborescence
```
.
├── CLAUDE.md
├── README.md
├── docker-compose.yml # dev : db + backend (hot reload)
├── .env.example
├── .github/workflows/deploy.yml # CI/CD : build → GHCR → SSH sur le VPS
├── deploy/
│ ├── docker-compose.prod.yml
│ └── README.md # secrets requis, setup VPS
├── docker/postgres-init/ # création de la base de test au 1er boot
├── backend/
│ ├── pyproject.toml # géré par uv
│ ├── Dockerfile
│ ├── alembic.ini
│ ├── app/
│ │ ├── main.py # instance FastAPI
│ │ ├── config.py # settings (pydantic-settings)
│ │ ├── db.py # engine + session
│ │ ├── logging_config.py # RGPD : pas de PII dans les logs
│ │ ├── api/ # routers + schemas Pydantic + deps
│ │ │ # health, auth, accounts, imports,
│ │ │ # volunteers, positions, shifts,
│ │ │ # assignments, matrix
│ │ ├── auth/ # lien magique + JWT + dépendances de rôle
│ │ ├── importing/ # import CSV/Excel (parser, values, service)
│ │ ├── models/ # tables SQLModel
│ │ ├── validation/ # moteur pur (models, engine) + loader (base)
│ │ ├── seed.py # jeu de démonstration (python -m app.seed)
│ │ ├── i18n/
│ │ │ ├── __init__.py # t(key, **params)
│ │ │ └── locales/fr.json
│ │ └── migrations/ # Alembic (env.py, versions/)
│ └── tests/
│ ├── conftest.py # vraie base de test PostgreSQL
│ └── ...
└── frontend/ # React + Vite + TS + Tailwind (étape ultérieure)
```
---
## 6. Commandes
Aucune dépendance locale requise hors Docker.
| Action | Commande |
|-------------------------------|-------------------------------------------------------------------------|
| Lancer en dev | `docker compose up --build` |
| API + docs interactives | http://localhost:8000/docs |
| Tests backend | `docker compose run --rm backend uv run pytest` |
| Couverture | `docker compose run --rm backend uv run pytest --cov=app` |
| Lint | `docker compose run --rm backend uv run ruff check .` |
| Format | `docker compose run --rm backend uv run ruff format .` |
| Créer une migration | `docker compose run --rm backend uv run alembic revision --autogenerate -m "..."` |
| Appliquer les migrations | `docker compose run --rm backend uv run alembic upgrade head` |
| Vérifier migration ↔ modèle | `docker compose run --rm backend uv run alembic check` (après `upgrade head`) |
| Charger le jeu de démo | `docker compose run --rm backend uv run python -m app.seed` (après `alembic upgrade head`) |
---
## 7. Conventions
- **Code, base, noms de fichiers en anglais.** Le français vit dans les
fichiers de traduction et les commits.
- **Aucun texte visible par l'utilisateur en dur.** Backend : `app.i18n.t(key)`
+ `locales/fr.json`. Frontend : fichier i18n dédié. Vrai même en monolingue.
- **RGPD :**
- Pas de log des noms, emails, téléphones. On logue des identifiants.
- `Edition.retention_days` : durée de rétention paramétrable.
- Endpoint de suppression complète d'un bénévole : *hard delete* en cascade.
- **Dates et heures :** stockées naïves, interprétées en heure locale
Europe/Paris (heure murale). Les règles « 8 h », « journée », « demi-journée »
sont toutes en heure locale.
- **Validation TDD :** tests avant implémentation, un cas passant + un cas
déclenchant par règle.
- **Git :** un commit atomique par étape validée, message en français.
- **Étapes courtes :** après chaque étape, montrer le résultat et attendre la
validation avant de continuer.
---
## 8. Déploiement
- Dépôt GitHub. VPS avec Docker.
- `.github/workflows/deploy.yml` : sur chaque push `main` → build de l'image
backend → push sur `ghcr.io/<owner>/<repo>-backend` → SSH sur le VPS →
`docker compose -f docker-compose.prod.yml pull && up -d`.
- Secrets requis (voir `deploy/README.md`) : `SSH_HOST`, `SSH_USER`,
`SSH_KEY`, `DEPLOY_PATH`, `GHCR_TOKEN`.
- Le VPS porte un fichier `.env` (non versionné) avec `POSTGRES_USER`,
`POSTGRES_PASSWORD`, `POSTGRES_DB`, `JWT_SECRET`.
---
## 9. Journal des étapes
| Étape | Description | État |
|-------|-------------------------------------------------------|----------|
| 1 | Squelette : monorepo, Docker, CI/CD, backend qui démarre | validée |
| 2 | Modèle de données complet + migration Alembic initiale | validée |
| 3 | Import CSV/Excel (analyse + exécution, rapport, doublons, simulation) | validée |
| 4 | Moteur de validation des affectations (pur, TDD, 5 règles) | validée |
| 5 | API CRUD (Position, Shift, Volunteer, Assignment) + matrice + validation branchée | validée |
| 6 | Jeu de données de démonstration (`app/seed.py`) | validée |
| 7 | Authentification : lien magique + JWT + application des rôles | à valider |
Premier incrément backend + authentification terminés. **Reste hors
périmètre** (modèle prêt à les accueillir) : envoi réel d'emails/SMS (le lien
magique est journalisé, pas envoyé), solveur OR-Tools, constructeur de
formulaire, pointage sur site, IA. Le frontend (React/Vite/TS/Tailwind) reste
à démarrer.