import json from typing import Any from sqlalchemy import select from sqlalchemy.orm import Session from app.core.http.pagination import bounded_limit from app.core.http.request_context import get_request_id from app.modules.audit.constants import AUDIT_REDACTED_VALUE, AUDIT_SENSITIVE_KEYS from app.modules.audit.models import AuditLog from app.modules.audit.schemas import AuditLogCreate def _redact(value: Any) -> Any: if isinstance(value, dict): safe: dict[str, Any] = {} for key, item in value.items(): key_text = str(key) if key_text.lower() in AUDIT_SENSITIVE_KEYS: safe[key_text] = AUDIT_REDACTED_VALUE else: safe[key_text] = _redact(item) return safe if isinstance(value, list): return [_redact(item) for item in value] if isinstance(value, tuple): return [_redact(item) for item in value] return value def _dump(value: Any | None) -> str | None: """Serialize audit payloads while preserving existing strings.""" if value is None: return None if isinstance(value, str): return value return json.dumps(_redact(value), ensure_ascii=False, default=str) class AuditService: """Persist and query audit log entries.""" def __init__(self, db: Session): self.db = db def record(self, payload: AuditLogCreate) -> AuditLog: """Stage an audit record in the caller's transaction.""" record = AuditLog( actor=payload.actor, source=payload.source, action=payload.action, target_type=payload.target_type, target_id=payload.target_id, risk_level=payload.risk_level, request_payload=_dump(payload.request_payload), response_payload=_dump(payload.response_payload), status=payload.status, request_id=payload.request_id or get_request_id(), ) self.db.add(record) self.db.flush() return record def log(self, payload: AuditLogCreate) -> AuditLog: """Persist an audit record as a standalone transaction.""" record = self.record(payload) self.db.commit() self.db.refresh(record) return record def list_logs(self, limit: int = 100) -> list[AuditLog]: stmt = select(AuditLog).order_by(AuditLog.id.desc()).limit(bounded_limit(limit)) return list(self.db.execute(stmt).scalars())