from __future__ import annotations from typing import Any from sqlalchemy.orm import Session from app.auth.dependencies import ApiPrincipal from app.db.models import AuditLog SENSITIVE_DETAIL_KEYS = { "password", "smtp_password", "imap_password", "secret", "api_key", "token", } def _sanitize_details(value: Any) -> Any: if isinstance(value, dict): sanitized: dict[str, Any] = {} for key, item in value.items(): if str(key).lower() in SENSITIVE_DETAIL_KEYS: sanitized[key] = "" else: sanitized[key] = _sanitize_details(item) return sanitized if isinstance(value, list): return [_sanitize_details(item) for item in value] return value def audit_event( session: Session, *, tenant_id: str | None, action: str, user_id: str | None = None, api_key_id: str | None = None, object_type: str | None = None, object_id: str | None = None, details: dict[str, Any] | None = None, commit: bool = False, ) -> AuditLog: """Persist one audit event. The function deliberately accepts primitive IDs so it can be used from API handlers, CLI commands and worker code without coupling the lower-level services to FastAPI request objects. """ item = AuditLog( tenant_id=tenant_id, user_id=user_id, api_key_id=api_key_id, action=action, object_type=object_type, object_id=object_id, details=_sanitize_details(details or {}), ) session.add(item) if commit: session.commit() else: session.flush() return item def audit_from_principal( session: Session, principal: ApiPrincipal, *, action: str, object_type: str | None = None, object_id: str | None = None, details: dict[str, Any] | None = None, commit: bool = False, ) -> AuditLog: return audit_event( session, tenant_id=principal.tenant_id, user_id=principal.user.id, api_key_id=principal.api_key.id, action=action, object_type=object_type, object_id=object_id, details=details, commit=commit, )