v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
@@ -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();
|
||||
})();
|
||||
Reference in new issue
Block a user