ForcePilot/backend/server/routers/auth_apikey_router.py
2026-05-04 11:36:08 +08:00

259 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""API Key 管理路由"""
import hashlib
import secrets
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.storage.postgres.models_business import User, APIKey
from server.utils.auth_middleware import get_db, get_required_user, get_superadmin_user
from yuxi.utils.datetime_utils import coerce_any_to_utc_datetime, utc_now_naive
apikey_router = APIRouter(prefix="/apikey", tags=["apikey"])
def generate_api_key() -> tuple[str, str, str]:
"""生成新的 API Key
Returns: (full_key, key_hash, key_prefix)
- full_key: 完整密钥,仅在创建时返回一次
- key_hash: 存储到数据库的哈希值
- key_prefix: 保存前缀用于显示
"""
random_part = secrets.token_hex(24)
full_key = f"yxkey_{random_part}"
key_hash = hashlib.sha256(full_key.encode()).hexdigest()
key_prefix = full_key[:12]
return full_key, key_hash, key_prefix
class APIKeyCreate(BaseModel):
name: str
user_id: int | None = None
department_id: int | None = None
expires_at: str | None = None
class APIKeyUpdate(BaseModel):
name: str | None = None
expires_at: str | None = None
is_enabled: bool | None = None
class APIKeyResponse(BaseModel):
id: int
key_prefix: str
name: str
user_id: int | None
department_id: int | None
expires_at: str | None
is_enabled: bool
last_used_at: str | None
created_by: str
created_at: str
class APIKeyCreateResponse(BaseModel):
api_key: APIKeyResponse
secret: str
@apikey_router.get("/", response_model=dict)
async def list_api_keys(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""列出当前用户的 API Keys"""
# 普通用户只能看到自己的 API Keys
if current_user.role == "superadmin":
# superadmin 可以看到所有
result = await db.execute(select(APIKey).order_by(APIKey.created_at.desc()).offset(skip).limit(limit))
api_keys = result.scalars().all()
total_result = await db.execute(select(func.count(APIKey.id)))
else:
# 普通用户只看自己的
result = await db.execute(
select(APIKey)
.filter(APIKey.user_id == current_user.id)
.order_by(APIKey.created_at.desc())
.offset(skip)
.limit(limit)
)
api_keys = result.scalars().all()
total_result = await db.execute(
select(func.count(APIKey.id)).filter(APIKey.user_id == current_user.id)
)
total = total_result.scalar()
return {
"api_keys": [key.to_dict() for key in api_keys],
"total": total,
}
@apikey_router.post("/", response_model=APIKeyCreateResponse)
async def create_api_key(
data: APIKeyCreate,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""创建新 API Keysecret 仅在此处返回一次)"""
# 生成 Key
full_key, key_hash, key_prefix = generate_api_key()
# 普通用户只能为自己创建 API Key不能指定其他用户
if data.user_id and data.user_id != current_user.id and current_user.role != "superadmin":
raise HTTPException(status_code=403, detail="无权为其他用户创建 API Key")
# 验证关联用户
if data.user_id:
result = await db.execute(select(User).filter(User.id == data.user_id))
user = result.scalar_one_or_none()
if not user or user.is_deleted:
raise HTTPException(status_code=404, detail="关联的用户不存在")
else:
# 自动绑定为当前登录用户
data.user_id = current_user.id
# 解析过期时间(转换为 naive datetime 以匹配数据库字段)
expires_at = None
if data.expires_at:
aware_dt = coerce_any_to_utc_datetime(data.expires_at)
if aware_dt:
expires_at = aware_dt.replace(tzinfo=None)
# 创建记录
api_key = APIKey(
key_hash=key_hash,
key_prefix=key_prefix,
name=data.name,
user_id=data.user_id,
department_id=data.department_id,
expires_at=expires_at,
created_by=str(current_user.id),
)
db.add(api_key)
await db.commit()
await db.refresh(api_key)
return APIKeyCreateResponse(
api_key=APIKeyResponse(**api_key.to_dict()),
secret=full_key,
)
@apikey_router.get("/{api_key_id}", response_model=dict)
async def get_api_key(
api_key_id: int,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""获取单个 API Key只能操作自己的 Key"""
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(status_code=404, detail="API Key 不存在")
# 检查权限:只能操作自己的 Key或者 superadmin 可以操作所有
if api_key.user_id != current_user.id and current_user.role != "superadmin":
raise HTTPException(status_code=403, detail="无权操作此 API Key")
return {"api_key": api_key.to_dict()}
@apikey_router.put("/{api_key_id}", response_model=dict)
async def update_api_key(
api_key_id: int,
data: APIKeyUpdate,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""更新 API Key只能操作自己的 Key"""
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(status_code=404, detail="API Key 不存在")
# 检查权限:只能操作自己的 Key或者 superadmin 可以操作所有
if api_key.user_id != current_user.id and current_user.role != "superadmin":
raise HTTPException(status_code=403, detail="无权操作此 API Key")
if data.name is not None:
api_key.name = data.name
if data.expires_at is not None:
aware_dt = coerce_any_to_utc_datetime(data.expires_at)
api_key.expires_at = aware_dt.replace(tzinfo=None) if aware_dt else None
if data.is_enabled is not None:
api_key.is_enabled = data.is_enabled
await db.commit()
await db.refresh(api_key)
return {"api_key": api_key.to_dict()}
@apikey_router.delete("/{api_key_id}", response_model=dict)
async def delete_api_key(
api_key_id: int,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""删除 API Key只能操作自己的 Key"""
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(status_code=404, detail="API Key 不存在")
# 检查权限:只能操作自己的 Key或者 superadmin 可以操作所有
if api_key.user_id != current_user.id and current_user.role != "superadmin":
raise HTTPException(status_code=403, detail="无权操作此 API Key")
await db.delete(api_key)
await db.commit()
return {"success": True}
@apikey_router.post("/{api_key_id}/regenerate", response_model=APIKeyCreateResponse)
async def regenerate_api_key(
api_key_id: int,
current_user: User = Depends(get_required_user),
db: AsyncSession = Depends(get_db),
):
"""重新生成 API Key 密钥secret 仅在此处返回一次,只能操作自己的 Key"""
result = await db.execute(select(APIKey).filter(APIKey.id == api_key_id))
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(status_code=404, detail="API Key 不存在")
# 检查权限:只能操作自己的 Key或者 superadmin 可以操作所有
if api_key.user_id != current_user.id and current_user.role != "superadmin":
raise HTTPException(status_code=403, detail="无权操作此 API Key")
# 生成新密钥
full_key, key_hash, key_prefix = generate_api_key()
api_key.key_hash = key_hash
api_key.key_prefix = key_prefix
await db.commit()
await db.refresh(api_key)
return APIKeyCreateResponse(
api_key=APIKeyResponse(**api_key.to_dict()),
secret=full_key,
)