"""Capture centralisée et expurgée des écritures HTTP.""" import json import re from flask import current_app, request from flask_login import current_user from ..extensions import db from .models.audit import AuditLog SENSITIVE_MARKERS = ("password", "mot_de_passe", "secret", "token", "api_key", "apikey", "pin") def _redacted_payload(): data = request.get_json(silent=True) if request.is_json else request.form.to_dict(flat=False) if not isinstance(data, dict): return None clean = {} for key, value in data.items(): clean[key] = "[MASQUÉ]" if any(marker in key.lower() for marker in SENSITIVE_MARKERS) else value encoded = json.dumps(clean, ensure_ascii=False, default=str) return encoded[:8000] def _entity_from_path(): endpoint = request.endpoint or "" blueprint = endpoint.split(".", 1)[0] or None match = re.search(r"/(\d+)(?:/|$)", request.path) return blueprint, match.group(1) if match else None def record_mutation(response): if request.method in {"GET", "HEAD", "OPTIONS"} or response.status_code >= 500: return response entity_type, entity_id = _entity_from_path() values = { "user_id": current_user.id if current_user.is_authenticated else None, "username": current_user.username if current_user.is_authenticated else "anonyme", "action": request.method, "endpoint": request.endpoint, "path": request.path[:500], "entity_type": entity_type, "entity_id": entity_id, "payload": _redacted_payload(), "status_code": response.status_code, "ip_address": (request.headers.get("X-Forwarded-For", request.remote_addr or "").split(",")[0].strip())[:64], } try: # Transaction séparée : le journal ne valide jamais par accident des # changements laissés en attente par une vue métier. with db.engine.begin() as connection: connection.execute(AuditLog.__table__.insert().values(**values)) except Exception: # Une panne du journal ne doit pas transformer une réponse métier déjà # produite en erreur 500 ; l'erreur reste visible dans les logs serveur. current_app.logger.exception("Impossible d'enregistrer l'opération dans le journal d'audit") return response