feat: virtual company module, team projects, PWA dishes app, and startup scripts overhaul
- 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>
This commit is contained in:
212
backend/app/api/api_keys.py
Normal file
212
backend/app/api/api_keys.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user