groupe-meditation/backend/app/routers/auth.py
2026-05-25 17:39:38 -04:00

137 lines
5 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 get_current_membre, hash_pin, verify_pin, create_token, is_sysadmin
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,
"adresse_local": g.adresse_local,
"jour_reunion": g.jour_reunion,
"heure_reunion": g.heure_reunion.strftime("%H:%M") if g.heure_reunion else None,
"assemblee_affaires_ordre": g.assemblee_affaires_ordre,
"format_reunion": g.format_reunion,
} for g in groupes]
@router.post("/connexion", response_model=TokenResponse)
async def connexion(req: ConnexionRequest, db: AsyncSession = Depends(get_db)):
# Chercher le membre par courriel + groupe (insensible à la casse)
result = await db.execute(
select(Membre).where(
func.lower(Membre.courriel) == func.lower(req.courriel),
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="Courriel ou PIN incorrect")
# Token plus long si "se souvenir de moi"
expire_jours = 90 if req.se_souvenir else 30
token = create_token(str(membre.id), str(membre.groupe_id), expire_jours=expire_jours)
return TokenResponse(
access_token=token,
membre=MembrePublic.model_validate(membre),
)
@router.post("/impersoner/{membre_id}", response_model=TokenResponse)
async def impersoner_membre(
membre_id: str,
admin=Depends(get_current_membre),
db: AsyncSession = Depends(get_db),
):
if not is_sysadmin(admin):
raise HTTPException(status_code=403, detail="Réservé au sysadmin")
cible = await db.get(Membre, membre_id)
if not cible or cible.groupe_id != admin.groupe_id or not cible.actif:
raise HTTPException(status_code=404, detail="Membre introuvable ou inactif")
setattr(cible, "_sysadmin_session", True)
token = create_token(str(cible.id), str(cible.groupe_id), admin_id=str(admin.id))
return TokenResponse(
access_token=token,
membre=MembrePublic.model_validate(cible),
)
@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 courriel dans le groupe
exists = await db.execute(
select(Membre).where(
func.lower(Membre.courriel) == func.lower(req.courriel),
Membre.groupe_id == invitation.groupe_id,
Membre.actif == True,
)
)
if exists.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Ce courriel 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() if req.nom else None,
telephone=req.telephone.strip(),
courriel=req.courriel.strip().lower(),
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),
)