"""Gestion des comptes par la coordination (créer un référent, un bénévole connecté…).""" import uuid from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlalchemy.exc import IntegrityError from sqlmodel import Session, select from app.api._common import owned_or_404 from app.api.deps import get_edition from app.api.schemas import AccountCreate, AccountRead, AccountUpdate from app.auth.deps import require_coordinator from app.auth.passwords import MIN_LENGTH, hash_password from app.db import get_session from app.models import Account, Edition, Role, Volunteer router = APIRouter( prefix="/editions/{edition_id}/accounts", tags=["accounts"] ) @router.post("", response_model=AccountRead, status_code=status.HTTP_201_CREATED) def create_account( payload: AccountCreate, edition: Edition = Depends(get_edition), _guard: Account = Depends(require_coordinator), session: Session = Depends(get_session), ) -> Account: if payload.volunteer_id is not None: volunteer = session.get(Volunteer, payload.volunteer_id) if volunteer is None or volunteer.edition_id != edition.id: raise HTTPException( status_code=422, detail={"code": "account.volunteer_wrong_edition"} ) # Deux systèmes de connexion : mot de passe pour la COORDINATION, # lien magique pour les autres. if payload.role is Role.COORDINATOR: if not payload.password or len(payload.password) < MIN_LENGTH: raise HTTPException( status_code=422, detail={"code": "account.password_required", "min": MIN_LENGTH}, ) password_hash = hash_password(payload.password) else: if payload.password is not None: raise HTTPException( status_code=422, detail={"code": "account.password_not_allowed"}, ) password_hash = None account = Account( edition_id=edition.id, email=payload.email.strip(), role=payload.role, volunteer_id=payload.volunteer_id, password_hash=password_hash, ) session.add(account) try: session.commit() except IntegrityError as exc: session.rollback() raise HTTPException( status_code=409, detail={"code": "account.duplicate"} ) from exc session.refresh(account) return account @router.get("", response_model=list[AccountRead]) def list_accounts( edition: Edition = Depends(get_edition), _guard: Account = Depends(require_coordinator), session: Session = Depends(get_session), ) -> list[Account]: return list( session.exec( select(Account) .where(Account.edition_id == edition.id) .order_by(Account.email) ) ) @router.patch("/{account_id}", response_model=AccountRead) def update_account( account_id: uuid.UUID, payload: AccountUpdate, edition: Edition = Depends(get_edition), _guard: Account = Depends(require_coordinator), session: Session = Depends(get_session), ) -> Account: """Seul le rôle change de main ici, et seulement entre LEAD et VOLUNTEER (lien magique dans les deux cas) : devenir référent d'un poste (voir `positions.py`) met à niveau le compte bénévole existant, sans toucher au mot de passe d'un compte COORDINATION.""" account = owned_or_404(session.get(Account, account_id), edition.id) if payload.role is not None and payload.role != account.role: if Role.COORDINATOR in (account.role, payload.role): raise HTTPException( status_code=422, detail={"code": "account.role_change_not_allowed"} ) account.role = payload.role session.commit() session.refresh(account) return account @router.delete("/{account_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_account( account_id: uuid.UUID, edition: Edition = Depends(get_edition), _guard: Account = Depends(require_coordinator), session: Session = Depends(get_session), ) -> Response: account = owned_or_404(session.get(Account, account_id), edition.id) session.delete(account) session.commit() return Response(status_code=status.HTTP_204_NO_CONTENT)