ForcePilot/backend/server/routers/external_systems/audit_log_router.py
Kris 2f6a4c29bb refactor(channel routers): 统一参数校验与常量定义,新增功能端点
1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用
2. 新增扫码登录向导端点,完善文档说明
3. 优化配对统计接口,移除无效参数
4. 为出站箱接口添加批量上限与202状态码
5. 新增测试用例、访问规则、配额等模块的查询与校验参数
6. 新增适配器健康批量查询、健康检查触发接口
7. 统一告警、审计日志的错误处理方式
8. 新增插件配置账户ID支持,优化批量操作响应
9. 新增环境健康批量查询、Webhook限流与参数校验
10. 完善会话管理、审计日志的参数与文档说明
11. 修复导入模块的校验错误处理逻辑
2026-07-11 21:39:05 +08:00

282 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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, Path, Query
from fastapi.exceptions import RequestValidationError
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"],
)
def _validate_time_range(start: datetime | None, end: datetime | None) -> None:
"""校验时间范围语义start 不晚于 end。
若校验失败,抛出 ``RequestValidationError``,由全局
``request_validation_error_handler`` 统一映射为 422 + 项目统一错误格式。
"""
if start is not None and end is not None and start > end:
raise RequestValidationError(
errors=[
{
"loc": ("query", "start_time"),
"msg": "start_time 不能晚于 end_time",
"type": "value_error",
"input": {"start_time": start, "end_time": end},
}
]
)
# =============================================================================
# === 静态路径端点(必须在 /{log_id} 之前声明) ===
# =============================================================================
@audit_log_router.get("/export")
async def export_audit_logs(
system_id: int | None = Query(None, ge=1),
env_key: str | None = Query(None, max_length=32),
event_type: str | None = Query(None, max_length=64),
action_type: str | None = Query(None, max_length=64),
resource: str | None = Query(None, max_length=256),
user: str | None = Query(None, max_length=64),
start_time: datetime | None = Query(None),
end_time: datetime | None = Query(None),
limit: int = Query(10000, ge=1, le=10000, description="导出记录上限,最大 10000"),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> StreamingResponse:
"""导出审计日志为 JSON 字节流。
审计日志含跨系统敏感操作记录,导出需管理员权限。
导出受 ``limit`` 限制(默认上限 10000 条),超出部分通过响应头
``X-Truncated`` 和 ``X-Total-Count`` 标识。
采用增量流式输出,避免一次性构建大 payload 导致内存峰值。
"""
_validate_time_range(start_time, end_time)
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,
limit=limit,
offset=offset,
)
output = await use_cases.audit_log_service.export_audit_logs(input_dto)
timestamp_str = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
exported_at = datetime.now(UTC).isoformat()
async def _stream():
# JSON 对象包裹 items 数组 + 元数据,增量 yield 每条记录
yield b'{"exported_at":'
yield json.dumps(exported_at).encode("utf-8")
yield b',"total":'
yield str(output.total).encode("utf-8")
yield b',"truncated":'
yield str(output.truncated).lower().encode("utf-8")
yield b',"count":'
yield str(len(output.items)).encode("utf-8")
yield b',"items":['
for i, item in enumerate(output.items):
if i > 0:
yield b","
yield json.dumps(item.model_dump(), ensure_ascii=False, default=str).encode(
"utf-8"
)
yield b"]}"
headers = {
"Content-Disposition": f"attachment; filename=audit_logs_{timestamp_str}.json",
"X-Total-Count": str(output.total),
"X-Truncated": str(output.truncated).lower(),
}
return StreamingResponse(
_stream(),
media_type="application/octet-stream",
headers=headers,
)
@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, ge=1),
env_key: str | None = Query(None, max_length=32),
event_type: str | None = Query(None, max_length=64),
action_type: str | None = Query(None, max_length=64),
resource: str | None = Query(None, max_length=256),
user: str | None = Query(None, max_length=64),
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]:
"""分页列出审计日志。"""
_validate_time_range(start_time, end_time)
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, ge=1),
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 分组)。"""
_validate_time_range(start_time, end_time)
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, ge=1),
env_key: str | None = Query(None, max_length=32),
event_type: str | None = Query(None, max_length=64),
action_type: str | None = Query(None, max_length=64),
resource: str | None = Query(None, max_length=256),
user: str | None = Query(None, max_length=64),
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 等)。
"""
_validate_time_range(start_time, end_time)
use_cases = create_use_cases_from_db(db)
input_dto = ListConfigChangesInput(
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_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 = Path(ge=1),
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()}