groupe-meditation/backend/app/routers/auth.py

140 lines
5.2 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
from app.services.admin_database import initialiser_demo
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)):
await initialiser_demo(db)
result = await db.execute(select(Groupe))
groupes = result.scalars().all()
groupes.sort(key=lambda g: (g.nom == "Groupe Démonstration", g.nom.lower()))
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),
)