mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-12 04:28:28 +08:00
feat: 添加访问令牌管理功能并升级至 0.2.4
- 新增 Management Token(访问令牌)功能,支持创建、更新、删除和管理 - 前端添加访问令牌管理页面,支持普通用户和管理员 - 后端实现完整的令牌生命周期管理 API - 添加数据库迁移脚本创建 management_tokens 表 - Nginx 配置添加 gzip 压缩,优化响应传输 - Dialog 组件添加 persistent 属性,防止意外关闭 - 为管理后台 API 添加详细的中文文档注释 - 简化多处类型注解,统一代码风格
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .management_tokens import router as management_tokens_router
|
||||
from .routes import router as me_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(me_router)
|
||||
router.include_router(management_tokens_router)
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
577
src/api/user_me/management_tokens.py
Normal file
577
src/api/user_me/management_tokens.py
Normal file
@@ -0,0 +1,577 @@
|
||||
"""用户 Management Token 管理端点"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.api.base.authenticated_adapter import AuthenticatedApiAdapter
|
||||
from src.api.base.context import ApiRequestContext
|
||||
from src.api.base.pipeline import ApiRequestPipeline
|
||||
from src.core.exceptions import InvalidRequestException, NotFoundException
|
||||
from src.database import get_db
|
||||
from src.models.database import AuditEventType
|
||||
from src.services.management_token import (
|
||||
ManagementTokenService,
|
||||
parse_expires_at,
|
||||
token_to_dict,
|
||||
validate_ip_list,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/me/management-tokens", tags=["Management Tokens"])
|
||||
pipeline = ApiRequestPipeline()
|
||||
|
||||
|
||||
# ============== 安全基类 ==============
|
||||
|
||||
|
||||
class ManagementTokenApiAdapter(AuthenticatedApiAdapter):
|
||||
"""Management Token 管理 API 的基类
|
||||
|
||||
安全限制:禁止使用 Management Token 调用这些接口,
|
||||
防止用户通过已有的 Token 再创建/修改/删除其他 Token。
|
||||
"""
|
||||
|
||||
def authorize(self, context: ApiRequestContext):
|
||||
# 先调用父类的认证检查
|
||||
super().authorize(context)
|
||||
|
||||
# 禁止使用 Management Token 调用 management-tokens 相关接口
|
||||
if context.management_token is not None:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="不允许使用 Management Token 管理其他 Token,请使用 Web 界面或 JWT 认证",
|
||||
)
|
||||
|
||||
|
||||
# ============== 请求/响应模型 ==============
|
||||
|
||||
|
||||
class CreateManagementTokenRequest(BaseModel):
|
||||
"""创建 Management Token 请求"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Token 名称")
|
||||
description: Optional[str] = Field(None, max_length=500, description="描述")
|
||||
allowed_ips: Optional[list[str]] = Field(None, description="IP 白名单")
|
||||
expires_at: Optional[datetime] = Field(None, description="过期时间")
|
||||
|
||||
@field_validator("allowed_ips")
|
||||
@classmethod
|
||||
def validate_allowed_ips(cls, v: Optional[list[str]]) -> Optional[list[str]]:
|
||||
return validate_ip_list(v)
|
||||
|
||||
@field_validator("expires_at", mode="before")
|
||||
@classmethod
|
||||
def parse_expires(cls, v):
|
||||
return parse_expires_at(v)
|
||||
|
||||
|
||||
class UpdateManagementTokenRequest(BaseModel):
|
||||
"""更新 Management Token 请求
|
||||
|
||||
对于 allowed_ips 和 expires_at 字段:
|
||||
- 未提供(字段不在请求中): 不修改
|
||||
- 显式设为 null: 清空该字段
|
||||
- 提供有效值: 更新为新值
|
||||
"""
|
||||
|
||||
model_config = {"extra": "allow"} # 允许额外字段以便检测哪些字段被显式提供
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=500)
|
||||
allowed_ips: Optional[list[str]] = None
|
||||
expires_at: Optional[datetime] = None
|
||||
|
||||
# 用于追踪哪些字段被显式提供(包括显式设为 null 的情况)
|
||||
_provided_fields: set[str] = set()
|
||||
|
||||
def __init__(self, **data):
|
||||
# 记录实际传入的字段(包括值为 None 的)
|
||||
provided = set(data.keys())
|
||||
super().__init__(**data)
|
||||
object.__setattr__(self, "_provided_fields", provided)
|
||||
|
||||
def is_field_provided(self, field_name: str) -> bool:
|
||||
"""检查字段是否被显式提供(区分未提供和显式设为 null)"""
|
||||
return field_name in self._provided_fields
|
||||
|
||||
@field_validator("allowed_ips")
|
||||
@classmethod
|
||||
def validate_allowed_ips(cls, v: Optional[list[str]]) -> Optional[list[str]]:
|
||||
# 如果是 None,表示要清空,直接返回
|
||||
if v is None:
|
||||
return None
|
||||
return validate_ip_list(v)
|
||||
|
||||
@field_validator("expires_at", mode="before")
|
||||
@classmethod
|
||||
def parse_expires(cls, v):
|
||||
# 如果是 None 或空字符串,表示要清空
|
||||
if v is None or (isinstance(v, str) and not v.strip()):
|
||||
return None
|
||||
return parse_expires_at(v)
|
||||
|
||||
|
||||
# ============== 路由 ==============
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_my_management_tokens(
|
||||
request: Request,
|
||||
is_active: Optional[bool] = Query(None, description="筛选激活状态"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""列出当前用户的 Management Tokens
|
||||
|
||||
获取当前登录用户创建的所有 Management Tokens,支持按激活状态筛选和分页。
|
||||
|
||||
**查询参数**
|
||||
- is_active (Optional[bool]): 筛选激活状态(true/false),不传则返回全部
|
||||
- skip (int): 分页偏移量,默认 0
|
||||
- limit (int): 每页数量,范围 1-100,默认 50
|
||||
|
||||
**返回字段**
|
||||
- items (List[dict]): Token 列表
|
||||
- id (str): Token ID
|
||||
- user_id (str): 所属用户 ID
|
||||
- name (str): Token 名称
|
||||
- description (Optional[str]): 描述
|
||||
- token_hash (str): Token 哈希值(不返回明文)
|
||||
- is_active (bool): 是否激活
|
||||
- allowed_ips (Optional[List[str]]): IP 白名单
|
||||
- expires_at (Optional[str]): 过期时间(ISO 8601 格式)
|
||||
- last_used_at (Optional[str]): 最后使用时间
|
||||
- created_at (str): 创建时间
|
||||
- updated_at (str): 更新时间
|
||||
- total (int): 总数量
|
||||
- skip (int): 当前偏移量
|
||||
- limit (int): 当前每页数量
|
||||
- quota (dict): 配额信息
|
||||
- used (int): 已使用数量
|
||||
- max (int): 最大允许数量
|
||||
"""
|
||||
adapter = ListMyManagementTokensAdapter(is_active=is_active, skip=skip, limit=limit)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_my_management_token(request: Request, db: Session = Depends(get_db)):
|
||||
"""创建 Management Token
|
||||
|
||||
为当前用户创建一个新的 Management Token。
|
||||
|
||||
**请求体字段**
|
||||
- name (str): Token 名称,必填,长度 1-100
|
||||
- description (Optional[str]): 描述,可选,最大长度 500
|
||||
- allowed_ips (Optional[List[str]]): IP 白名单,可选,支持 IPv4/IPv6 和 CIDR 格式
|
||||
- expires_at (Optional[datetime]): 过期时间,可选,支持 ISO 8601 格式字符串或 datetime 对象
|
||||
|
||||
**返回字段**
|
||||
- message (str): 操作结果消息
|
||||
- token (str): 生成的 Token 明文(仅在创建时返回一次,请妥善保存)
|
||||
- data (dict): Token 信息
|
||||
- id (str): Token ID
|
||||
- user_id (str): 所属用户 ID
|
||||
- name (str): Token 名称
|
||||
- description (Optional[str]): 描述
|
||||
- token_hash (str): Token 哈希值
|
||||
- is_active (bool): 是否激活(新创建默认为 true)
|
||||
- allowed_ips (Optional[List[str]]): IP 白名单
|
||||
- expires_at (Optional[str]): 过期时间
|
||||
- last_used_at (Optional[str]): 最后使用时间
|
||||
- created_at (str): 创建时间
|
||||
- updated_at (str): 更新时间
|
||||
"""
|
||||
adapter = CreateMyManagementTokenAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/{token_id}")
|
||||
async def get_my_management_token(
|
||||
token_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取 Management Token 详情
|
||||
|
||||
获取当前用户指定 Token 的详细信息。
|
||||
|
||||
**路径参数**
|
||||
- token_id (str): Token ID
|
||||
|
||||
**返回字段**
|
||||
- id (str): Token ID
|
||||
- user_id (str): 所属用户 ID
|
||||
- name (str): Token 名称
|
||||
- description (Optional[str]): 描述
|
||||
- token_hash (str): Token 哈希值(不返回明文)
|
||||
- is_active (bool): 是否激活
|
||||
- allowed_ips (Optional[List[str]]): IP 白名单
|
||||
- expires_at (Optional[str]): 过期时间(ISO 8601 格式)
|
||||
- last_used_at (Optional[str]): 最后使用时间
|
||||
- created_at (str): 创建时间
|
||||
- updated_at (str): 更新时间
|
||||
"""
|
||||
adapter = GetMyManagementTokenAdapter(token_id=token_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.put("/{token_id}")
|
||||
async def update_my_management_token(
|
||||
token_id: str, request: Request, db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新 Management Token
|
||||
|
||||
更新当前用户指定 Token 的信息。支持部分字段更新。
|
||||
|
||||
**路径参数**
|
||||
- token_id (str): Token ID
|
||||
|
||||
**请求体字段**(所有字段均可选)
|
||||
- name (Optional[str]): Token 名称,长度 1-100
|
||||
- description (Optional[str]): 描述,最大长度 500,传空字符串或 null 可清空
|
||||
- allowed_ips (Optional[List[str]]): IP 白名单,传 null 可清空
|
||||
- expires_at (Optional[datetime]): 过期时间,传 null 可清空
|
||||
|
||||
注意:未提供的字段不会被修改,显式传 null 表示清空该字段。
|
||||
|
||||
**返回字段**
|
||||
- message (str): 操作结果消息
|
||||
- data (dict): 更新后的 Token 信息
|
||||
- id (str): Token ID
|
||||
- user_id (str): 所属用户 ID
|
||||
- name (str): Token 名称
|
||||
- description (Optional[str]): 描述
|
||||
- token_hash (str): Token 哈希值
|
||||
- is_active (bool): 是否激活
|
||||
- allowed_ips (Optional[List[str]]): IP 白名单
|
||||
- expires_at (Optional[str]): 过期时间
|
||||
- last_used_at (Optional[str]): 最后使用时间
|
||||
- created_at (str): 创建时间
|
||||
- updated_at (str): 更新时间
|
||||
"""
|
||||
adapter = UpdateMyManagementTokenAdapter(token_id=token_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.delete("/{token_id}")
|
||||
async def delete_my_management_token(
|
||||
token_id: str, request: Request, db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除 Management Token
|
||||
|
||||
删除当前用户指定的 Token。
|
||||
|
||||
**路径参数**
|
||||
- token_id (str): 要删除的 Token ID
|
||||
|
||||
**返回字段**
|
||||
- message (str): 操作结果消息
|
||||
"""
|
||||
adapter = DeleteMyManagementTokenAdapter(token_id=token_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.patch("/{token_id}/status")
|
||||
async def toggle_my_management_token(
|
||||
token_id: str, request: Request, db: Session = Depends(get_db)
|
||||
):
|
||||
"""切换 Management Token 状态
|
||||
|
||||
启用或禁用当前用户指定的 Token。
|
||||
|
||||
**路径参数**
|
||||
- token_id (str): Token ID
|
||||
|
||||
**返回字段**
|
||||
- message (str): 操作结果消息("Token 已启用" 或 "Token 已禁用")
|
||||
- data (dict): 更新后的 Token 信息
|
||||
- id (str): Token ID
|
||||
- user_id (str): 所属用户 ID
|
||||
- name (str): Token 名称
|
||||
- description (Optional[str]): 描述
|
||||
- token_hash (str): Token 哈希值
|
||||
- is_active (bool): 是否激活(已切换后的状态)
|
||||
- allowed_ips (Optional[List[str]]): IP 白名单
|
||||
- expires_at (Optional[str]): 过期时间
|
||||
- last_used_at (Optional[str]): 最后使用时间
|
||||
- created_at (str): 创建时间
|
||||
- updated_at (str): 更新时间
|
||||
"""
|
||||
adapter = ToggleMyManagementTokenAdapter(token_id=token_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.post("/{token_id}/regenerate")
|
||||
async def regenerate_my_management_token(
|
||||
token_id: str, request: Request, db: Session = Depends(get_db)
|
||||
):
|
||||
"""重新生成 Management Token
|
||||
|
||||
重新生成当前用户指定 Token 的值,旧 Token 将立即失效。
|
||||
|
||||
**路径参数**
|
||||
- token_id (str): Token ID
|
||||
|
||||
**返回字段**
|
||||
- message (str): 操作结果消息
|
||||
- token (str): 新生成的 Token 明文(仅在重新生成时返回一次,请妥善保存)
|
||||
- data (dict): Token 信息
|
||||
- id (str): Token ID
|
||||
- user_id (str): 所属用户 ID
|
||||
- name (str): Token 名称
|
||||
- description (Optional[str]): 描述
|
||||
- token_hash (str): 新的 Token 哈希值
|
||||
- is_active (bool): 是否激活
|
||||
- allowed_ips (Optional[List[str]]): IP 白名单
|
||||
- expires_at (Optional[str]): 过期时间
|
||||
- last_used_at (Optional[str]): 最后使用时间(重置为 null)
|
||||
- created_at (str): 创建时间
|
||||
- updated_at (str): 更新时间
|
||||
"""
|
||||
adapter = RegenerateMyManagementTokenAdapter(token_id=token_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
# ============== 适配器 ==============
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListMyManagementTokensAdapter(ManagementTokenApiAdapter):
|
||||
"""列出用户的 Management Tokens"""
|
||||
|
||||
name: str = "list_my_management_tokens"
|
||||
is_active: Optional[bool] = None
|
||||
skip: int = 0
|
||||
limit: int = 50
|
||||
|
||||
async def handle(self, context: ApiRequestContext):
|
||||
from src.config.settings import config
|
||||
|
||||
tokens, total = ManagementTokenService.list_tokens(
|
||||
db=context.db,
|
||||
user_id=context.user.id,
|
||||
is_active=self.is_active,
|
||||
skip=self.skip,
|
||||
limit=self.limit,
|
||||
)
|
||||
|
||||
# 获取用户 Token 总数(用于配额显示)
|
||||
max_tokens = config.management_token_max_per_user
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"items": [token_to_dict(t) for t in tokens],
|
||||
"total": total,
|
||||
"skip": self.skip,
|
||||
"limit": self.limit,
|
||||
"quota": {
|
||||
"used": total,
|
||||
"max": max_tokens,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CreateMyManagementTokenAdapter(ManagementTokenApiAdapter):
|
||||
"""创建 Management Token"""
|
||||
|
||||
name: str = "create_my_management_token"
|
||||
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_CREATED
|
||||
|
||||
async def handle(self, context: ApiRequestContext):
|
||||
body = context.ensure_json_body()
|
||||
|
||||
try:
|
||||
req = CreateManagementTokenRequest(**body)
|
||||
except Exception as e:
|
||||
raise InvalidRequestException(str(e))
|
||||
|
||||
try:
|
||||
token, raw_token = ManagementTokenService.create_token(
|
||||
db=context.db,
|
||||
user_id=context.user.id,
|
||||
name=req.name,
|
||||
description=req.description,
|
||||
allowed_ips=req.allowed_ips,
|
||||
expires_at=req.expires_at,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise InvalidRequestException(str(e))
|
||||
|
||||
context.add_audit_metadata(token_id=token.id, token_name=token.name)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=201,
|
||||
content={
|
||||
"message": "Management Token 创建成功",
|
||||
"token": raw_token, # 仅在创建时返回一次
|
||||
"data": token_to_dict(token),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetMyManagementTokenAdapter(ManagementTokenApiAdapter):
|
||||
"""获取 Management Token 详情"""
|
||||
|
||||
name: str = "get_my_management_token"
|
||||
token_id: str = ""
|
||||
|
||||
async def handle(self, context: ApiRequestContext):
|
||||
token = ManagementTokenService.get_token_by_id(
|
||||
db=context.db, token_id=self.token_id, user_id=context.user.id
|
||||
)
|
||||
|
||||
if not token:
|
||||
raise NotFoundException("Management Token 不存在")
|
||||
|
||||
return JSONResponse(content=token_to_dict(token))
|
||||
|
||||
|
||||
@dataclass
|
||||
class UpdateMyManagementTokenAdapter(ManagementTokenApiAdapter):
|
||||
"""更新 Management Token"""
|
||||
|
||||
name: str = "update_my_management_token"
|
||||
token_id: str = ""
|
||||
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_UPDATED
|
||||
|
||||
async def handle(self, context: ApiRequestContext):
|
||||
body = context.ensure_json_body()
|
||||
|
||||
try:
|
||||
req = UpdateManagementTokenRequest(**body)
|
||||
except Exception as e:
|
||||
raise InvalidRequestException(str(e))
|
||||
|
||||
# 构建更新参数,只包含显式提供的字段
|
||||
update_kwargs: dict = {
|
||||
"db": context.db,
|
||||
"token_id": self.token_id,
|
||||
"user_id": context.user.id,
|
||||
}
|
||||
|
||||
# 对于普通字段,只有提供了才更新
|
||||
if req.is_field_provided("name"):
|
||||
update_kwargs["name"] = req.name
|
||||
if req.is_field_provided("description"):
|
||||
update_kwargs["description"] = req.description
|
||||
update_kwargs["clear_description"] = req.description is None or req.description == ""
|
||||
|
||||
# 对于可清空字段,需要传递特殊标记
|
||||
if req.is_field_provided("allowed_ips"):
|
||||
update_kwargs["allowed_ips"] = req.allowed_ips
|
||||
update_kwargs["clear_allowed_ips"] = req.allowed_ips is None
|
||||
if req.is_field_provided("expires_at"):
|
||||
update_kwargs["expires_at"] = req.expires_at
|
||||
update_kwargs["clear_expires_at"] = req.expires_at is None
|
||||
|
||||
try:
|
||||
token = ManagementTokenService.update_token(**update_kwargs)
|
||||
except ValueError as e:
|
||||
raise InvalidRequestException(str(e))
|
||||
|
||||
if not token:
|
||||
raise NotFoundException("Management Token 不存在")
|
||||
|
||||
context.add_audit_metadata(token_id=token.id, token_name=token.name)
|
||||
|
||||
return JSONResponse(
|
||||
content={"message": "更新成功", "data": token_to_dict(token)}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeleteMyManagementTokenAdapter(ManagementTokenApiAdapter):
|
||||
"""删除 Management Token"""
|
||||
|
||||
name: str = "delete_my_management_token"
|
||||
token_id: str = ""
|
||||
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_DELETED
|
||||
|
||||
async def handle(self, context: ApiRequestContext):
|
||||
# 先获取 token 信息用于审计
|
||||
token = ManagementTokenService.get_token_by_id(
|
||||
db=context.db, token_id=self.token_id, user_id=context.user.id
|
||||
)
|
||||
|
||||
if not token:
|
||||
raise NotFoundException("Management Token 不存在")
|
||||
|
||||
context.add_audit_metadata(token_id=token.id, token_name=token.name)
|
||||
|
||||
success = ManagementTokenService.delete_token(
|
||||
db=context.db, token_id=self.token_id, user_id=context.user.id
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise NotFoundException("Management Token 不存在")
|
||||
|
||||
return JSONResponse(content={"message": "删除成功"})
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToggleMyManagementTokenAdapter(ManagementTokenApiAdapter):
|
||||
"""切换 Management Token 状态"""
|
||||
|
||||
name: str = "toggle_my_management_token"
|
||||
token_id: str = ""
|
||||
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_UPDATED
|
||||
|
||||
async def handle(self, context: ApiRequestContext):
|
||||
token = ManagementTokenService.toggle_status(
|
||||
db=context.db, token_id=self.token_id, user_id=context.user.id
|
||||
)
|
||||
|
||||
if not token:
|
||||
raise NotFoundException("Management Token 不存在")
|
||||
|
||||
context.add_audit_metadata(
|
||||
token_id=token.id, token_name=token.name, is_active=token.is_active
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": f"Token 已{'启用' if token.is_active else '禁用'}",
|
||||
"data": token_to_dict(token),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegenerateMyManagementTokenAdapter(ManagementTokenApiAdapter):
|
||||
"""重新生成 Management Token"""
|
||||
|
||||
name: str = "regenerate_my_management_token"
|
||||
token_id: str = ""
|
||||
audit_success_event = AuditEventType.MANAGEMENT_TOKEN_UPDATED
|
||||
|
||||
async def handle(self, context: ApiRequestContext):
|
||||
token, raw_token, old_token_hash = ManagementTokenService.regenerate_token(
|
||||
db=context.db, token_id=self.token_id, user_id=context.user.id
|
||||
)
|
||||
|
||||
if not token:
|
||||
raise NotFoundException("Management Token 不存在")
|
||||
|
||||
context.add_audit_metadata(
|
||||
token_id=token.id,
|
||||
token_name=token.name,
|
||||
regenerated=True,
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"message": "Token 已重新生成",
|
||||
"token": raw_token, # 仅在重新生成时返回一次
|
||||
"data": token_to_dict(token),
|
||||
}
|
||||
)
|
||||
@@ -35,20 +35,43 @@ pipeline = ApiRequestPipeline()
|
||||
|
||||
@router.get("")
|
||||
async def get_my_profile(request: Request, db: Session = Depends(get_db)):
|
||||
"""获取当前用户完整信息(包含偏好设置)"""
|
||||
"""
|
||||
获取当前用户信息
|
||||
|
||||
返回当前登录用户的完整信息,包括基本信息和偏好设置。
|
||||
|
||||
**返回字段**: id, email, username, role, is_active, quota_usd, used_usd, preferences 等
|
||||
"""
|
||||
adapter = MeProfileAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.put("")
|
||||
async def update_my_profile(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
更新个人信息
|
||||
|
||||
更新当前用户的邮箱或用户名。
|
||||
|
||||
**请求体**:
|
||||
- `email`: 新邮箱地址(可选)
|
||||
- `username`: 新用户名(可选)
|
||||
"""
|
||||
adapter = UpdateProfileAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.patch("/password")
|
||||
async def change_my_password(request: Request, db: Session = Depends(get_db)):
|
||||
"""Change current user's password"""
|
||||
"""
|
||||
修改密码
|
||||
|
||||
修改当前用户的登录密码。
|
||||
|
||||
**请求体**:
|
||||
- `old_password`: 当前密码
|
||||
- `new_password`: 新密码(至少 6 位)
|
||||
"""
|
||||
adapter = ChangePasswordAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -58,12 +81,30 @@ async def change_my_password(request: Request, db: Session = Depends(get_db)):
|
||||
|
||||
@router.get("/api-keys")
|
||||
async def list_my_api_keys(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
获取 API 密钥列表
|
||||
|
||||
返回当前用户的所有 API 密钥,包含使用统计信息。
|
||||
密钥值仅显示前后几位,完整密钥需通过详情接口获取。
|
||||
|
||||
**返回字段**: id, name, key_display, is_active, total_requests, total_cost_usd, last_used_at 等
|
||||
"""
|
||||
adapter = ListMyApiKeysAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.post("/api-keys")
|
||||
async def create_my_api_key(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
创建 API 密钥
|
||||
|
||||
为当前用户创建新的 API 密钥。创建成功后会返回完整的密钥值,请妥善保存。
|
||||
|
||||
**请求体**:
|
||||
- `name`: 密钥名称
|
||||
|
||||
**返回**: 包含完整密钥值的响应(仅此一次显示完整密钥)
|
||||
"""
|
||||
adapter = CreateMyApiKeyAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -72,10 +113,20 @@ async def create_my_api_key(request: Request, db: Session = Depends(get_db)):
|
||||
async def get_my_api_key(
|
||||
key_id: str,
|
||||
request: Request,
|
||||
include_key: bool = Query(False, description="Include full decrypted key in response"),
|
||||
include_key: bool = Query(False, description="是否返回完整密钥"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get API key detail, optionally include full key"""
|
||||
"""
|
||||
获取 API 密钥详情
|
||||
|
||||
获取指定 API 密钥的详细信息。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: 密钥 ID
|
||||
|
||||
**查询参数**:
|
||||
- `include_key`: 设为 true 时返回完整解密后的密钥值
|
||||
"""
|
||||
if include_key:
|
||||
adapter = GetMyFullKeyAdapter(key_id=key_id)
|
||||
else:
|
||||
@@ -85,13 +136,28 @@ async def get_my_api_key(
|
||||
|
||||
@router.delete("/api-keys/{key_id}")
|
||||
async def delete_my_api_key(key_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
删除 API 密钥
|
||||
|
||||
永久删除指定的 API 密钥,删除后无法恢复。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: 密钥 ID
|
||||
"""
|
||||
adapter = DeleteMyApiKeyAdapter(key_id=key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.patch("/api-keys/{key_id}")
|
||||
async def toggle_my_api_key(key_id: str, request: Request, db: Session = Depends(get_db)):
|
||||
"""Toggle API key active status"""
|
||||
"""
|
||||
切换 API 密钥状态
|
||||
|
||||
启用或禁用指定的 API 密钥。禁用后该密钥将无法用于 API 调用。
|
||||
|
||||
**路径参数**:
|
||||
- `key_id`: 密钥 ID
|
||||
"""
|
||||
adapter = ToggleMyApiKeyAdapter(key_id=key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -102,13 +168,27 @@ async def toggle_my_api_key(key_id: str, request: Request, db: Session = Depends
|
||||
@router.get("/usage")
|
||||
async def get_my_usage(
|
||||
request: Request,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
search: Optional[str] = None, # 通用搜索:密钥名、模型名
|
||||
start_date: Optional[datetime] = Query(None, description="开始时间(ISO 格式)"),
|
||||
end_date: Optional[datetime] = Query(None, description="结束时间(ISO 格式)"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词(密钥名、模型名)"),
|
||||
limit: int = Query(100, ge=1, le=200, description="每页记录数,默认100,最大200"),
|
||||
offset: int = Query(0, ge=0, le=2000, description="偏移量,用于分页,最大2000"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
获取使用统计
|
||||
|
||||
获取当前用户的 API 使用统计数据,包括总量汇总、按模型/提供商分组统计及详细记录。
|
||||
|
||||
**返回字段**:
|
||||
- `total_requests`: 总请求数
|
||||
- `total_tokens`: 总 Token 数
|
||||
- `total_cost`: 总成本(USD)
|
||||
- `summary_by_model`: 按模型分组统计
|
||||
- `summary_by_provider`: 按提供商分组统计
|
||||
- `records`: 详细使用记录列表
|
||||
- `pagination`: 分页信息
|
||||
"""
|
||||
adapter = GetUsageAdapter(
|
||||
start_date=start_date, end_date=end_date, search=search, limit=limit, offset=offset
|
||||
)
|
||||
@@ -118,10 +198,17 @@ async def get_my_usage(
|
||||
@router.get("/usage/active")
|
||||
async def get_my_active_requests(
|
||||
request: Request,
|
||||
ids: Optional[str] = Query(None, description="Comma-separated request IDs to query"),
|
||||
ids: Optional[str] = Query(None, description="请求 ID 列表,逗号分隔"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取用户活跃请求状态(用于轮询更新)"""
|
||||
"""
|
||||
获取活跃请求状态
|
||||
|
||||
查询正在进行中的请求状态,用于前端轮询更新流式请求的进度。
|
||||
|
||||
**查询参数**:
|
||||
- `ids`: 要查询的请求 ID 列表,逗号分隔
|
||||
"""
|
||||
adapter = GetActiveRequestsAdapter(ids=ids)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -133,7 +220,13 @@ async def get_my_interval_timeline(
|
||||
limit: int = Query(5000, ge=100, le=20000, description="最大返回数据点数量"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取当前用户的请求间隔时间线数据,用于散点图展示"""
|
||||
"""
|
||||
获取请求间隔时间线
|
||||
|
||||
获取请求间隔时间线数据,用于散点图展示请求分布情况。
|
||||
|
||||
**返回**: 包含时间戳和间隔时间的数据点列表
|
||||
"""
|
||||
adapter = GetMyIntervalTimelineAdapter(hours=hours, limit=limit)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -144,9 +237,12 @@ async def get_my_activity_heatmap(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get user's activity heatmap data for the past 365 days.
|
||||
获取活动热力图数据
|
||||
|
||||
This endpoint is cached for 5 minutes to reduce database load.
|
||||
获取过去 365 天的活动热力图数据,用于展示每日使用频率。
|
||||
此接口有 5 分钟缓存。
|
||||
|
||||
**返回**: 包含日期和请求数量的数据列表
|
||||
"""
|
||||
adapter = GetMyActivityHeatmapAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
@@ -154,13 +250,26 @@ async def get_my_activity_heatmap(
|
||||
|
||||
@router.get("/providers")
|
||||
async def list_available_providers(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
获取可用提供商列表
|
||||
|
||||
获取当前用户可用的所有提供商及其模型信息。
|
||||
|
||||
**返回字段**: id, name, display_name, endpoints, models 等
|
||||
"""
|
||||
adapter = ListAvailableProvidersAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/endpoint-status")
|
||||
async def get_endpoint_status(request: Request, db: Session = Depends(get_db)):
|
||||
"""获取端点状态(简化版,不包含敏感信息)"""
|
||||
"""
|
||||
获取端点健康状态
|
||||
|
||||
获取各 API 格式端点的健康状态(简化版,不包含敏感信息)。
|
||||
|
||||
**返回**: 按 API 格式分组的端点健康状态
|
||||
"""
|
||||
adapter = GetEndpointStatusAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -177,6 +286,17 @@ async def update_api_key_providers(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
更新 API 密钥可用提供商
|
||||
|
||||
设置指定 API 密钥可以使用哪些提供商。未设置时使用用户默认权限。
|
||||
|
||||
**路径参数**:
|
||||
- `api_key_id`: API 密钥 ID
|
||||
|
||||
**请求体**:
|
||||
- `allowed_providers`: 允许的提供商 ID 列表
|
||||
"""
|
||||
adapter = UpdateApiKeyProvidersAdapter(api_key_id=api_key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -187,7 +307,17 @@ async def update_api_key_capabilities(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""更新 API Key 的强制能力配置"""
|
||||
"""
|
||||
更新 API 密钥能力配置
|
||||
|
||||
设置指定 API 密钥的强制能力配置(如是否启用代码执行等)。
|
||||
|
||||
**路径参数**:
|
||||
- `api_key_id`: API 密钥 ID
|
||||
|
||||
**请求体**:
|
||||
- `force_capabilities`: 能力配置字典,如 `{"code_execution": true}`
|
||||
"""
|
||||
adapter = UpdateApiKeyCapabilitiesAdapter(api_key_id=api_key_id)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -197,26 +327,59 @@ async def update_api_key_capabilities(
|
||||
|
||||
@router.get("/preferences")
|
||||
async def get_my_preferences(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
获取偏好设置
|
||||
|
||||
获取当前用户的偏好设置,包括主题、语言、通知配置等。
|
||||
|
||||
**返回字段**: avatar_url, bio, theme, language, timezone, notifications 等
|
||||
"""
|
||||
adapter = GetPreferencesAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.put("/preferences")
|
||||
async def update_my_preferences(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
更新偏好设置
|
||||
|
||||
更新当前用户的偏好设置。
|
||||
|
||||
**请求体**:
|
||||
- `theme`: 主题(light/dark)
|
||||
- `language`: 语言
|
||||
- `timezone`: 时区
|
||||
- `email_notifications`: 邮件通知开关
|
||||
- `usage_alerts`: 用量告警开关
|
||||
- 等
|
||||
"""
|
||||
adapter = UpdatePreferencesAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.get("/model-capabilities")
|
||||
async def get_model_capability_settings(request: Request, db: Session = Depends(get_db)):
|
||||
"""获取用户的模型能力配置"""
|
||||
"""
|
||||
获取模型能力配置
|
||||
|
||||
获取用户针对各模型的能力配置(如是否启用特定功能)。
|
||||
|
||||
**返回**: model_capability_settings 字典
|
||||
"""
|
||||
adapter = GetModelCapabilitySettingsAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@router.put("/model-capabilities")
|
||||
async def update_model_capability_settings(request: Request, db: Session = Depends(get_db)):
|
||||
"""更新用户的模型能力配置"""
|
||||
"""
|
||||
更新模型能力配置
|
||||
|
||||
更新用户针对各模型的能力配置。
|
||||
|
||||
**请求体**:
|
||||
- `model_capability_settings`: 模型能力配置字典,格式为 `{"model_name": {"capability": true}}`
|
||||
"""
|
||||
adapter = UpdateModelCapabilitySettingsAdapter()
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
@@ -225,11 +388,15 @@ async def update_model_capability_settings(request: Request, db: Session = Depen
|
||||
|
||||
|
||||
class MeProfileAdapter(AuthenticatedApiAdapter):
|
||||
"""获取当前用户信息的适配器"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
return PreferenceService.get_user_with_preferences(context.db, context.user.id)
|
||||
|
||||
|
||||
class UpdateProfileAdapter(AuthenticatedApiAdapter):
|
||||
"""更新用户个人信息的适配器"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
db = context.db
|
||||
user = context.user
|
||||
@@ -265,6 +432,8 @@ class UpdateProfileAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
|
||||
class ChangePasswordAdapter(AuthenticatedApiAdapter):
|
||||
"""修改用户密码的适配器"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
db = context.db
|
||||
user = context.user
|
||||
@@ -290,6 +459,8 @@ class ChangePasswordAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
|
||||
class ListMyApiKeysAdapter(AuthenticatedApiAdapter):
|
||||
"""获取用户 API 密钥列表的适配器"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
db = context.db
|
||||
user = context.user
|
||||
@@ -359,6 +530,8 @@ class ListMyApiKeysAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
|
||||
class CreateMyApiKeyAdapter(AuthenticatedApiAdapter):
|
||||
"""创建 API 密钥的适配器"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
payload = context.ensure_json_body()
|
||||
try:
|
||||
@@ -388,6 +561,8 @@ class CreateMyApiKeyAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
@dataclass
|
||||
class GetMyFullKeyAdapter(AuthenticatedApiAdapter):
|
||||
"""获取 API 密钥完整密钥值的适配器"""
|
||||
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
@@ -420,7 +595,8 @@ class GetMyFullKeyAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
@dataclass
|
||||
class GetMyApiKeyDetailAdapter(AuthenticatedApiAdapter):
|
||||
"""Get API key detail without full key"""
|
||||
"""获取 API 密钥详情的适配器(不包含完整密钥值)"""
|
||||
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
@@ -449,6 +625,8 @@ class GetMyApiKeyDetailAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
@dataclass
|
||||
class DeleteMyApiKeyAdapter(AuthenticatedApiAdapter):
|
||||
"""删除 API 密钥的适配器"""
|
||||
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
@@ -466,6 +644,8 @@ class DeleteMyApiKeyAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
@dataclass
|
||||
class ToggleMyApiKeyAdapter(AuthenticatedApiAdapter):
|
||||
"""切换 API 密钥启用/禁用状态的适配器"""
|
||||
|
||||
key_id: str
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
@@ -488,6 +668,8 @@ class ToggleMyApiKeyAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
@dataclass
|
||||
class GetUsageAdapter(AuthenticatedApiAdapter):
|
||||
"""获取用户使用统计的适配器"""
|
||||
|
||||
start_date: Optional[datetime]
|
||||
end_date: Optional[datetime]
|
||||
search: Optional[str] = None
|
||||
@@ -766,7 +948,7 @@ class GetMyIntervalTimelineAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
|
||||
class GetMyActivityHeatmapAdapter(AuthenticatedApiAdapter):
|
||||
"""Activity heatmap adapter with Redis caching for user."""
|
||||
"""获取用户活动热力图数据的适配器(带 Redis 缓存)"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
user = context.user
|
||||
@@ -780,6 +962,8 @@ class GetMyActivityHeatmapAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
|
||||
class ListAvailableProvidersAdapter(AuthenticatedApiAdapter):
|
||||
"""获取可用提供商列表的适配器"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
@@ -851,6 +1035,8 @@ class ListAvailableProvidersAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
@dataclass
|
||||
class UpdateApiKeyProvidersAdapter(AuthenticatedApiAdapter):
|
||||
"""更新 API 密钥可用提供商的适配器"""
|
||||
|
||||
api_key_id: str
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
@@ -962,6 +1148,8 @@ class UpdateApiKeyCapabilitiesAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
|
||||
class GetPreferencesAdapter(AuthenticatedApiAdapter):
|
||||
"""获取用户偏好设置的适配器"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
preferences = PreferenceService.get_or_create_preferences(context.db, context.user.id)
|
||||
return {
|
||||
@@ -983,6 +1171,8 @@ class GetPreferencesAdapter(AuthenticatedApiAdapter):
|
||||
|
||||
|
||||
class UpdatePreferencesAdapter(AuthenticatedApiAdapter):
|
||||
"""更新用户偏好设置的适配器"""
|
||||
|
||||
async def handle(self, context): # type: ignore[override]
|
||||
payload = context.ensure_json_body()
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user