"""后台管理:配置项 CRUD(settings)与运行日志查询。 所有接口需管理员鉴权(require_admin)。路由前缀 /api/v1/admin。 配置项白名单见 src/core/runtime_config.py SETTING_DEFS,之外的 key 一律拒绝。 """ from __future__ import annotations import logging from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from src.api.deps import require_admin from src.core.log_buffer import get_entries from src.core.runtime_config import ( SETTING_DEFS, clear_runtime_value, get_setting_origin, mask_value, set_runtime_value, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v1/admin", tags=["admin"], dependencies=[Depends(require_admin)]) class SettingUpdateIn(BaseModel): value: str @router.get("/settings") async def list_settings(): """全部可配置项(脱敏),供后台各配置页渲染。""" out = [] for key, defn in SETTING_DEFS.items(): origin, value = await get_setting_origin(key) out.append( { "key": key, "label": defn.label, "description": defn.description, "sensitive": defn.sensitive, "configured": origin != "none", "masked": mask_value(value, defn.sensitive), "origin": origin, } ) return out @router.get("/logs") async def read_logs( level: str | None = Query(None, description="最低级别: DEBUG/INFO/WARNING/ERROR"), keyword: str | None = Query(None, description="消息或 logger 关键字"), limit: int = Query(200, ge=1, le=1000), ): """查询应用运行日志(内存环形缓冲,最新在前;进程重启后清零)。""" entries = get_entries(level, keyword, limit) return {"entries": entries, "count": len(entries)} @router.put("/settings/{key}") async def update_setting(key: str, body: SettingUpdateIn): """更新配置项(写入 app_settings 覆盖 .env)。传空值请改用 DELETE。""" if key not in SETTING_DEFS: raise HTTPException(404, f"不支持的配置项: {key}") value = body.value.strip() if not value: raise HTTPException(400, "值不能为空;如需回落 .env 请调用清除接口") await set_runtime_value(key, value) defn = SETTING_DEFS[key] return {"key": key, "masked": mask_value(value, defn.sensitive), "origin": "db"} @router.delete("/settings/{key}") async def clear_setting(key: str): """清除 DB 覆盖值,回落 .env 默认。""" if key not in SETTING_DEFS: raise HTTPException(404, f"不支持的配置项: {key}") await clear_runtime_value(key) origin, value = await get_setting_origin(key) defn = SETTING_DEFS[key] return { "key": key, "masked": mask_value(value, defn.sensitive), "origin": origin, }