Files
beige/backend/app/api/v1/api_keys.py
yangqianqian 6a2632da70 baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:30:22 +08:00

131 lines
4.2 KiB
Python
Raw 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.
"""
app/api/v1/api_keys.py — API Key 路由
只录不显余额CLAUDE.md 红线)。
只返回 provider + key_last4不返回 encrypted_key。
url 字段不接收token站固定自家站架构方案§二
"""
import logging
from typing import Annotated
from fastapi import APIRouter, Depends
from pydantic import BaseModel, field_validator
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.response import ok, raise_not_found
from app.core.security import encrypt_api_key, mask_api_key
from app.middleware.workspace_guard import CurrentUser, require_write_permission
from app.models.workspace import UserApiKey
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api-keys", tags=["api-keys"])
# ── DTO ────────────────────────────────────────────────────
class CreateApiKeyRequest(BaseModel):
provider: str
api_key: str
@field_validator("provider")
@classmethod
def provider_not_empty(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("provider 不能为空")
return v.strip().lower()
@field_validator("api_key")
@classmethod
def key_not_empty(cls, v: str) -> str:
if not v or len(v) < 8:
raise ValueError("api_key 无效")
return v
# 注:不接收 url 字段token站固定自家站基石B
def _format_key(k: UserApiKey) -> dict:
"""只显 provider + key_last4不暴露余额/用量/encrypted_key红线"""
return {
"id": k.id,
"provider": k.provider,
"key_last4": k.key_last4,
"created_at": k.created_at.isoformat(),
}
# ── 路由 ───────────────────────────────────────────────────
@router.get("")
def list_api_keys(
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
db: Session = Depends(get_db),
):
"""列出当前用户在此 workspace 的 API Key只显后4位"""
keys = (
db.query(UserApiKey)
.filter(
UserApiKey.user_id == current_user.user_id,
UserApiKey.workspace_id == current_user.workspace_id,
)
.all()
)
return ok({"items": [_format_key(k) for k in keys]})
@router.post("")
def create_api_key(
body: CreateApiKeyRequest,
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
db: Session = Depends(get_db),
):
"""录入 API KeyFernet 加密存储只保存后4位明文用于展示"""
from app.core.response import raise_business
# 检查同 user+workspace+provider 是否已有
existing = (
db.query(UserApiKey)
.filter(
UserApiKey.user_id == current_user.user_id,
UserApiKey.workspace_id == current_user.workspace_id,
UserApiKey.provider == body.provider,
)
.first()
)
if existing:
raise_business(f"已存在 {body.provider} 的 API Key请先删除再录入")
encrypted = encrypt_api_key(body.api_key)
key_obj = UserApiKey(
user_id=current_user.user_id,
workspace_id=current_user.workspace_id,
provider=body.provider,
encrypted_key=encrypted,
key_last4=mask_api_key(body.api_key),
)
db.add(key_obj)
db.commit()
db.refresh(key_obj)
logger.info("API key created: user=%s provider=%s", current_user.user_id, body.provider)
return ok(_format_key(key_obj))
@router.delete("/{key_id}")
def delete_api_key(
key_id: int,
current_user: Annotated[CurrentUser, Depends(require_write_permission)],
db: Session = Depends(get_db),
):
"""删除 API Key只能删自己的"""
key_obj = (
db.query(UserApiKey)
.filter(
UserApiKey.id == key_id,
UserApiKey.user_id == current_user.user_id,
UserApiKey.workspace_id == current_user.workspace_id,
)
.first()
)
if not key_obj:
raise_not_found("API Key 不存在")
db.delete(key_obj)
db.commit()
return ok({"deleted": key_id})