groupe-meditation/backend/app/routers/auth.py
2026-03-04 14:01:00 -05:00

107 lines
3.8 KiB
Python

"""Routes d'authentification."""
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.core.database import get_db
from app.core.security import hash_pin, verify_pin, create_token
from app.models.membre import Membre
from app.models.groupe import Groupe
from app.models.invitation import Invitation
from app.schemas.schemas import ConnexionRequest, InscriptionRequest, TokenResponse, MembrePublic
router = APIRouter(prefix="/auth", tags=["Authentification"])
@router.get("/health")
async def health(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Groupe).limit(1))
groupe = result.scalar_one_or_none()
return {
"status": "ok",
"groupe": groupe.nom if groupe else "Aucun",
"district": groupe.district if groupe else "-",
}
@router.get("/groupes")
async def liste_groupes(db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Groupe).order_by(Groupe.nom))
groupes = result.scalars().all()
return [{"id": str(g.id), "nom": g.nom, "district": g.district} for g in groupes]
@router.post("/connexion", response_model=TokenResponse)
async def connexion(req: ConnexionRequest, db: AsyncSession = Depends(get_db)):
# Chercher le membre par prénom + groupe (insensible à la casse)
result = await db.execute(
select(Membre).where(
func.lower(Membre.prenom) == func.lower(req.prenom),
Membre.groupe_id == req.groupe_id,
Membre.actif == True,
)
)
membre = result.scalar_one_or_none()
if not membre or not verify_pin(req.pin, membre.pin_hash):
raise HTTPException(status_code=401, detail="Prénom ou PIN incorrect")
token = create_token(str(membre.id), str(membre.groupe_id))
return TokenResponse(
access_token=token,
membre=MembrePublic.model_validate(membre),
)
@router.post("/inscription", response_model=TokenResponse)
async def inscription(req: InscriptionRequest, db: AsyncSession = Depends(get_db)):
# Valider le code d'invitation
result = await db.execute(
select(Invitation).where(
Invitation.code == req.code_invitation,
Invitation.utilise == False,
)
)
invitation = result.scalar_one_or_none()
if not invitation:
raise HTTPException(status_code=400, detail="Code d'invitation invalide")
if invitation.expire_le < datetime.now(timezone.utc):
raise HTTPException(status_code=400, detail="Code d'invitation expiré")
# Vérifier unicité du prénom dans le groupe
exists = await db.execute(
select(Membre).where(
func.lower(Membre.prenom) == func.lower(req.prenom),
Membre.groupe_id == invitation.groupe_id,
Membre.actif == True,
)
)
if exists.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Ce prénom est déjà utilisé dans ce groupe")
# Valider le PIN (4 chiffres)
if not req.pin.isdigit() or len(req.pin) != 4:
raise HTTPException(status_code=400, detail="Le PIN doit être composé de 4 chiffres")
# Créer le membre
membre = Membre(
groupe_id=invitation.groupe_id,
prenom=req.prenom.strip().title(),
nom=req.nom.strip().title(),
telephone=req.telephone,
courriel=req.courriel,
date_abstinence=req.date_abstinence,
pin_hash=hash_pin(req.pin),
)
db.add(membre)
# Marquer l'invitation utilisée
invitation.utilise = True
invitation.utilise_par = membre.id
await db.flush()
token = create_token(str(membre.id), str(membre.groupe_id))
return TokenResponse(
access_token=token,
membre=MembrePublic.model_validate(membre),
)