"""AuditLog 子域 Router。 外部系统限界上下文的审计日志查询 API,覆盖审计日志列表 / 详情 / 导出。 所有端点通过 ``create_use_cases_from_db`` 装配 use_cases,经 ``audit_log_service`` 端口调用用例。 路径顺序约束:静态路径 ``/export`` 必须在 ``/{log_id}`` 前声明,避免被路径参数捕获。 """ from __future__ import annotations import json from datetime import UTC, datetime from typing import Any from fastapi import APIRouter, Depends, Query from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db from yuxi.external_systems.use_cases.dto.audit_log import ( ExportAuditLogsInput, GetAuditLogDetailInput, GetAuditLogStatsInput, ListActionTypesInput, ListAuditLogsInput, ListConfigChangesInput, ListEventTypesInput, ListOperatorsInput, ) from yuxi.storage.postgres.models_business import User from server.utils.auth_middleware import get_admin_user, get_db, get_required_user audit_log_router = APIRouter( prefix="/audit-logs", tags=["external-systems-audit-log"], ) # ============================================================================= # === 静态路径端点(必须在 /{log_id} 之前声明) === # ============================================================================= @audit_log_router.get("/export", response_model=dict) async def export_audit_logs( system_id: int | None = Query(None), env_key: str | None = Query(None), event_type: str | None = Query(None), action_type: str | None = Query(None), resource: str | None = Query(None), user: str | None = Query(None), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ): """导出审计日志为 JSON 字节流。 审计日志含跨系统敏感操作记录,导出需管理员权限。 """ use_cases = create_use_cases_from_db(db) input_dto = ExportAuditLogsInput( system_id=system_id, env_key=env_key, event_type=event_type, action_type=action_type, resource=resource, user=user, start_time=start_time, end_time=end_time, ) output = await use_cases.audit_log_service.export_audit_logs(input_dto) payload = json.dumps( [item.model_dump() for item in output.items], ensure_ascii=False, default=str, ).encode("utf-8") timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S") async def _stream(): yield payload return StreamingResponse( _stream(), media_type="application/octet-stream", headers={ "Content-Disposition": f"attachment; filename=audit_logs_{timestamp}.json", }, ) @audit_log_router.get("", response_model=dict) async def list_audit_logs( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), system_id: int | None = Query(None), env_key: str | None = Query(None), event_type: str | None = Query(None), action_type: str | None = Query(None), resource: str | None = Query(None), user: str | None = Query(None), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """分页列出审计日志。""" use_cases = create_use_cases_from_db(db) input_dto = ListAuditLogsInput( limit=limit, offset=offset, system_id=system_id, env_key=env_key, event_type=event_type, action_type=action_type, resource=resource, user=user, start_time=start_time, end_time=end_time, ) output = await use_cases.audit_log_service.list_audit_logs(input_dto) return {"success": True, "data": output.model_dump()} @audit_log_router.get("/stats", response_model=dict) async def get_audit_log_stats( system_id: int | None = Query(None), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询审计日志统计聚合(按 action_type 分组)。""" use_cases = create_use_cases_from_db(db) input_dto = GetAuditLogStatsInput( system_id=system_id, start_time=start_time, end_time=end_time, ) output = await use_cases.audit_log_service.get_audit_log_stats(input_dto) return {"success": True, "data": output.model_dump()} @audit_log_router.get("/config-changes", response_model=dict) async def list_config_changes( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), system_id: int | None = Query(None), env_key: str | None = Query(None), action_type: str | None = Query(None), resource: str | None = Query(None), user: str | None = Query(None), start_time: datetime | None = Query(None), end_time: datetime | None = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询配置变更历史(含 snapshot_before / snapshot_after 的审计日志)。 ``action_type`` 默认不限制,返回所有含快照的记录(CREATE/UPDATE/DELETE 等)。 """ use_cases = create_use_cases_from_db(db) input_dto = ListConfigChangesInput( limit=limit, offset=offset, system_id=system_id, env_key=env_key, action_type=action_type, resource=resource, user=user, start_time=start_time, end_time=end_time, ) output = await use_cases.audit_log_service.list_config_changes(input_dto) return {"success": True, "data": output.model_dump()} @audit_log_router.get("/action-types", response_model=dict) async def list_action_types( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询审计日志中已出现的操作类型列表。""" use_cases = create_use_cases_from_db(db) input_dto = ListActionTypesInput() output = await use_cases.audit_log_service.list_action_types(input_dto) return {"success": True, "data": output.model_dump()} @audit_log_router.get("/event-types", response_model=dict) async def list_event_types( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询审计日志中已出现的事件类型列表。""" use_cases = create_use_cases_from_db(db) input_dto = ListEventTypesInput() output = await use_cases.audit_log_service.list_event_types(input_dto) return {"success": True, "data": output.model_dump()} @audit_log_router.get("/operators", response_model=dict) async def list_operators( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询审计日志中已出现的操作人列表。""" use_cases = create_use_cases_from_db(db) input_dto = ListOperatorsInput() output = await use_cases.audit_log_service.list_operators(input_dto) return {"success": True, "data": output.model_dump()} # ============================================================================= # === 动态路径端点 /{log_id} === # ============================================================================= @audit_log_router.get("/{log_id}", response_model=dict) async def get_audit_log_detail( log_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """获取审计日志详情。""" use_cases = create_use_cases_from_db(db) input_dto = GetAuditLogDetailInput(id=log_id) output = await use_cases.audit_log_service.get_audit_log_detail(input_dto) return {"success": True, "data": output.model_dump()}