// 系统设置:admin 后台管理 system_configs(仅 roleId=1) // 后端 /api/admin/configs,权限闸:require_admin (role_id==1 或 BOOTSTRAP_ADMIN_USER_IDS) const CATEGORY_META = { business_credentials: { label: "业务凭据", desc: "敏感字段;Fernet 加密落库;改完即时生效" }, external_apis: { label: "第三方接口", desc: "外部 API 的 base URL / path;改完 ≤30s 缓存生效" }, business_params: { label: "业务参数", desc: "运行时可调;改完 ≤30s 缓存生效" }, storage: { label: "S3 / 对象存储", desc: "前端拿到的对象 URL 基址(公开链接)" }, cors: { label: "CORS / 安全", desc: "⚠️ 改 CORS 后需要重启容器才能生效" }, }; const CATEGORY_ORDER = ["business_credentials", "external_apis", "business_params", "storage", "cors"]; function _isAdmin() { const u = Auth.getUser() || {}; return u.roleId === 1; } function _fmtTime(iso) { if (!iso) return ""; try { const d = new Date(iso); if (isNaN(d.getTime())) return iso; const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; } catch (_) { return iso; } } const AdminConfigsPage = () => { // 前端兜底:非 admin 直接 403 占位(后端也会拒,但这里给个友好提示) if (!_isAdmin()) { return (
403 仅管理员可访问
当前账号 roleId 不为 1。请联系运维设置管理员角色,或临时通过容器 BOOTSTRAP_ADMIN_USER_IDS env 加入白名单。
); } const [items, setItems] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(""); const [draft, setDraft] = React.useState({}); // {key: newValue} 未保存 const [revealed, setRevealed] = React.useState({}); // {key: plaintext} 已解密 const [saving, setSaving] = React.useState({}); // {key: true} 正在保存 const [flash, setFlash] = React.useState({}); // {key: "saved"|"error"} 短暂提示 React.useEffect(() => { reload(); }, []); async function reload() { setLoading(true); setError(""); try { const r = await API.admin.configs.list(); setItems(r.items || []); } catch (e) { setError(String(e.message || e)); } finally { setLoading(false); } } async function onReveal(key) { try { const r = await API.admin.configs.reveal(key); setRevealed((s) => ({ ...s, [key]: r.value || "" })); setDraft((s) => ({ ...s, [key]: r.value || "" })); } catch (e) { setError(`显示 ${key} 失败:${e.message || e}`); } } function onHide(key) { setRevealed((s) => { const x = { ...s }; delete x[key]; return x; }); setDraft((s) => { const x = { ...s }; delete x[key]; return x; }); } async function onSave(item) { const v = draft[item.key] ?? ""; setSaving((s) => ({ ...s, [item.key]: true })); setError(""); try { await API.admin.configs.update(item.key, v); setFlash((s) => ({ ...s, [item.key]: "saved" })); setTimeout(() => setFlash((s) => { const x = { ...s }; delete x[item.key]; return x; }), 2400); await reload(); // 保存后默认重新 mask(除非用户还在显示状态) if (item.is_secret && !revealed[item.key]) { setDraft((s) => { const x = { ...s }; delete x[item.key]; return x; }); } } catch (e) { setError(`保存 ${item.key} 失败:${e.message || e}`); setFlash((s) => ({ ...s, [item.key]: "error" })); setTimeout(() => setFlash((s) => { const x = { ...s }; delete x[item.key]; return x; }), 3000); } finally { setSaving((s) => { const x = { ...s }; delete x[item.key]; return x; }); } } const grouped = React.useMemo(() => { const m = {}; for (const it of items) { const c = it.category || "general"; if (!m[c]) m[c] = []; m[c].push(it); } return m; }, [items]); return (
{loading ? "加载..." : "刷新"}} />
{error && (
{error}
)}
运行时配置中心。改动落库后通过 30s in-memory 缓存广播,consumer 自动热生效(除 CORS)。 所有 secret 走 Fernet 加密;DB 行清空 → 自动回退到容器 env。
{loading && items.length === 0 && (
加载中…
)} {CATEGORY_ORDER.map((cat) => { const list = grouped[cat]; if (!list || list.length === 0) return null; const meta = CATEGORY_META[cat] || { label: cat, desc: "" }; return (
{meta.label}
{meta.desc}
{list.map((it) => { const isRevealed = it.key in revealed; const draftVal = draft[it.key]; // 显示用:secret 未 reveal 时显示 mask;reveal 后或非 secret 都用 draft(或当前 value) const displayValue = it.is_secret && !isRevealed ? (draftVal !== undefined ? draftVal : (it.has_value ? "********" : "")) : (draftVal !== undefined ? draftVal : (it.value || "")); const dirty = draftVal !== undefined && draftVal !== (it.is_secret && !isRevealed ? (it.has_value ? "********" : "") : (it.value || "")); const isSaving = !!saving[it.key]; const flashType = flash[it.key]; return (
{it.label}
{it.key} {it.is_secret && ( secret )} {it.value_type !== "str" && ( {it.value_type} )} {it.key === "CORS_ORIGINS" && ( 需重启容器生效 )}
{it.updated_at && ( 上次更新 {_fmtTime(it.updated_at)}{it.updated_by ? ` · uid=${it.updated_by}` : ""} )}
{it.description && (
{it.description}
)}
setDraft((s) => ({ ...s, [it.key]: e.target.value }))} style={{ flex: 1, fontSize: 13, padding: "6px 10px", background: "var(--bg-0)", color: "var(--fg-1)", border: "1px solid var(--line-1)", borderRadius: 4, fontFamily: it.is_secret ? "var(--font-mono)" : "var(--font-sans)", }} disabled={isSaving} /> {it.is_secret && ( isRevealed ? : )} {flashType === "saved" && ( ✓ 已保存 )} {flashType === "error" && ( ✗ 失败 )}
); })}
); })}
); }; Object.assign(window, { AdminConfigsPage });