Files
aiagent/backend/app/api/api_keys.py

213 lines
6.7 KiB
Python
Raw Normal View History

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