ForcePilot/backend/server/routers/external_systems/audit_log_router.py

232 lines
8.0 KiB
Python
Raw Normal View History

"""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()}