from __future__ import annotations import uuid from typing import Sequence from sqlalchemy import select from sqlalchemy import or_ from sqlalchemy.ext.asyncio import AsyncSession from app.core.request_context import get_request_audit_context from app.models.audit_log import AuditLog async def log_action( db: AsyncSession, *, study_id: uuid.UUID | None, entity_type: str, entity_id: uuid.UUID | None, action: str, detail: str | None, operator_id: uuid.UUID, operator_role: str, client_ip: str | None = None, user_agent: str | None = None, client_type: str | None = None, client_version: str | None = None, client_platform: str | None = None, build_channel: str | None = None, build_commit: str | None = None, auto_commit: bool = True, ) -> AuditLog: request_context = get_request_audit_context() log = AuditLog( study_id=study_id, entity_type=entity_type, entity_id=entity_id, action=action, detail=detail, operator_id=operator_id, operator_role=operator_role, client_ip=client_ip if client_ip is not None else (request_context.client_ip if request_context else None), user_agent=user_agent if user_agent is not None else (request_context.user_agent if request_context else None), client_type=client_type if client_type is not None else (request_context.client_type if request_context else None), client_version=client_version if client_version is not None else (request_context.client_version if request_context else None), client_platform=client_platform if client_platform is not None else (request_context.client_platform if request_context else None), build_channel=build_channel if build_channel is not None else (request_context.build_channel if request_context else None), build_commit=build_commit if build_commit is not None else (request_context.build_commit if request_context else None), ) db.add(log) if auto_commit: await db.commit() await db.refresh(log) else: await db.flush() return log async def list_logs( db: AsyncSession, *, study_id: uuid.UUID, entity_type: str | None = None, entity_id: uuid.UUID | None = None, action: str | None = None, operator_id: uuid.UUID | None = None, client_ip: str | None = None, client_type: str | None = None, start_time=None, end_time=None, skip: int = 0, limit: int = 100, ) -> Sequence[AuditLog]: stmt = select(AuditLog).where(AuditLog.study_id == study_id) if entity_type: stmt = stmt.where(AuditLog.entity_type == entity_type) if entity_id: stmt = stmt.where(AuditLog.entity_id == entity_id) if action: stmt = stmt.where(AuditLog.action == action) if operator_id: stmt = stmt.where(AuditLog.operator_id == operator_id) if client_ip: stmt = stmt.where(AuditLog.client_ip.ilike(f"%{client_ip.strip()}%")) if client_type: normalized_client_type = client_type.strip().lower() if normalized_client_type == "unknown": stmt = stmt.where( or_( AuditLog.client_type.is_(None), AuditLog.client_type == "", ~AuditLog.client_type.in_(("web", "desktop")), ) ) else: stmt = stmt.where(AuditLog.client_type == normalized_client_type) if start_time: stmt = stmt.where(AuditLog.created_at >= start_time) if end_time: stmt = stmt.where(AuditLog.created_at <= end_time) stmt = stmt.order_by(AuditLog.created_at.desc()).offset(skip).limit(limit) result = await db.execute(stmt) return result.scalars().all() async def get_log(db: AsyncSession, log_id: uuid.UUID) -> AuditLog | None: result = await db.execute(select(AuditLog).where(AuditLog.id == log_id)) return result.scalar_one_or_none()