"""Routes membres — profil et gestion.""" 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, require_executif, hash_pin, verify_pin from app.models.membre import Membre from app.schemas.schemas import MembrePublic, MembreComplet, MembreUpdate, ChangerPinRequest router = APIRouter(prefix="/membres", tags=["Membres"]) @router.get("/moi", response_model=MembreComplet) async def mon_profil(membre=Depends(get_current_membre)): """C7 — Mon profil.""" return MembreComplet.model_validate(membre) @router.patch("/moi", response_model=MembreComplet) async def modifier_profil( updates: MembreUpdate, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """C7 — Modifier mon profil.""" if updates.prenom is not None: prenom = updates.prenom.strip() if not prenom: raise HTTPException(status_code=400, detail="Le prénom est obligatoire") membre.prenom = prenom.title() if updates.nom is not None: membre.nom = updates.nom.strip().title() if updates.nom.strip() else None if updates.telephone is not None: telephone = updates.telephone.strip() if not telephone: raise HTTPException(status_code=400, detail="Le téléphone est obligatoire") membre.telephone = telephone if updates.courriel is not None: courriel = updates.courriel.strip().lower() if not courriel or "@" not in courriel: raise HTTPException(status_code=400, detail="Courriel invalide") result = await db.execute( select(Membre).where( func.lower(Membre.courriel) == courriel, Membre.groupe_id == membre.groupe_id, Membre.id != membre.id, Membre.actif == True, ) ) if result.scalar_one_or_none(): raise HTTPException(status_code=400, detail="Ce courriel est déjà utilisé dans ce groupe") membre.courriel = courriel if updates.date_abstinence is not None: membre.date_abstinence = updates.date_abstinence await db.flush() return MembreComplet.model_validate(membre) @router.post("/moi/pin") async def changer_pin( req: ChangerPinRequest, membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """C7 — Changer mon PIN.""" if not verify_pin(req.ancien_pin, membre.pin_hash): raise HTTPException(status_code=400, detail="Ancien PIN incorrect") if not req.nouveau_pin.isdigit() or len(req.nouveau_pin) != 4: raise HTTPException(status_code=400, detail="Le PIN doit être composé de 4 chiffres") membre.pin_hash = hash_pin(req.nouveau_pin) await db.flush() return {"message": "PIN modifié avec succès"} @router.get("/actifs", response_model=list[MembrePublic]) async def liste_membres_actifs( membre=Depends(get_current_membre), db: AsyncSession = Depends(get_db), ): """Liste simplifiée des membres actifs (prénom + id) — pour les dropdowns.""" result = await db.execute( select(Membre).where( Membre.groupe_id == membre.groupe_id, Membre.actif == True, ).order_by(Membre.prenom) ) return [MembrePublic.model_validate(m) for m in result.scalars().all()] @router.get("/", response_model=list[MembreComplet]) async def liste_membres( executif=Depends(require_executif), db: AsyncSession = Depends(get_db), ): """G2 — Liste des membres (exécutif seulement).""" result = await db.execute( select(Membre).where( Membre.groupe_id == executif.groupe_id, Membre.actif == True, ).order_by(Membre.prenom) ) return [MembreComplet.model_validate(m) for m in result.scalars().all()] @router.patch("/{membre_id}/desactiver") async def desactiver_membre( membre_id: str, executif=Depends(require_executif), db: AsyncSession = Depends(get_db), ): """G2 — Désactiver un membre.""" result = await db.execute( select(Membre).where(Membre.id == membre_id, Membre.groupe_id == executif.groupe_id) ) membre = result.scalar_one_or_none() if not membre: raise HTTPException(status_code=404, detail="Membre introuvable") membre.actif = False await db.flush() return {"message": f"{membre.prenom} a été désactivé"} @router.delete("/{membre_id}/oubli") async def droit_oubli( membre_id: str, executif=Depends(require_executif), db: AsyncSession = Depends(get_db), ): """G2 — Droit à l'oubli : anonymise les données personnelles.""" result = await db.execute( select(Membre).where(Membre.id == membre_id, Membre.groupe_id == executif.groupe_id) ) membre = result.scalar_one_or_none() if not membre: raise HTTPException(status_code=404, detail="Membre introuvable") if str(membre.id) == str(executif.id): raise HTTPException(status_code=400, detail="Impossible de supprimer votre propre compte") prenom_original = membre.prenom # Anonymiser membre.prenom = "Ancien membre" membre.nom = None membre.telephone = "000-000-0000" membre.courriel = f"supprime_{membre.id}@anon.local" membre.pin_hash = "SUPPRIME" membre.actif = False membre.push_sub = None membre.date_abstinence = None await db.flush() return {"message": f"Données de {prenom_original} anonymisées (droit à l'oubli)"}