96 lines
3.4 KiB
Python
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
|