É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
+5 -4
View File
@@ -24,11 +24,11 @@ def test_magic_link_unknown_email_is_accepted_without_token(
def test_full_flow_magic_link_then_jwt_then_me(
raw_client, session, edition, make_account, caplog
):
make_account(Role.COORDINATOR, email="coord@example.fr")
make_account(Role.LEAD, email="ref@example.fr")
with caplog.at_level(logging.INFO, logger="app.auth"):
resp = raw_client.post(
_url(edition, "magic-link"), json={"email": "COORD@example.fr"}
_url(edition, "magic-link"), json={"email": "REF@example.fr"}
)
assert resp.status_code == 202
log_line = next(m for m in caplog.messages if "STUB lien magique" in m)
@@ -40,14 +40,15 @@ def test_full_flow_magic_link_then_jwt_then_me(
assert verified.status_code == 200
body = verified.json()
assert body["token_type"] == "bearer"
assert body["account"]["role"] == "COORDINATOR"
assert body["account"]["role"] == "LEAD"
assert body["account"]["auth_method"] == "magic_link"
me = raw_client.get(
"/auth/me",
headers={"Authorization": f"Bearer {body['access_token']}"},
)
assert me.status_code == 200
assert me.json()["email"] == "coord@example.fr"
assert me.json()["email"] == "ref@example.fr"
def test_verify_is_single_use(raw_client, session, edition, make_account):