groupe-meditation/backend/app/core/audit.py

96 lines
3.4 KiB
Python

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