Étape 8 : connexion par mot de passe pour la coordination

Deux systèmes d'authentification, une même session JWT :
- COORDINATION : login e-mail + mot de passe (bcrypt), POST .../auth/login ;
  POST .../auth/change-password pour le compte courant
- RÉFÉRENTS / BÉNÉVOLES : lien magique inchangé, mais refusé pour un compte
  qui a un mot de passe

- Account.password_hash (nullable) + migration 0002 ; propriété auth_method
  ("password" / "magic_link") exposée dans AccountRead
- app/auth/passwords.py : hash/verify bcrypt, longueur mini 10
- Création de compte : password requis pour COORDINATOR, interdit sinon
- seed : la coordination de démo a un mot de passe, affiché dans le résumé
- 12 tests (login ok/ko, séparation des deux systèmes, change-password,
  validation à la création) ; 129 tests au total ; alembic check vert

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:42:31 +02:00
co-authored by Claude Sonnet 5
parent 1f9f460540
commit 8a879ca5c3
14 changed files with 446 additions and 41 deletions
+27 -13
View File
@@ -115,20 +115,33 @@ Matrice appliquée par les endpoints :
| `assignments` lecture | ✔ | ✔ | les siennes |
| `matrix`, `import`, `accounts` | ✔ / ✔ / ✔ | ✔ / — / — | — |
**Authentification** : lien magique + session JWT (`Bearer`), sans mot de passe.
**Authentification — deux systèmes selon le compte**, une même session JWT
HS256 (`Authorization: Bearer <jwt>`, TTL `session_ttl_hours`) à l'arrivée.
Le compte porte `password_hash` : défini ⇒ mot de passe, `NULL` ⇒ lien magique
(`Account.auth_method` = `"password"` / `"magic_link"`).
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>`.
*Mot de passe* — comptes `COORDINATOR` :
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`.
1. `POST /editions/{id}/auth/login` `{email, password}` → JWT + `AccountRead`.
Échec = `401 auth.invalid_credentials` (aucune divulgation).
2. `POST /editions/{id}/auth/change-password` `{current_password, new_password}`
(compte courant). Hachage **bcrypt**, longueur mini `MIN_LENGTH` (10).
*Lien magique* — comptes `LEAD` / `VOLUNTEER` (sans mot de passe) :
3. `POST /editions/{id}/auth/magic-link` `{email}` → `202` **constant**. Si un
compte **sans mot de passe** correspond, un `MagicLinkToken` est créé
(jeton aléatoire, **haché SHA-256** en base, TTL `magic_link_ttl_minutes`).
4. **Envoi d'email stubé** : le lien est écrit dans les logs (`app.auth`, INFO,
`jeton=…` — identifiants seulement, aucune donnée nominative).
5. `POST /editions/{id}/auth/verify` `{token}` → JWT + `AccountRead`. Jeton à
usage unique.
`GET /auth/me` → compte courant.
La coordination crée les comptes via `POST /editions/{id}/accounts` : un
`COORDINATOR` **exige** un `password` (≥ 10) ; un `LEAD` / `VOLUNTEER` le
**refuse** (lien magique). Un bénévole sans email n'a pas de `Account`.
---
@@ -294,7 +307,8 @@ Aucune dépendance locale requise hors Docker.
| 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 |
| 7 | Authentification : lien magique + JWT + application des rôles | validée |
| 8 | Second système : connexion mot de passe (bcrypt) pour la coordination | à 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