"""Journalisation automatique des changements API.""" from jose import JWTError, jwt from sqlalchemy import select from starlette.middleware.base import BaseHTTPMiddleware from app.core.config import settings from app.core.database import async_session from app.core.sanitization import safe_query_params from app.models.journal_action import JournalAction from app.models.membre import Membre from app.services.notifications import notify_group_change MUTATING_METHODS = {"POST", "PUT", "PATCH", "DELETE"} SKIPPED_PREFIXES = ( "/api/auth/connexion", "/api/health", ) def _notification_category(path: str) -> str: mappings = ( (("/api/propositions", "/api/pv"), "gouvernance"), (("/api/collectes", "/api/depenses", "/api/contributions", "/api/banque"), "tresorerie"), (("/api/membres", "/api/invitations"), "membres"), (("/api/postes", "/api/rotations"), "postes"), (("/api/reunions", "/api/presences"), "rencontres"), (("/api/litterature", "/api/ventes-litterature"), "litterature"), (("/api/evenements", "/api/calendrier"), "evenements"), ) for prefixes, category in mappings: if any(path.startswith(prefix) for prefix in prefixes): return category return "systeme" def _token_payload(request): header = request.headers.get("authorization") or "" if not header.lower().startswith("bearer "): return None token = header.split(" ", 1)[1].strip() try: return jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM]) except JWTError: return None class AuditMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): response = await call_next(request) path = request.url.path if ( request.method not in MUTATING_METHODS or not path.startswith("/api/") or any(path.startswith(prefix) for prefix in SKIPPED_PREFIXES) or response.status_code >= 400 ): return response payload = _token_payload(request) if not payload or not payload.get("sub"): return response async with async_session() as db: membre = None result = await db.execute(select(Membre).where(Membre.id == payload["sub"])) membre = result.scalar_one_or_none() if not membre: return response details = { "query": safe_query_params(request.query_params), "impersonification": bool(payload.get("adm")), } db.add(JournalAction( groupe_id=membre.groupe_id, membre_id=membre.id, admin_id=payload.get("adm"), methode=request.method, chemin=path, statut_http=response.status_code, adresse_ip=request.client.host if request.client else None, user_agent=(request.headers.get("user-agent") or "")[:255] or None, details=details, )) await notify_group_change( db, groupe_id=membre.groupe_id, actor_id=membre.id, title="Changement dans l'application", body=f"{membre.prenom} a enregistré une modification.", url="/", category=_notification_category(path), ) await db.commit() return response