213 lines
6.7 KiB
Python
213 lines
6.7 KiB
Python
|
|
"""
|
|||
|
|
API Key 管理 API — 创建、列表、撤销 API 密钥
|
|||
|
|
"""
|
|||
|
|
import hashlib
|
|||
|
|
import secrets
|
|||
|
|
import logging
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from typing import List, Optional
|
|||
|
|
|
|||
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|||
|
|
from pydantic import BaseModel, Field
|
|||
|
|
from sqlalchemy.orm import Session
|
|||
|
|
|
|||
|
|
from app.core.database import get_db
|
|||
|
|
from app.api.auth import get_current_user
|
|||
|
|
from app.api.deps import require_admin, get_current_workspace_id
|
|||
|
|
from app.models.user import User
|
|||
|
|
from app.models.api_key import ApiKey, KEY_PREFIX, verify_api_key
|
|||
|
|
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
router = APIRouter(prefix="/api/v1/api-keys", tags=["api-keys"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _generate_api_key() -> tuple[str, str, str]:
|
|||
|
|
"""生成 API Key。返回 (完整key, 前缀, SHA256哈希)。"""
|
|||
|
|
raw = secrets.token_hex(24) # 48 hex chars
|
|||
|
|
full_key = f"{KEY_PREFIX}{raw}"
|
|||
|
|
key_hash = hashlib.sha256(full_key.encode()).hexdigest()
|
|||
|
|
return full_key, KEY_PREFIX, key_hash
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ApiKeyCreate(BaseModel):
|
|||
|
|
name: str = Field(..., min_length=1, max_length=100, description="密钥名称")
|
|||
|
|
expires_in_days: Optional[int] = Field(None, ge=1, description="过期天数(null=永不过期)")
|
|||
|
|
permissions: Optional[List[str]] = Field(None, description="权限范围(null=全部权限)")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ApiKeyResponse(BaseModel):
|
|||
|
|
id: str
|
|||
|
|
name: str
|
|||
|
|
key_prefix: str
|
|||
|
|
permissions: Optional[List[str]] = None
|
|||
|
|
last_used_at: Optional[str] = None
|
|||
|
|
expires_at: Optional[str] = None
|
|||
|
|
status: str
|
|||
|
|
created_at: str
|
|||
|
|
|
|||
|
|
class Config:
|
|||
|
|
from_attributes = True
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ApiKeyCreatedResponse(BaseModel):
|
|||
|
|
"""创建成功后返回,包含明文密钥(仅此一次)"""
|
|||
|
|
id: str
|
|||
|
|
name: str
|
|||
|
|
api_key: str = Field(..., description="完整的 API Key(仅创建时返回,请立即保存)")
|
|||
|
|
key_prefix: str
|
|||
|
|
expires_at: Optional[str] = None
|
|||
|
|
message: str = "请立即保存此密钥,关闭后无法再次查看"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("", response_model=ApiKeyCreatedResponse, status_code=status.HTTP_201_CREATED)
|
|||
|
|
async def create_api_key(
|
|||
|
|
req: ApiKeyCreate,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
workspace_id: str = Depends(get_current_workspace_id),
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
):
|
|||
|
|
"""创建新的 API Key。返回完整密钥(仅此一次)。"""
|
|||
|
|
full_key, prefix, key_hash = _generate_api_key()
|
|||
|
|
|
|||
|
|
expires_at = None
|
|||
|
|
if req.expires_in_days:
|
|||
|
|
expires_at = datetime.utcnow() + timedelta(days=req.expires_in_days)
|
|||
|
|
|
|||
|
|
api_key = ApiKey(
|
|||
|
|
user_id=current_user.id,
|
|||
|
|
workspace_id=workspace_id,
|
|||
|
|
name=req.name,
|
|||
|
|
key_prefix=prefix,
|
|||
|
|
key_hash=key_hash,
|
|||
|
|
permissions=req.permissions,
|
|||
|
|
expires_at=expires_at,
|
|||
|
|
)
|
|||
|
|
db.add(api_key)
|
|||
|
|
db.commit()
|
|||
|
|
db.refresh(api_key)
|
|||
|
|
|
|||
|
|
logger.info("用户 %s 创建了 API Key: %s", current_user.username, api_key.name)
|
|||
|
|
|
|||
|
|
return ApiKeyCreatedResponse(
|
|||
|
|
id=api_key.id,
|
|||
|
|
name=api_key.name,
|
|||
|
|
api_key=full_key,
|
|||
|
|
key_prefix=prefix,
|
|||
|
|
expires_at=api_key.expires_at.isoformat() if api_key.expires_at else None,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("", response_model=List[ApiKeyResponse])
|
|||
|
|
async def list_api_keys(
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
workspace_id: str = Depends(get_current_workspace_id),
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
):
|
|||
|
|
"""列出当前用户的所有 API Key(不返回密钥明文)。"""
|
|||
|
|
keys = (
|
|||
|
|
db.query(ApiKey)
|
|||
|
|
.filter(
|
|||
|
|
ApiKey.user_id == current_user.id,
|
|||
|
|
ApiKey.workspace_id == workspace_id,
|
|||
|
|
)
|
|||
|
|
.order_by(ApiKey.created_at.desc())
|
|||
|
|
.all()
|
|||
|
|
)
|
|||
|
|
return [
|
|||
|
|
ApiKeyResponse(
|
|||
|
|
id=k.id,
|
|||
|
|
name=k.name,
|
|||
|
|
key_prefix=k.key_prefix,
|
|||
|
|
permissions=k.permissions,
|
|||
|
|
last_used_at=k.last_used_at.isoformat() if k.last_used_at else None,
|
|||
|
|
expires_at=k.expires_at.isoformat() if k.expires_at else None,
|
|||
|
|
status=k.status,
|
|||
|
|
created_at=k.created_at.isoformat() if k.created_at else None,
|
|||
|
|
)
|
|||
|
|
for k in keys
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.delete("/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|||
|
|
async def revoke_api_key(
|
|||
|
|
key_id: str,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
):
|
|||
|
|
"""撤销 API Key(设置为 revoked 状态)。"""
|
|||
|
|
api_key = db.query(ApiKey).filter(
|
|||
|
|
ApiKey.id == key_id,
|
|||
|
|
ApiKey.user_id == current_user.id,
|
|||
|
|
).first()
|
|||
|
|
|
|||
|
|
if not api_key:
|
|||
|
|
raise HTTPException(status_code=404, detail="API Key 不存在")
|
|||
|
|
|
|||
|
|
if api_key.status == "revoked":
|
|||
|
|
raise HTTPException(status_code=400, detail="该密钥已被撤销")
|
|||
|
|
|
|||
|
|
api_key.status = "revoked"
|
|||
|
|
db.commit()
|
|||
|
|
|
|||
|
|
logger.info("用户 %s 撤销了 API Key: %s", current_user.username, api_key.name)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════
|
|||
|
|
# 管理员端点
|
|||
|
|
# ═══════════════════════════════════════════
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/admin/all", response_model=List[dict])
|
|||
|
|
async def admin_list_all_keys(
|
|||
|
|
_admin: User = Depends(require_admin()),
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
):
|
|||
|
|
"""管理员查看所有用户的 API Key(不含密钥明文)。"""
|
|||
|
|
keys = (
|
|||
|
|
db.query(ApiKey)
|
|||
|
|
.order_by(ApiKey.created_at.desc())
|
|||
|
|
.all()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
results = []
|
|||
|
|
for k in keys:
|
|||
|
|
owner = k.user
|
|||
|
|
results.append({
|
|||
|
|
"id": k.id,
|
|||
|
|
"name": k.name,
|
|||
|
|
"key_prefix": k.key_prefix,
|
|||
|
|
"permissions": k.permissions,
|
|||
|
|
"last_used_at": k.last_used_at.isoformat() if k.last_used_at else None,
|
|||
|
|
"expires_at": k.expires_at.isoformat() if k.expires_at else None,
|
|||
|
|
"status": k.status,
|
|||
|
|
"created_at": k.created_at.isoformat() if k.created_at else None,
|
|||
|
|
"owner": {
|
|||
|
|
"id": owner.id,
|
|||
|
|
"username": owner.username,
|
|||
|
|
"email": owner.email,
|
|||
|
|
} if owner else None,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.delete("/admin/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|||
|
|
async def admin_revoke_key(
|
|||
|
|
key_id: str,
|
|||
|
|
_admin: User = Depends(require_admin()),
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
):
|
|||
|
|
"""管理员撤销任意 API Key。"""
|
|||
|
|
api_key = db.query(ApiKey).filter(ApiKey.id == key_id).first()
|
|||
|
|
if not api_key:
|
|||
|
|
raise HTTPException(status_code=404, detail="API Key 不存在")
|
|||
|
|
if api_key.status == "revoked":
|
|||
|
|
raise HTTPException(status_code=400, detail="该密钥已被撤销")
|
|||
|
|
|
|||
|
|
api_key.status = "revoked"
|
|||
|
|
db.commit()
|
|||
|
|
|
|||
|
|
logger.info("管理员 %s 撤销了 %s 的 API Key: %s",
|
|||
|
|
current_user.username, api_key.user.username if api_key.user else "?", api_key.name)
|