Files
RustProxy/src/admin/web/login.js
T
2026-10-08 09:42:10 +02:00

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")); }
})();
})();