import json import uuid from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_operator_role_label, get_current_user, get_db_session, require_api_permission from app.crud import audit as audit_crud from app.crud import study as study_crud from app.models.user import User from app.schemas.audit import AuditEventCreate, AuditLogRead from app.services.ip_location import resolve_ip_location router = APIRouter() async def _build_export_audit_detail(db: AsyncSession, study_id: uuid.UUID, action: str) -> str: study = await study_crud.get(db, study_id) if not study: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") scope = "system" if action == "AUDIT_EXPORT_SYSTEM" else "project" description = "导出了系统审计日志" if scope == "system" else f"导出了项目审计日志:{study.name}" return json.dumps( { "targetName": "系统审计日志" if scope == "system" else study.name, "scope": scope, "description": description, "result": "SUCCESS", }, ensure_ascii=False, ) @router.get( "/", response_model=list[AuditLogRead], dependencies=[Depends(require_api_permission("audit_logs:read"))], ) async def list_audit_logs( 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: datetime | None = None, end_time: datetime | None = None, skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db_session), ) -> list[AuditLogRead]: logs = await audit_crud.list_logs( db, study_id=study_id, entity_type=entity_type, entity_id=entity_id, action=action, operator_id=operator_id, client_ip=client_ip, client_type=client_type, start_time=start_time, end_time=end_time, skip=skip, limit=limit, ) operator_ids = {log.operator_id for log in logs} operator_map: dict[uuid.UUID, User] = {} if operator_ids: result = await db.execute(select(User).where(User.id.in_(operator_ids))) operator_map = {user.id: user for user in result.scalars().all()} items: list[AuditLogRead] = [] for log in logs: ip_location = resolve_ip_location(log.client_ip) operator = operator_map.get(log.operator_id) items.append( AuditLogRead( id=log.id, study_id=log.study_id, entity_type=log.entity_type, entity_id=log.entity_id, action=log.action, detail=log.detail, operator_id=log.operator_id, operator_name=operator.full_name if operator else None, operator_email=operator.email if operator else None, operator_role=log.operator_role, client_ip=log.client_ip, ip_location=ip_location.location, ip_country=ip_location.country, ip_province=ip_location.province, ip_city=ip_location.city, ip_isp=ip_location.isp, user_agent=log.user_agent, client_type=log.client_type, client_version=log.client_version, client_platform=log.client_platform, build_channel=log.build_channel, build_commit=log.build_commit, created_at=log.created_at, ) ) return items @router.post( "/events", status_code=status.HTTP_201_CREATED, ) async def create_audit_event( study_id: uuid.UUID, payload: AuditEventCreate, _export_permission=Depends(require_api_permission("audit_logs:export")), db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): allowed_actions = {"AUDIT_EXPORT_SYSTEM", "AUDIT_EXPORT_PROJECT"} if payload.action not in allowed_actions: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不支持的审计事件") await audit_crud.log_action( db, study_id=study_id, entity_type="audit_log", entity_id=None, action=payload.action, detail=await _build_export_audit_detail(db, study_id, payload.action), operator_id=current_user.id, operator_role=await get_operator_role_label(db, study_id, current_user), ) return {"ok": True}