159 lines
6 KiB
Python
159 lines
6 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, is_sysadmin
|
|
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:
|
|
raise HTTPException(status_code=400, detail="Identifiant invalide")
|
|
if is_sysadmin(membre) and courriel != "sysadmin":
|
|
raise HTTPException(status_code=400, detail="L'identifiant Sysadmin est protégé")
|
|
if not is_sysadmin(membre) and "@" 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="Cet identifiant 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")
|
|
if is_sysadmin(membre):
|
|
raise HTTPException(status_code=400, detail="Le compte Sysadmin est protégé")
|
|
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")
|
|
if is_sysadmin(membre):
|
|
raise HTTPException(status_code=400, detail="Le compte Sysadmin est protégé")
|
|
|
|
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)"}
|