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