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

151 lines
5.5 KiB
Python

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