- Add company module (3-tier org, CEO planning, parallel departments) - Add company orchestrator, knowledge extractor, presets, scheduler - Add company API endpoints, models, and frontend views - Add 今天吃啥 PWA app (69 dishes, real images, offline support) - Add team_projects output directory structure - Add unified manage.ps1 for service lifecycle - Add Windows startup guide v1.0 - Add TTS troubleshooting doc - Update frontend (AgentChat UX overhaul, new views) - Update backend (voice engine fix, multi-tenant, RBAC) - Remove deprecated startup scripts and old docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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)
|