from __future__ import annotations from collections.abc import Mapping from flask import current_app, has_request_context, request from sqlalchemy.exc import SQLAlchemyError from app.extensions import db from app.models import Account, AuditLog ANONYMOUS_ACTOR_USERNAME = "anonymous" def account_snapshot(account: Account | None) -> dict[str, object] | None: if account is None: return None return { "id": account.id, "username": account.username, "display_name": account.display_name, "role": account.role, "status": account.status, } def write_audit_log( *, action_type: str, target_type: str, actor: Account | None = None, actor_username: str | None = None, actor_display_name: str | None = None, target_id: int | None = None, target_display_name: str | None = None, before_data: Mapping[str, object] | list[object] | None = None, after_data: Mapping[str, object] | list[object] | None = None, commit: bool = True, ) -> AuditLog | None: log = AuditLog( actor_user_id=actor.id if actor is not None else None, actor_username=actor.username if actor is not None else (actor_username or ANONYMOUS_ACTOR_USERNAME), actor_display_name=actor.display_name if actor is not None else actor_display_name, action_type=action_type, target_type=target_type, target_id=target_id, target_display_name=target_display_name, before_data_json=dict(before_data) if isinstance(before_data, Mapping) else before_data, after_data_json=dict(after_data) if isinstance(after_data, Mapping) else after_data, ) _attach_request_metadata(log) db.session.add(log) if not commit: return log try: db.session.commit() except SQLAlchemyError: db.session.rollback() current_app.logger.exception("Failed to persist audit log for action '%s'.", action_type) return None return log def _attach_request_metadata(log: AuditLog) -> None: if not has_request_context(): return log.request_path = request.path log.request_method = request.method log.ip_address = request.headers.get("X-Forwarded-For", request.remote_addr) user_agent = request.user_agent.string if request.user_agent is not None else None log.user_agent = user_agent[:512] if user_agent else None