Files
hive-manage-v3/backend/app/api/keys.py
T
enzo 3d905b606a feat: Key batch management (Key Manager service, API, frontend) (Phase 6)
Backend service for reading/writing profile .env files with masking,
auto-backup, batch overwrite, batch scan, diff view, and CSV export.
Owner/operator-only API endpoints. Frontend KeysPage with batch-overwrite
modal, batch-scan results, diff view with highlight, and CSV download.
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 17:24:53 +08:00

107 lines
3.4 KiB
Python

"""Keys API — 密钥批量管理。"""
import logging
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import PlainTextResponse
from app.database import get_db
from app.middleware.auth import require_role
from app.models.audit import AuditLog
from app.models.user import User
from app.services.key_manager import (
WHITELIST_FIELDS,
get_keys,
batch_overwrite,
batch_scan as scan_profiles,
get_diff,
export_csv,
list_profiles,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/keys", tags=["keys"])
@router.get("")
async def list_keys(
profiles: str = Query("", description="逗号分隔的 Profile 名"),
_user: User = Depends(require_role("owner", "operator")),
):
"""返回密钥字段列表(脱敏显示)。"""
profile_list = [p.strip() for p in profiles.split(",") if p.strip()] if profiles else None
data = get_keys(profile_list)
return {"data": data}
@router.post("/batch-overwrite")
async def batch_overwrite_keys(
body: dict,
user: User = Depends(require_role("owner", "operator")),
db: AsyncSession = Depends(get_db),
):
"""批量覆盖密钥。"""
profile_names = body.get("profile_names", [])
key_name = body.get("key_name", "")
new_value = body.get("new_value", "")
confirmation = body.get("confirmation", "")
if key_name not in WHITELIST_FIELDS:
raise HTTPException(status_code=400, detail=f"不支持的密钥字段: {key_name}")
# 验证确认短语
expected = f"我确认覆盖 {len(profile_names)} 个 Profile"
if confirmation != expected:
raise HTTPException(status_code=400, detail="确认短语不匹配")
results = batch_overwrite(profile_names, key_name, new_value)
# 审计日志(不记录原文)
audit = AuditLog(
user_id=user.id,
username=user.username,
action="keys:batch-overwrite",
target=f"profiles:{','.join(profile_names)}",
detail=f"key={key_name}, profiles={','.join(profile_names)}, "
f"success={sum(1 for r in results if r['success'])}/{len(results)}",
)
db.add(audit)
await db.commit()
return {"results": results}
@router.post("/batch-scan")
async def batch_scan_keys(
body: dict,
_user: User = Depends(require_role("owner", "operator")),
):
"""批量检查缺失密钥。"""
profile_names = body.get("profile_names")
if profile_names is not None and not isinstance(profile_names, list):
raise HTTPException(status_code=400, detail="profile_names 必须为数组")
results = scan_profiles(profile_names)
return {"results": results}
@router.get("/diff")
async def diff_keys(
profiles: str = Query("", description="逗号分隔的 Profile 名"),
_user: User = Depends(require_role("owner", "operator")),
):
"""差异视图。"""
profile_list = [p.strip() for p in profiles.split(",") if p.strip()] if profiles else list_profiles()
rows = get_diff(profile_list)
return {"profiles": profile_list, "rows": rows}
@router.get("/export", response_class=PlainTextResponse)
async def export_keys_csv(
profiles: str = Query("", description="逗号分隔的 Profile 名"),
_user: User = Depends(require_role("owner", "operator")),
):
"""CSV 导出批量检查结果。"""
profile_list = [p.strip() for p in profiles.split(",") if p.strip()] if profiles else None
return export_csv(profile_list)