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>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user