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