This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 09:29:10 +02:00
1 parent 2e4aa4df4d
commit 87e571c170
106 files changed
+17952 -6185

No files matched your search

+121 -13
View File
@@ -1,9 +1,11 @@
"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;
const element = id => document.getElementById(id);
let session = null, spec = null, ui = null, translations = {}, fallback = {}, epoch = 0;
let documents = [], docsByPath = new Map(), currentDocument = null, activeTab = "reader";
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";
@@ -18,11 +20,16 @@
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 clear() {
epoch++; session = null; spec = null; ui = null; documents = []; docsByPath = new Map(); currentDocument = null;
element("docs-writes").checked = false; element("swagger-ui").replaceChildren(); element("docs-reader-list").replaceChildren(); element("docs-reader-content").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();
@@ -36,12 +43,9 @@
}
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.
function renderApi() {
if (!session || !spec || typeof SwaggerUIBundle !== "function") return;
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)
@@ -55,17 +59,121 @@
requestInterceptor, responseInterceptor: response => { if (response.status === 401) expired(); return response; }
});
}
element("docs-writes").addEventListener("change", render);
function normalizeDocPath(base, target) {
const clean = target.split("#", 1)[0].split("?", 1)[0];
if (!clean) return base;
const stack = clean.startsWith("/") ? [] : base.split("/").slice(0, -1);
for (const part of clean.replace(/^\//, "").split("/")) {
if (!part || part === ".") continue;
if (part === "..") { if (stack.length) stack.pop(); continue; }
stack.push(part);
}
return stack.join("/");
}
function appendInline(parent, text, basePath) {
const pattern = /(`[^`]+`|\[[^\]]+\]\([^)]+\))/g; let offset = 0;
for (const match of text.matchAll(pattern)) {
if (match.index > offset) parent.append(document.createTextNode(text.slice(offset, match.index)));
const token = match[0];
if (token.startsWith("`")) {
const code = document.createElement("code"); code.textContent = token.slice(1, -1); parent.append(code);
} else {
const parsed = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(token); const anchor = document.createElement("a");
anchor.textContent = parsed[1]; const target = parsed[2].trim();
if (/^https?:\/\//i.test(target)) { anchor.href = target; anchor.target = "_blank"; anchor.rel = "noreferrer"; }
else if (target.startsWith("#")) { anchor.href = target; }
else {
const resolved = normalizeDocPath(basePath, target);
if (docsByPath.has(resolved)) {
anchor.href = `#doc=${encodeURIComponent(resolved)}`;
anchor.addEventListener("click", event => { event.preventDefault(); selectDocument(resolved, true); });
} else { anchor.href = target; }
}
parent.append(anchor);
}
offset = match.index + token.length;
}
if (offset < text.length) parent.append(document.createTextNode(text.slice(offset)));
}
function renderMarkdown(documentInfo) {
const target = element("docs-reader-content"); target.replaceChildren();
const lines = documentInfo.content.replaceAll("\r\n", "\n").split("\n");
let code = null, list = null, paragraph = [];
const flushParagraph = () => { if (!paragraph.length) return; const p = document.createElement("p"); appendInline(p, paragraph.join(" ").trim(), documentInfo.path); target.append(p); paragraph = []; };
const closeList = () => { list = null; };
for (const line of lines) {
if (line.startsWith("```")) {
flushParagraph(); closeList();
if (code) { target.append(code.pre); code = null; }
else { const pre = document.createElement("pre"), node = document.createElement("code"); pre.append(node); code = { pre, node, lines: [] }; }
continue;
}
if (code) { code.lines.push(line); code.node.textContent = code.lines.join("\n"); continue; }
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
if (heading) { flushParagraph(); closeList(); const h = document.createElement(`h${heading[1].length}`); appendInline(h, heading[2], documentInfo.path); target.append(h); continue; }
const bullet = /^\s*[-*]\s+(.+)$/.exec(line), ordered = /^\s*\d+\.\s+(.+)$/.exec(line);
if (bullet || ordered) {
flushParagraph(); const tag = ordered ? "ol" : "ul";
if (!list || list.tagName.toLowerCase() !== tag) { list = document.createElement(tag); target.append(list); }
const item = document.createElement("li"); appendInline(item, (bullet || ordered)[1], documentInfo.path); list.append(item); continue;
}
if (/^\s*>\s?/.test(line)) { flushParagraph(); closeList(); const q = document.createElement("blockquote"); appendInline(q, line.replace(/^\s*>\s?/, ""), documentInfo.path); target.append(q); continue; }
if (/^\s*---+\s*$/.test(line)) { flushParagraph(); closeList(); target.append(document.createElement("hr")); continue; }
if (!line.trim()) { flushParagraph(); closeList(); continue; }
closeList(); paragraph.push(line.trim());
}
flushParagraph(); if (code) target.append(code.pre);
}
const categoryLabel = category => t({ project: "docsCategoryProject", guides: "docsCategoryGuides", technical: "docsCategoryTechnical", audits: "docsCategoryAudits", historical: "docsCategoryHistorical" }[category] || category);
function renderReaderList() {
const query = (element("docs-reader-search").value || "").trim().toLowerCase();
const root = element("docs-reader-list"); root.replaceChildren();
const matches = documents.filter(item => !query || `${item.title} ${item.path}`.toLowerCase().includes(query));
for (const category of ["project", "guides", "technical", "audits", "historical"]) {
const group = matches.filter(item => item.category === category); if (!group.length) continue;
const heading = document.createElement("div"); heading.className = "reader-group"; heading.textContent = categoryLabel(category); root.append(heading);
for (const item of group) {
const button = document.createElement("button"); button.type = "button"; button.classList.toggle("active", item.path === currentDocument?.path);
const title = document.createElement("span"); title.textContent = item.title; const path = document.createElement("small"); path.textContent = item.path;
button.append(title, path); button.addEventListener("click", () => selectDocument(item.path, true)); root.append(button);
}
}
if (!root.children.length) { const empty = document.createElement("div"); empty.className = "reader-empty"; empty.textContent = t("docsReaderEmpty"); root.append(empty); }
}
function selectDocument(path, updateHash = false) {
const item = docsByPath.get(path) || docsByPath.get("docs/README.md") || documents[0]; if (!item) return;
currentDocument = item; renderReaderList(); renderMarkdown(item);
if (updateHash) history.replaceState(null, "", `#doc=${encodeURIComponent(item.path)}`);
}
function selectTab(tab) {
activeTab = tab === "api" ? "api" : "reader";
element("docs-reader-pane").hidden = activeTab !== "reader"; element("docs-api-pane").hidden = activeTab !== "api";
element("docs-tab-reader").classList.toggle("active", activeTab === "reader"); element("docs-tab-api").classList.toggle("active", activeTab === "api");
if (activeTab === "api") renderApi();
}
element("docs-writes").addEventListener("change", renderApi);
element("docs-reader-search").addEventListener("input", renderReaderList);
element("docs-tab-reader").addEventListener("click", () => selectTab("reader"));
element("docs-tab-api").addEventListener("click", () => selectTab("api"));
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 [documentSpec, bundle, docBundle] = await Promise.all([get("/docs/openapi.json"), get("/assets/langs.json"), get("/docs/content.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();
window.document.querySelectorAll("[data-i18n]").forEach(n => n.textContent = t(n.dataset.i18n)); window.document.querySelectorAll("[data-i18n-placeholder]").forEach(n => n.placeholder = t(n.dataset.i18nPlaceholder));
session = info; spec = documentSpec; documents = Array.isArray(docBundle.documents) ? docBundle.documents : []; docsByPath = new Map(documents.map(item => [item.path, item]));
element("docs-session").textContent = `${t("docsAccount")}: ${session.username}`; element("docs-error").textContent = "";
const hash = new URLSearchParams(location.hash.replace(/^#/, "")).get("doc"); selectDocument(hash && docsByPath.has(hash) ? hash : "docs/README.md"); selectTab("reader");
} catch (error) { if (mine === epoch) { clear(); element("docs-error").textContent = error.message; } }
}
document.addEventListener("visibilitychange", () => { if (!document.hidden && session) void checkSession().catch(() => expired()); });