This commit is contained in:
gru committed 2026-10-03 00:27:09 +01:00
commit 5e23e9af02
240 files changed
+99957

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
"use strict";
// Fixed same-origin schema + browser session; never query-configurable/CDN-based.
(() => {
const element = id => document.getElementById(id); let session = null, spec = null, ui = null, translations = {}, fallback = {}, epoch = 0;
let channel = null; try { channel = new BroadcastChannel("rustproxy-admin-session"); } catch { }
const t = key => translations[key] ?? fallback[key] ?? key;
function docsConfirm(message) {
return new Promise(resolve => {
const dialog = document.createElement("dialog"); dialog.className = "docs-confirm";
const title = document.createElement("h2"); title.textContent = t("confirmTitle");
const body = document.createElement("p"); body.textContent = message;
const actions = document.createElement("div"); actions.className = "docs-confirm-actions";
const cancel = document.createElement("button"); cancel.type = "button"; cancel.textContent = t("cancelAction");
const accept = document.createElement("button"); accept.type = "button"; accept.className = "docs-confirm-primary"; accept.textContent = t("continueAction");
actions.append(cancel, accept); dialog.append(title, body, actions); document.body.append(dialog);
let settled = false; const finish = value => { if (settled) return; settled = true; if (dialog.open) dialog.close(); dialog.remove(); resolve(value); };
cancel.addEventListener("click", () => finish(false)); accept.addEventListener("click", () => finish(true)); dialog.addEventListener("cancel", event => { event.preventDefault(); finish(false); }); dialog.addEventListener("close", () => finish(false));
dialog.showModal(); cancel.focus();
});
}
function clear() { epoch++; session = null; spec = null; ui = null; element("docs-writes").checked = false; element("swagger-ui").replaceChildren(); element("docs-session").textContent = ""; }
function expired() { clear(); element("docs-error").replaceChildren(); const link = document.createElement("a"); link.href = "/?next=docs"; link.textContent = t("docsExpired"); element("docs-error").append(link); }
channel?.addEventListener("message", event => { if (event.data === "locked") expired(); });
async function get(path) { const response = await fetch(path, { credentials: "same-origin", cache: "no-store", redirect: "error" }); if (!response.ok) { if (response.status === 401) expired(); throw new Error(t("docsError")); } return response.json(); }
async function checkSession() { const mine = epoch; const info = await get("/api/auth/session"); if (mine !== epoch || !session || info.csrf_token !== session.csrf_token || info.username !== session.username) { expired(); throw new Error(t("docsExpired")); } return info; }
async function requestInterceptor(request) {
if (!session || !spec) throw new Error(t("docsExpired"));
const url = new URL(request.url, location.origin), method = (request.method || "GET").toLowerCase();
if (url.origin !== location.origin || url.username || url.password || !Object.hasOwn(spec.paths, url.pathname) || !Object.hasOwn(spec.paths[url.pathname], method)) throw new Error("Only documented same-origin operations are allowed.");
if (["/api/auth/login", "/api/auth/setup", "/api/events"].includes(url.pathname)) throw new Error(t("docsReadOnly"));
await checkSession();
request.credentials = "same-origin"; request.headers = { ...request.headers, "X-RustProxy-Request": "1" }; request.cache = "no-store"; request.redirect = "error";
if (!["get", "head"].includes(method)) {
if (!element("docs-writes").checked || !(await docsConfirm(t("docsWriteConfirm")))) throw new Error(t("docsReadOnly"));
request.headers["X-CSRF-Token"] = session.csrf_token;
}
return request;
}
function render() {
if (!session || !spec) return;
// Disable the online validator and URL/config overrides. No persisted auth or
// externally supplied schema can send cookies or CSRF tokens elsewhere.
// These mandatory wire headers are supplied by the browser/interceptor, not
// typed into Swagger fields. Keep the downloadable specification accurate.
const displaySpec = structuredClone(spec);
for (const path of Object.values(displaySpec.paths)) for (const [method, operation] of Object.entries(path)) {
if (["get", "post", "put", "delete", "patch", "head", "options"].includes(method) && operation.parameters)
operation.parameters = operation.parameters.filter(p => !(p.in === "header" && ["origin", "x-rustproxy-request"].includes(p.name.toLowerCase())));
}
ui = SwaggerUIBundle({
spec: displaySpec, dom_id: "#swagger-ui", layout: "BaseLayout", presets: [SwaggerUIBundle.presets.apis],
validatorUrl: null, queryConfigEnabled: false, persistAuthorization: false, withCredentials: true,
supportedSubmitMethods: element("docs-writes").checked ? ["get", "post", "put"] : ["get"],
docExpansion: "none", filter: true, displayRequestDuration: true, tryItOutEnabled: false, showMutatedRequest: false,
requestInterceptor, responseInterceptor: response => { if (response.status === 401) expired(); return response; }
});
}
element("docs-writes").addEventListener("change", render);
element("docs-logout").addEventListener("click", async () => { if (!session) return; try { await checkSession(); const response = await fetch("/api/auth/logout", { method: "POST", credentials: "same-origin", cache: "no-store", redirect: "error", headers: { "X-RustProxy-Request": "1", "X-CSRF-Token": session.csrf_token } }); if (!response.ok && response.status !== 401) throw new Error(t("docsError")); channel?.postMessage("locked"); expired(); } catch (error) { element("docs-error").textContent = error.message; } });
async function boot() {
const mine = ++epoch; try {
const info = await get("/api/auth/session"); if (mine !== epoch) return;
const [document, bundle] = await Promise.all([get("/docs/openapi.json"), get("/assets/langs.json")]); if (mine !== epoch) return;
const packs = bundle.languages || []; fallback = packs.find(p => p.id === bundle.default)?.translations || {}; let selected = null; try { selected = localStorage.getItem("rp-language"); } catch { }
const pack = packs.find(p => p.id === selected) || packs.find(p => p.id === bundle.default); translations = pack?.translations || {};
window.document.documentElement.lang = pack?.meta.locale || "und"; window.document.documentElement.dir = pack?.meta.direction || "ltr";
window.document.querySelectorAll("[data-i18n]").forEach(n => n.textContent = t(n.dataset.i18n));
session = info; spec = document; element("docs-session").textContent = `${t("docsAccount")}: ${session.username}`; element("docs-error").textContent = ""; render();
} catch (error) { if (mine === epoch) { clear(); element("docs-error").textContent = error.message; } }
}
document.addEventListener("visibilitychange", () => { if (!document.hidden && session) void checkSession().catch(() => expired()); });
window.addEventListener("pageshow", event => { if (event.persisted) { clear(); void boot(); } });
setInterval(() => { if (!document.hidden && session) void checkSession().catch(() => expired()); }, 30000);
void boot();
})();