新增外部系统限界上下文的完整路由体系,包含聚合路由层与23个子领域路由,覆盖外部系统全生命周期管理、健康检查、指标监控、审计日志、Token管理、通知渠道、回收站等功能,并将外部系统路由挂载到全局API前缀下。
237 lines
8.7 KiB
Python
237 lines
8.7 KiB
Python
"""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()}
|