"""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), )