80 lines
6.9 KiB
JavaScript
80 lines
6.9 KiB
JavaScript
"use strict";
|
|
// No operational API or dashboard module is loaded on this document.
|
|
(() => {
|
|
const node = id => document.getElementById(id);
|
|
const themes = new Set(["dark", "light"]);
|
|
try { const cached = localStorage.getItem("rp-theme"); document.documentElement.dataset.theme = themes.has(cached) ? cached : "dark"; } catch { document.documentElement.dataset.theme = "dark"; }
|
|
const panelPaths = new Set(["/overview", "/vhosts", "/docker-vhosts", "/redirects", "/404-hosts", "/streams", "/acl-profiles", "/configuration", "/tls", "/traffic", "/cache", "/transform", "/logging", "/logs", "/settings"]);
|
|
const settingsTabs = new Set(["appearance", "listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "geoip", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
|
|
let packs = new Map(), fallback = null, language = null, setupRequired = false, ready = false;
|
|
try { language = localStorage.getItem("rp-language"); } catch { }
|
|
const t = key => packs.get(language)?.translations?.[key] ?? packs.get(fallback)?.translations?.[key] ?? key;
|
|
const reveal = () => document.body.classList.remove("auth-loading");
|
|
function message(value) { const box = node("auth-message"); box.textContent = value; box.classList.toggle("error", Boolean(value)); }
|
|
function panelDestination() {
|
|
const params = new URLSearchParams(location.search);
|
|
if (params.get("next") === "docs") return "/docs";
|
|
if (!panelPaths.has(location.pathname)) return "/";
|
|
if (location.pathname === "/logging") return "/settings?tab=logging";
|
|
if (location.pathname === "/settings") {
|
|
const tab = params.get("tab");
|
|
if (settingsTabs.has(tab)) return `/settings?tab=${encodeURIComponent(tab)}`;
|
|
}
|
|
return location.pathname;
|
|
}
|
|
async function request(path, body) {
|
|
const response = await fetch(path, { method: body ? "POST" : "GET", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "Content-Type": "application/json", "X-RustProxy-Request": "1" }, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
let value; try { value = await response.json(); } catch { value = { error: "auth.unavailable" }; }
|
|
if (!response.ok) { const error = new Error(t(value.error || "auth.unavailable")); error.code = value.error; throw error; } return value;
|
|
}
|
|
function translate() {
|
|
const pack = packs.get(language) || packs.get(fallback); document.documentElement.lang = pack?.meta.locale || "und"; document.documentElement.dir = pack?.meta.direction || "ltr";
|
|
document.querySelectorAll("[data-i18n]").forEach(n => n.textContent = t(n.dataset.i18n));
|
|
document.querySelectorAll("[data-i18n-aria]").forEach(n => n.setAttribute("aria-label", t(n.dataset.i18nAria)));
|
|
node("setup-fields").hidden = !setupRequired; node("confirm-field").hidden = !setupRequired;
|
|
node("setup-token").required = setupRequired; node("password-confirm").required = setupRequired;
|
|
node("password").minLength = setupRequired ? 12 : 1; node("password").autocomplete = setupRequired ? "new-password" : "current-password";
|
|
node("auth-title").textContent = t(setupRequired ? "setupTitle" : "signIn"); node("login-submit").textContent = t(setupRequired ? "createAndSignIn" : "signIn");
|
|
node("auth-badge").textContent = setupRequired ? "SETUP" : "LOCAL";
|
|
}
|
|
function selectLanguage(bundle) {
|
|
if (!Array.isArray(bundle.languages) || !bundle.languages.length) throw new Error("auth.unavailable");
|
|
packs = new Map(bundle.languages.map(p => [p.id, p])); fallback = bundle.default; if (!packs.has(fallback)) throw new Error("auth.unavailable");
|
|
const values = [...packs.values()], candidates = [language, ...(navigator.languages || [navigator.language])].filter(Boolean).map(s => s.replaceAll("_", "-").toLowerCase());
|
|
language = candidates.map(v => values.find(p => p.id.replaceAll("_", "-").toLowerCase() === v || p.meta.locale.toLowerCase() === v) || values.find(p => p.meta.locale.toLowerCase().split("-")[0] === v.split("-")[0])).find(Boolean)?.id || fallback;
|
|
node("language").replaceChildren(...values.map(p => { const option = document.createElement("option"); option.value = p.id; option.textContent = p.meta.native_name; return option; })); node("language").value = language;
|
|
}
|
|
node("language").addEventListener("change", () => { language = node("language").value; try { localStorage.setItem("rp-language", language); } catch { } message(""); translate(); });
|
|
node("login-submit").disabled = true;
|
|
node("login-form").addEventListener("submit", async event => {
|
|
event.preventDefault(); if (!ready) return; const button = node("login-submit"); button.disabled = true; message("");
|
|
try {
|
|
const username = node("username").value.trim(), password = node("password").value;
|
|
if (setupRequired) { if (password !== node("password-confirm").value) throw new Error(t("passwordMismatch")); await request("/api/auth/setup", { token: node("setup-token").value.trim(), username, password }); setupRequired = false; translate(); }
|
|
await request("/api/auth/login", { username, password });
|
|
node("password").value = ""; node("password-confirm").value = ""; node("setup-token").value = "";
|
|
// Never accept arbitrary return URLs (including scheme-relative redirects).
|
|
// A direct panel route is an exact local allowlist entry, so an expired
|
|
// session can sign back in without losing the requested subpage.
|
|
location.replace(panelDestination());
|
|
} catch (error) { node("password").value = ""; node("password-confirm").value = ""; if (error.code === "auth.already_initialized") { setupRequired = false; translate(); } message(error.message); }
|
|
finally { button.disabled = false; }
|
|
});
|
|
async function redirectExistingSession() {
|
|
try { await request("/api/auth/session"); location.replace(panelDestination()); return true; }
|
|
catch (error) { if (error.code === "auth.required") return false; throw error; }
|
|
}
|
|
window.addEventListener("pageshow", event => {
|
|
node("password").value = ""; node("password-confirm").value = ""; node("setup-token").value = "";
|
|
if (event.persisted && ready && !setupRequired) void redirectExistingSession().catch(() => message(t("auth.unavailable")));
|
|
});
|
|
(async () => {
|
|
try {
|
|
const [bundle, status] = await Promise.all([request("/assets/login-langs.json"), request("/api/auth/status")]);
|
|
selectLanguage(bundle); setupRequired = status.setup_required === true; translate();
|
|
if (!setupRequired && await redirectExistingSession()) return;
|
|
ready = true; node("login-submit").disabled = false; reveal();
|
|
} catch (error) { reveal(); message(t("auth.unavailable")); }
|
|
})();
|
|
})();
|