"""Audit-Log Helper. Schreibt Eintraege in portal_audit_log mit before/after-JSON. Diff-Helper filtert auf veraenderte Felder, um Speicher zu sparen. """ import json import logging from typing import Optional import aiosqlite from fastapi import Request logger = logging.getLogger("verwaltung.audit") def get_client_ip(request: Optional[Request]) -> str: """IP aus X-Forwarded-For (Nginx) oder direkt aus Connection. Nginx setzt X-Forwarded-For mit der echten Client-IP. Falls nicht vorhanden, fallback auf request.client.host. """ if request is None: return "" xff = request.headers.get("x-forwarded-for") if xff: # Erste IP in der Kette ist die echte Client-IP return xff.split(",")[0].strip() real = request.headers.get("x-real-ip") if real: return real.strip() if request.client: return request.client.host or "" return "" def diff(before: Optional[dict], after: Optional[dict]) -> Optional[dict]: """Filtert auf veraenderte Felder. Bei CREATE/DELETE: voller Datensatz.""" if before is None or after is None: return None changes = {} keys = set(before.keys()) | set(after.keys()) for k in keys: bv = before.get(k) av = after.get(k) if bv != av: changes[k] = {"old": bv, "new": av} return changes if changes else None def _to_json(d: Optional[dict]) -> Optional[str]: if d is None: return None try: return json.dumps(d, default=str, ensure_ascii=False) except Exception as e: logger.warning("audit JSON-Serialisierung fehlgeschlagen: %s", e) return json.dumps({"_error": str(e)}) async def log_action( db: aiosqlite.Connection, admin: Optional[dict], ip: str, action: str, resource_type: Optional[str] = None, resource_id: Optional[int] = None, before: Optional[dict] = None, after: Optional[dict] = None, ) -> None: """Schreibt einen Audit-Eintrag. action: 'create' | 'update' | 'delete' | 'login_success' | 'login_failed' | 'login_blocked' resource_type: 'organization' | 'user' | 'license' | 'source' | 'admin' | 'auth' """ admin_id = admin.get("id") if admin else None admin_username = admin.get("username") if admin else None # Bei UPDATE: nur Diff speichern, sonst vollen Datensatz if action == "update" and before is not None and after is not None: d = diff(before, after) if d is None: # Keine Aenderung -> nicht loggen return before_json = _to_json({k: v["old"] for k, v in d.items()}) after_json = _to_json({k: v["new"] for k, v in d.items()}) else: before_json = _to_json(before) after_json = _to_json(after) try: await db.execute( """INSERT INTO portal_audit_log (admin_id, admin_username, ip, action, resource_type, resource_id, before_json, after_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", (admin_id, admin_username, ip, action, resource_type, resource_id, before_json, after_json), ) await db.commit() except Exception as e: logger.error("audit log_action fehlgeschlagen: %s", e) async def row_to_dict(db: aiosqlite.Connection, table: str, row_id: int) -> Optional[dict]: """Holt Datensatz als Dict (fuer before/after-Snapshot).""" cursor = await db.execute(f"SELECT * FROM {table} WHERE id = ?", (row_id,)) row = await cursor.fetchone() return dict(row) if row else None