// 系统设置: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 (
{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 });