1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用 2. 新增扫码登录向导端点,完善文档说明 3. 优化配对统计接口,移除无效参数 4. 为出站箱接口添加批量上限与202状态码 5. 新增测试用例、访问规则、配额等模块的查询与校验参数 6. 新增适配器健康批量查询、健康检查触发接口 7. 统一告警、审计日志的错误处理方式 8. 新增插件配置账户ID支持,优化批量操作响应 9. 新增环境健康批量查询、Webhook限流与参数校验 10. 完善会话管理、审计日志的参数与文档说明 11. 修复导入模块的校验错误处理逻辑
285 lines
11 KiB
Python
285 lines
11 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),
|
||
expiring_days: int = Query(7, ge=1, le=90, description="expiring_soon 阈值天数"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取 Token 统计信息。
|
||
|
||
返回字段:``total`` / ``active`` / ``invalidated`` / ``expired`` / ``expiring_soon``。
|
||
``active = total - invalidated - expired``,``expiring_days`` 控制 ``expiring_soon``
|
||
的阈值天数(默认 7,与 ``/expiring`` 端点保持一致)。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetTokenStatsInput(
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
expiring_days=expiring_days,
|
||
)
|
||
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,
|
||
limit=limit,
|
||
offset=offset,
|
||
)
|
||
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`` 由当前管理员填充,
|
||
写入审计日志以追溯操作人。已失效的 Token 抛 ``ConflictError``(409)。
|
||
"""
|
||
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),
|
||
env_key: str | None = Query(None, max_length=32),
|
||
token_type: str | None = Query(None),
|
||
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(默认 7 天内,未失效)。
|
||
|
||
所有过滤条件(``system_id`` / ``env_key`` / ``token_type``)均下推至 DB 层,
|
||
``offset`` 支持分页,与 ``/`` 列表端点契约对齐。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListExpiringTokensInput(
|
||
days=days,
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
token_type=token_type,
|
||
limit=limit,
|
||
offset=offset,
|
||
)
|
||
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),
|
||
sort_by: str | None = Query(None, description="排序字段:updated_at/expires_at/created_at"),
|
||
sort_order: str = Query("desc", pattern="^(asc|desc)$", description="排序方向"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""分页列出 Token。
|
||
|
||
所有过滤条件(``system_id`` / ``env_key`` / ``token_type`` / ``is_invalidated``)
|
||
均下推至 DB 层,保证分页 ``total`` 与 ``items`` 的一致性。
|
||
``sort_by`` / ``sort_order`` 控制排序,由仓储白名单校验防注入。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListTokensInput(
|
||
limit=limit,
|
||
offset=offset,
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
token_type=token_type,
|
||
is_invalidated=is_invalidated,
|
||
sort_by=sort_by,
|
||
sort_order=sort_order,
|
||
)
|
||
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 详情。
|
||
|
||
返回 Token 字段 + 关联系统上下文(``system_name`` / ``system_slug``),
|
||
``access_token`` / ``refresh_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`` 由当前管理员填充,写入审计日志以追溯操作人。
|
||
|
||
已失效的 Token 抛 ``ConflictError``(409),避免重复审计日志与事件发布。
|
||
"""
|
||
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 使用记录。
|
||
|
||
基于 ``ExternalToolExecution`` 表按 ``(system_id, env_key)`` 关联查询,
|
||
返回使用该 Token 的工具执行记录摘要(id / execution_id / tool_slug /
|
||
status / started_at / ended_at / duration_ms / operation / caller)。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListTokenUsageInput(
|
||
token_id=token_id,
|
||
limit=limit,
|
||
offset=offset,
|
||
)
|
||
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,
|
||
expiring_days: int = Query(7, ge=1, le=90, description="expiring_soon 阈值天数"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""预览 Token 轮换的影响范围(关联系统、环境、工具、密钥轮换策略)。
|
||
|
||
只读查询(不调用外部适配器),对齐 ``secret_rotation_router.precheck_rotation``
|
||
的权限梯度,使用 ``get_required_user``。
|
||
|
||
返回字段:``token_status``(active/expiring_soon/expired/invalidated)+
|
||
``affected_system`` + ``affected_environment`` + ``affected_tools`` +
|
||
``affected_policies``(关联的密钥轮换策略列表)。
|
||
|
||
``expiring_days`` 控制 ``token_status`` 中 ``expiring_soon`` 的阈值天数,
|
||
与 ``/stats``、``/expiring`` 端点保持一致。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetTokenRotationPreviewInput(
|
||
token_id=token_id,
|
||
expiring_days=expiring_days,
|
||
)
|
||
output = await use_cases.token_service.get_token_rotation_preview(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|