É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
+12
View File
@@ -13,6 +13,7 @@ from dataclasses import dataclass, field
from sqlmodel import Session, select
from app.auth.passwords import hash_password
from app.db import engine
from app.importing.text import normalize
from app.models import (
@@ -47,6 +48,7 @@ from app.validation.loader import build_validation_input
SEED = 20260709
DEMO_EDITION_NAME = "Festival des Lucioles"
DEMO_COORDINATION_PASSWORD = "coordination-lucioles-2026"
DEMO_YEAR = 2026
START = dt.date(2026, 7, 9)
END = dt.date(2026, 7, 12)
@@ -229,6 +231,15 @@ class Summary:
for code, count in sorted(self.warnings.items()):
lines.append(f" {code} : {count}")
lines.append(f" Violations bloquantes : {self.blocking}")
lines.append("")
lines.append(
" Connexion coordination (mot de passe) : "
"coordination@lucioles.test / " + DEMO_COORDINATION_PASSWORD
)
lines.append(
" Référents et bénévoles : lien magique "
"(referent0..2@lucioles.test, <prénom>.<nom><n>@example.test)"
)
return "\n".join(lines)
@@ -292,6 +303,7 @@ def build(session: Session, *, rng: random.Random | None = None) -> Summary:
edition_id=edition.id,
email="coordination@lucioles.test",
role=Role.COORDINATOR,
password_hash=hash_password(DEMO_COORDINATION_PASSWORD),
)
leads = [
Account(