314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
(function initSub2ApiAdminCommon(global) {
|
||
const ADMIN_LINKS = [
|
||
{ key: "home", href: "/portal/admin/", label: "管理首页" },
|
||
{ key: "logical-groups", href: "/portal/admin/logical-groups.html", label: "逻辑分组 / 路由" },
|
||
{ key: "route-health", href: "/portal/admin/route-health.html", label: "Route 健康视图" },
|
||
{ key: "accounts", href: "/portal/admin/accounts.html", label: "帐号资产" },
|
||
{ key: "providers", href: "/portal/admin/providers.html", label: "新增模型 / 供应商目录" },
|
||
{ key: "batch-import", href: "/portal/admin/batch-import.html", label: "导入供应商帐号" },
|
||
{ key: "portal", href: "/portal/", label: "用户 Portal", target: "_blank", rel: "noreferrer" },
|
||
];
|
||
|
||
function escapeHTML(value) {
|
||
return String(value ?? "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function defaultApiBase() {
|
||
const origin = global.location && typeof global.location.origin === "string" ? global.location.origin : "";
|
||
if (!origin || origin === "null") {
|
||
return "/portal-admin-api";
|
||
}
|
||
return `${origin}/portal-admin-api`;
|
||
}
|
||
|
||
function normalizeApiBase(value) {
|
||
return (String(value || "").trim() || defaultApiBase()).replace(/\/+$/, "");
|
||
}
|
||
|
||
function authHeaders(tokenValue) {
|
||
const token = String(tokenValue || "").trim();
|
||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||
}
|
||
|
||
function normalizeTone(tone) {
|
||
const value = String(tone || "").trim().toLowerCase();
|
||
if (!value || value === "note" || value === "info") {
|
||
return "note";
|
||
}
|
||
if (value === "warn") {
|
||
return "warning";
|
||
}
|
||
if (value === "error") {
|
||
return "danger";
|
||
}
|
||
return value;
|
||
}
|
||
|
||
function setStatus(element, message, tone = "note") {
|
||
if (!element) {
|
||
return;
|
||
}
|
||
element.textContent = message;
|
||
const normalizedTone = normalizeTone(tone);
|
||
if (normalizedTone !== "note") {
|
||
element.setAttribute("data-tone", normalizedTone);
|
||
} else {
|
||
element.removeAttribute("data-tone");
|
||
}
|
||
}
|
||
|
||
function describeSessionPayload(payload, options = {}) {
|
||
const fallbackUsername = String(options.usernameFallback || "admin").trim() || "admin";
|
||
const currentUsername = String(payload?.username || "").trim();
|
||
const effectiveUsername = currentUsername || fallbackUsername;
|
||
if (payload?.authenticated) {
|
||
const suffix = options.includeSessionSuffix ? "(session)" : "";
|
||
return {
|
||
tone: "success",
|
||
message: `管理员会话已建立:${currentUsername || "unknown"}${suffix}`,
|
||
};
|
||
}
|
||
if (payload?.login_enabled) {
|
||
if (options.allowBearerFallback) {
|
||
return {
|
||
tone: "warning",
|
||
message: `当前未登录。可用管理员用户名 ${effectiveUsername} 建立 session,或继续使用 Bearer token。`,
|
||
};
|
||
}
|
||
return {
|
||
tone: "warning",
|
||
message: "当前未登录。可直接使用管理员用户名密码建立会话。",
|
||
};
|
||
}
|
||
return {
|
||
tone: "warning",
|
||
message: "当前实例未启用管理员登录,只能使用 Bearer token。",
|
||
};
|
||
}
|
||
|
||
function sessionStateSpec(kind, context = {}, options = {}) {
|
||
const username = String(context.username || options.usernameFallback || "unknown").trim() || "unknown";
|
||
const message = context.error instanceof Error ? context.error.message : String(context.error || "").trim();
|
||
switch (kind) {
|
||
case "missing_credentials":
|
||
return {
|
||
tone: "warning",
|
||
message: "请先输入管理员用户名和密码。",
|
||
};
|
||
case "login_success":
|
||
return {
|
||
tone: "success",
|
||
message: `管理员会话已建立:${username}`,
|
||
};
|
||
case "login_failed":
|
||
return {
|
||
tone: "danger",
|
||
message: `管理员登录失败:${message || "未知错误"}`,
|
||
};
|
||
case "logout_success":
|
||
return {
|
||
tone: "warning",
|
||
message: "管理员会话已退出。",
|
||
};
|
||
case "logout_failed":
|
||
return {
|
||
tone: "danger",
|
||
message: `退出会话失败:${message || "未知错误"}`,
|
||
};
|
||
case "check_failed":
|
||
return {
|
||
tone: "danger",
|
||
message: `检查管理员会话失败:${message || "未知错误"}`,
|
||
};
|
||
default:
|
||
return {
|
||
tone: "note",
|
||
message: String(context.message || ""),
|
||
};
|
||
}
|
||
}
|
||
|
||
function applySessionPayload(element, payload, options = {}) {
|
||
const status = describeSessionPayload(payload, options);
|
||
setStatus(element, status.message, status.tone);
|
||
return status;
|
||
}
|
||
|
||
function setSessionState(element, kind, context = {}, options = {}) {
|
||
const status = sessionStateSpec(kind, context, options);
|
||
setStatus(element, status.message, status.tone);
|
||
return status;
|
||
}
|
||
|
||
async function requestJSON(client, path, options = {}) {
|
||
const { skipAuth = false, headers = {}, ...rest } = options;
|
||
const finalHeaders = { Accept: "application/json", ...headers };
|
||
if (!skipAuth) {
|
||
Object.assign(finalHeaders, authHeaders(client.adminTokenInput && client.adminTokenInput.value));
|
||
}
|
||
const response = await fetch(`${normalizeApiBase(client.apiBaseInput && client.apiBaseInput.value)}${path}`, {
|
||
...rest,
|
||
credentials: "include",
|
||
headers: finalHeaders,
|
||
});
|
||
const text = await response.text();
|
||
let payload = {};
|
||
try {
|
||
payload = text ? JSON.parse(text) : {};
|
||
} catch {
|
||
payload = { raw: text };
|
||
}
|
||
if (!response.ok) {
|
||
const message = payload?.error?.message || payload?.message || payload?.error || payload?.raw || `HTTP ${response.status}`;
|
||
throw new Error(message);
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
function readStoredConfig(storageKey) {
|
||
try {
|
||
return JSON.parse(global.localStorage.getItem(storageKey) || "{}");
|
||
} catch (error) {
|
||
console.warn(`failed to parse ${storageKey}`, error);
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function writeStoredConfig(storageKey, payload) {
|
||
global.localStorage.setItem(storageKey, JSON.stringify(payload));
|
||
}
|
||
|
||
function renderAdminNav(container, currentKey) {
|
||
if (!container) {
|
||
return;
|
||
}
|
||
container.innerHTML = ADMIN_LINKS.map((link) => {
|
||
const currentClass = currentKey === link.key ? " is-current" : "";
|
||
const target = link.target ? ` target="${link.target}"` : "";
|
||
const rel = link.rel ? ` rel="${link.rel}"` : "";
|
||
return `<a href="${escapeHTML(link.href)}"${target}${rel} class="${currentClass.trim()}">${escapeHTML(link.label)}</a>`;
|
||
}).join("");
|
||
}
|
||
|
||
function createAdminPageRuntime(options) {
|
||
const client = {
|
||
apiBaseInput: options.apiBaseInput,
|
||
adminTokenInput: options.adminTokenInput,
|
||
};
|
||
const sessionPresentation = options.sessionPresentation || {};
|
||
|
||
async function refreshAdminSession() {
|
||
try {
|
||
const payload = await requestJSON(client, "/api/admin/session", {
|
||
skipAuth: !options.includeAuthOnSessionCheck,
|
||
});
|
||
if (payload.username && options.adminUsernameInput && !options.adminUsernameInput.value.trim()) {
|
||
options.adminUsernameInput.value = payload.username;
|
||
}
|
||
applySessionPayload(options.adminSessionStatus, payload, sessionPresentation);
|
||
return payload;
|
||
} catch (error) {
|
||
setSessionState(options.adminSessionStatus, "check_failed", { error }, sessionPresentation);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function loginAdminSession() {
|
||
const username = options.adminUsernameInput ? options.adminUsernameInput.value.trim() : "";
|
||
const password = options.adminPasswordInput ? options.adminPasswordInput.value : "";
|
||
if (!username || !password) {
|
||
const error = new Error("管理员用户名和密码不能为空");
|
||
setSessionState(options.adminSessionStatus, "missing_credentials", {}, sessionPresentation);
|
||
throw error;
|
||
}
|
||
try {
|
||
const payload = await requestJSON(client, "/api/admin/session/login", {
|
||
method: "POST",
|
||
skipAuth: true,
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ username, password }),
|
||
});
|
||
if (options.adminPasswordInput) {
|
||
options.adminPasswordInput.value = "";
|
||
}
|
||
if (typeof options.onSessionPersist === "function") {
|
||
options.onSessionPersist();
|
||
}
|
||
setSessionState(options.adminSessionStatus, "login_success", { username: payload.username || username }, sessionPresentation);
|
||
return payload;
|
||
} catch (error) {
|
||
setSessionState(options.adminSessionStatus, "login_failed", { error }, sessionPresentation);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function logoutAdminSession() {
|
||
try {
|
||
const response = await fetch(`${normalizeApiBase(options.apiBaseInput && options.apiBaseInput.value)}/api/admin/session/logout`, {
|
||
method: "POST",
|
||
credentials: "include",
|
||
headers: authHeaders(options.adminTokenInput && options.adminTokenInput.value),
|
||
});
|
||
if (!response.ok) {
|
||
const text = await response.text();
|
||
throw new Error(text || `HTTP ${response.status}`);
|
||
}
|
||
if (options.adminPasswordInput) {
|
||
options.adminPasswordInput.value = "";
|
||
}
|
||
setSessionState(options.adminSessionStatus, "logout_success", {}, sessionPresentation);
|
||
} catch (error) {
|
||
setSessionState(options.adminSessionStatus, "logout_failed", { error }, sessionPresentation);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
return {
|
||
defaultApiBase,
|
||
normalizeApiBase() {
|
||
return normalizeApiBase(options.apiBaseInput && options.apiBaseInput.value);
|
||
},
|
||
authHeaders() {
|
||
return authHeaders(options.adminTokenInput && options.adminTokenInput.value);
|
||
},
|
||
requestJSON(path, requestOptions = {}) {
|
||
return requestJSON(client, path, requestOptions);
|
||
},
|
||
readStoredConfig,
|
||
writeStoredConfig,
|
||
renderAdminNav,
|
||
setStatus,
|
||
applySessionPayload(element, payload) {
|
||
return applySessionPayload(element, payload, sessionPresentation);
|
||
},
|
||
setSessionState(element, kind, context = {}) {
|
||
return setSessionState(element, kind, context, sessionPresentation);
|
||
},
|
||
refreshAdminSession,
|
||
loginAdminSession,
|
||
logoutAdminSession,
|
||
};
|
||
}
|
||
|
||
global.Sub2ApiAdminCommon = {
|
||
ADMIN_LINKS,
|
||
createAdminPageRuntime,
|
||
defaultApiBase,
|
||
normalizeApiBase,
|
||
authHeaders,
|
||
normalizeTone,
|
||
setStatus,
|
||
describeSessionPayload,
|
||
applySessionPayload,
|
||
setSessionState,
|
||
readStoredConfig,
|
||
writeStoredConfig,
|
||
renderAdminNav,
|
||
};
|
||
})(window);
|