ForcePilot/backend/server/routers/external_systems/token_router.py
Kris ba798a9bb5 feat(routers): add external systems management module and its sub routers
新增外部系统限界上下文的完整路由体系,包含聚合路由层与23个子领域路由,覆盖外部系统全生命周期管理、健康检查、指标监控、审计日志、Token管理、通知渠道、回收站等功能,并将外部系统路由挂载到全局API前缀下。
2026-06-20 22:16:07 +08:00

237 lines
8.7 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.

"""Token 子域 Router。
外部系统限界上下文的 Token 管理 API覆盖 Token 查询 / 详情 / 统计 /
历史 / 使用记录 / 失效(单条 + 环境级)/ 即将过期查询 / 轮换预览。
所有端点通过 ``create_use_cases_from_db`` 装配 use_cases经 ``token_service`` 端口调用用例。
路径顺序约束:静态路径(``/stats``、``/history``、``/invalidate-environment``、``/expiring``
必须在 ``/{token_id}`` 前声明,避免被路径参数捕获。
注意:``get_token_history`` 实际按 ``system_id`` 查询审计日志(非 token_id
故采用静态路径 ``/history`` + 必填 Query ``system_id``,路径语义与查询语义对齐。
"""
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, Field
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.token import (
GetTokenDetailInput,
GetTokenHistoryInput,
GetTokenRotationPreviewInput,
GetTokenStatsInput,
InvalidateEnvironmentTokensInput,
InvalidateTokenInput,
ListExpiringTokensInput,
ListTokensInput,
ListTokenUsageInput,
)
from yuxi.storage.postgres.models_business import User
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
token_router = APIRouter(prefix="/tokens", tags=["external-systems-token"])
# ---------------- Request Schemas ----------------
class InvalidateEnvironmentTokensRequest(BaseModel):
"""失效环境级 Token 请求体。字段对齐 ``InvalidateEnvironmentTokensInput``(不含 invalidated_by
``invalidated_by`` 由 Router 从 ``current_user.uid`` 注入。
``env_key`` 长度约束对齐 ``ExternalSystemEnvironment`` ORM 列定义String(32)
在边界层拦截非法输入。
"""
model_config = ConfigDict(frozen=True)
system_id: int
env_key: str = Field(..., min_length=1, max_length=32)
# ---------------- 静态路径端点(必须在 /{token_id} 之前声明) ----------------
@token_router.get("/stats", response_model=dict)
async def get_token_stats(
system_id: int | None = Query(None),
env_key: str | None = Query(None, max_length=32),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""获取 Token 统计信息。"""
use_cases = create_use_cases_from_db(db)
input_dto = GetTokenStatsInput(system_id=system_id, env_key=env_key)
output = await use_cases.token_service.get_token_stats(input_dto)
return {"success": True, "data": output.model_dump()}
@token_router.get("/history", response_model=dict)
async def get_token_history(
system_id: int = Query(..., description="按系统 ID 查询 Token 变更历史"),
env_key: str | None = Query(None, max_length=32),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""获取 Token 变更历史(基于审计日志)。
按 ``system_id`` 查询 Token 子域相关的审计事件(失效 / 环境失效 / 刷新 / 刷新失败),
从审计日志的 ``snapshot_after`` 构造 ``TokenHistoryItem``Token 字段 + 审计元数据)。
"""
use_cases = create_use_cases_from_db(db)
input_dto = GetTokenHistoryInput(
system_id=system_id,
env_key=env_key,
page=offset // limit + 1,
page_size=limit,
)
output = await use_cases.token_service.get_token_history(input_dto)
return {"success": True, "data": output.model_dump()}
@token_router.post("/invalidate-environment", response_model=dict)
async def invalidate_environment_tokens(
body: InvalidateEnvironmentTokensRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""失效指定系统 + 环境下的 Token。
``(system_id, env_key)`` 唯一对应一条 Token``invalidated_by`` 由当前管理员填充,
写入审计日志以追溯操作人。
"""
use_cases = create_use_cases_from_db(db)
input_dto = InvalidateEnvironmentTokensInput(
system_id=body.system_id,
env_key=body.env_key,
invalidated_by=current_user.uid,
)
output = await use_cases.token_service.invalidate_environment_tokens(input_dto)
return {"success": True, "data": output.model_dump()}
@token_router.get("/expiring", response_model=dict)
async def list_expiring_tokens(
days: int = Query(7, ge=1, le=90),
system_id: int | None = Query(None),
limit: int = Query(100, ge=1, le=500),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询即将过期的 Token默认 7 天内,未失效)。"""
use_cases = create_use_cases_from_db(db)
input_dto = ListExpiringTokensInput(
days=days,
system_id=system_id,
limit=limit,
)
output = await use_cases.token_service.list_expiring_tokens(input_dto)
return {"success": True, "data": output.model_dump()}
# ---------------- 根路径端点 ----------------
@token_router.get("", response_model=dict)
async def list_tokens(
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, max_length=32),
token_type: str | None = Query(None),
is_invalidated: bool | None = Query(None),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""分页列出 Token。"""
use_cases = create_use_cases_from_db(db)
input_dto = ListTokensInput(
page=offset // limit + 1,
page_size=limit,
system_id=system_id,
env_key=env_key,
token_type=token_type,
is_invalidated=is_invalidated,
)
output = await use_cases.token_service.list_tokens(input_dto)
return {"success": True, "data": output.model_dump()}
# ---------------- 动态路径端点 /{token_id} ----------------
@token_router.get("/{token_id}", response_model=dict)
async def get_token_detail(
token_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""获取 Token 详情。"""
use_cases = create_use_cases_from_db(db)
input_dto = GetTokenDetailInput(id=token_id)
output = await use_cases.token_service.get_token_detail(input_dto)
return {"success": True, "data": output.model_dump()}
@token_router.post("/{token_id}/invalidate", response_model=dict)
async def invalidate_token(
token_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""失效 Token。``invalidated_by`` 由当前管理员填充,写入审计日志以追溯操作人。"""
use_cases = create_use_cases_from_db(db)
input_dto = InvalidateTokenInput(
id=token_id,
invalidated_by=current_user.uid,
)
output = await use_cases.token_service.invalidate_token(input_dto)
return {"success": True, "data": output.model_dump()}
@token_router.get("/{token_id}/usage", response_model=dict)
async def list_token_usage(
token_id: int,
limit: int = Query(20, ge=1, le=500),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""查询 Token 使用记录。
当前系统未单独记录每次 Token 使用,返回空列表占位,
便于前端保持接口契约一致。
"""
use_cases = create_use_cases_from_db(db)
input_dto = ListTokenUsageInput(
token_id=token_id,
page=offset // limit + 1,
page_size=limit,
)
output = await use_cases.token_service.list_token_usage(input_dto)
return {"success": True, "data": output.model_dump()}
@token_router.get("/{token_id}/rotation-preview", response_model=dict)
async def get_token_rotation_preview(
token_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_required_user),
) -> dict[str, Any]:
"""预览 Token 轮换的影响范围(关联系统、环境、工具)。
只读查询(不调用外部适配器),对齐 ``secret_rotation_router.precheck_rotation``
的权限梯度,使用 ``get_required_user``。
"""
use_cases = create_use_cases_from_db(db)
input_dto = GetTokenRotationPreviewInput(token_id=token_id)
output = await use_cases.token_service.get_token_rotation_preview(input_dto)
return {"success": True, "data": output.model_dump()}