É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
This commit is contained in:
AdamThomas
2026-09-09 23:22:35 +02:00
co-authored by Claude Sonnet 5
parent c9dda65361
commit ced922f785
24 changed files with 971 additions and 50 deletions
+42 -16
View File
@@ -95,17 +95,40 @@ Conventions techniques :
- Dates/heures **naïves**, heure locale Europe/Paris (`TZ` du conteneur +
`tzdata`). `app.models.local_now()` pour les horodatages.
### Rôles et accès
### 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 à ses
propres postes.
- **`VOLUNTEER`** : accès à ses seules données.
- **`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.
Authentification par lien magique envoyé par email, sans mot de passe. L'envoi
d'email est **stubé** pour l'instant (le lien est écrit dans les logs). 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.
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`.
---
@@ -188,8 +211,10 @@ pointage sur site · fonctions d'IA.
│ │ ├── db.py # engine + session
│ │ ├── logging_config.py # RGPD : pas de PII dans les logs
│ │ ├── api/ # routers + schemas Pydantic + deps
│ │ │ # health, imports, volunteers, positions,
│ │ │ # shifts, assignments, matrix
│ │ │ # 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)
@@ -268,10 +293,11 @@ Aucune dépendance locale requise hors Docker.
| 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`) | à valider |
| 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 terminé. **Reste hors périmètre** (modèle prêt à les
accueillir) : authentification par lien magique (`Account` + `MagicLinkToken`
existent, endpoints non encore protégés), solveur OR-Tools, envoi
d'emails/SMS, constructeur de formulaire, pointage, IA. Le frontend
(React/Vite/TS/Tailwind) reste à démarrer.
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.