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