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

237 lines
8.7 KiB
Python
Raw Normal View History

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