from __future__ import annotations import json from datetime import datetime from typing import Any from fastapi import Request from sqlalchemy import select from sqlalchemy.orm import Session from app.modules.core.audit.models import AuditLog from app.modules.core.iam.models import User from app.modules.core.iam.scope import UserScope def _safe_json(value: Any) -> str: return json.dumps(value or {}, ensure_ascii=False, default=_json_default, sort_keys=True) def _json_default(value: Any): if isinstance(value, datetime): return value.isoformat() if hasattr(value, "isoformat"): try: return value.isoformat() except Exception: pass return str(value) def _request_meta(request: Request | None) -> tuple[str | None, str | None]: if not request: return None, None ip = request.client.host if request.client else None user_agent = request.headers.get("user-agent") return ip, user_agent def write_audit_log( db: Session, *, action: str, entity_type: str, actor: User | None = None, request: Request | None = None, entity_id: str | int | None = None, entity_name: str | None = None, status: str = "success", target_tenant_id: int | None = None, target_branch_id: int | None = None, details: dict[str, Any] | None = None, actor_email: str | None = None, ) -> AuditLog: ip_address, user_agent = _request_meta(request) log = AuditLog( actor_user_id=actor.id if actor else None, actor_email=(actor.email if actor else actor_email), actor_tenant_id=(actor.tenant_id if actor else None), actor_branch_id=(actor.branch_id if actor else None), action=action, entity_type=entity_type, entity_id=str(entity_id) if entity_id is not None else None, entity_name=entity_name, status=status, target_tenant_id=target_tenant_id, target_branch_id=target_branch_id, ip_address=ip_address, user_agent=user_agent, details_json=_safe_json(details), ) db.add(log) db.commit() db.refresh(log) return log def model_snapshot(obj: Any, fields: list[str]) -> dict[str, Any]: return {field: getattr(obj, field, None) for field in fields} def pair_before_after(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]: return {"before": before, "after": after} def list_audit_logs(db: Session, scope: UserScope, limit: int = 200) -> list[AuditLog]: q = select(AuditLog) if not scope.is_system_admin: q = q.where(AuditLog.target_tenant_id == scope.actor.tenant_id) if scope.branch_scoped: q = q.where(AuditLog.target_branch_id == scope.actor.branch_id) return db.execute(q.order_by(AuditLog.created_at_utc.desc(), AuditLog.id.desc()).limit(limit)).scalars().all() def parse_details(log: AuditLog) -> dict[str, Any]: try: return json.loads(log.details_json or "{}") except Exception: return {"raw": log.details_json} def search_audit_logs(db: Session, scope: UserScope, q: str | None = None) -> list[AuditLog]: rows = list_audit_logs(db, scope, limit=1000) query = (q or "").strip().lower() if not query: return rows result = [] for row in rows: hay = " ".join([str(row.action or ""), str(row.entity_type or ""), str(row.entity_name or ""), str(row.actor_email or ""), str(row.details_json or "")]).lower() if query in hay: result.append(row) return result