diff --git a/helm/openbao-ui-overlay/nginx.conf b/helm/openbao-ui-overlay/nginx.conf index 15a1e67..8facdf3 100644 --- a/helm/openbao-ui-overlay/nginx.conf +++ b/helm/openbao-ui-overlay/nginx.conf @@ -26,6 +26,16 @@ http { add_header Cache-Control "public, max-age=300"; } + # Static UI bundles and API calls bypass HTML injection and stay compressed. + location ~ ^/(v1|ui/assets|ui/engines-dist|ui/favicon\.svg) { + proxy_pass http://openbao_upstream; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location / { proxy_pass http://openbao_upstream; proxy_http_version 1.1; @@ -33,7 +43,7 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - # Disable upstream compression so sub_filter can rewrite HTML. + # Disable upstream compression only for HTML shell injection. proxy_set_header Accept-Encoding ""; proxy_buffering on; diff --git a/helm/openbao-ui-overlay/overlay.js b/helm/openbao-ui-overlay/overlay.js index 52c83a4..d6a2fe8 100644 --- a/helm/openbao-ui-overlay/overlay.js +++ b/helm/openbao-ui-overlay/overlay.js @@ -2,6 +2,8 @@ "use strict"; const PRESETS_URL = "/ui/platform-overlay/presets.json"; + const MAX_APPLY_ATTEMPTS = 40; + const APPLY_INTERVAL_MS = 250; const DEFAULT_PRESETS = { namespace: "", method: "oidc", @@ -14,6 +16,9 @@ }; let presets = { ...DEFAULT_PRESETS }; + let applyAttempts = 0; + let applyTimer = null; + let overlayApplied = false; function isAuthPage() { const path = window.location.pathname; @@ -24,19 +29,23 @@ } function hideNode(node) { - if (!node) return; + if (!node || node.dataset.keycapeOverlayHidden === "true") return; const field = node.closest(".field.is-horizontal") || node.closest(".field") || node.closest(".box") || node; + if (field.dataset.keycapeOverlayHidden === "true") return; field.style.display = "none"; field.setAttribute("aria-hidden", "true"); + field.dataset.keycapeOverlayHidden = "true"; } function setInputValue(input, value) { - if (!input || input.value === value) return; + if (!input || input.dataset.keycapeOverlayPreset === value) return; input.value = value; + input.dataset.keycapeOverlayPreset = value; + // Fire once so Ember picks up the preset without a mutation feedback loop. input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); } @@ -62,8 +71,16 @@ } } + function loginShellReady() { + return Boolean( + document.querySelector(".login-form") || + document.querySelector(".auth-form") || + document.querySelector(".toolbar-namespace-picker") + ); + } + function applyDom() { - if (!isAuthPage()) return; + if (!isAuthPage() || overlayApplied) return false; hideNode(document.querySelector(".toolbar-namespace-picker")); document @@ -85,8 +102,10 @@ .forEach(hideNode); document.querySelectorAll("nav.tabs").forEach((el) => { + if (el.dataset.keycapeOverlayHidden === "true") return; el.style.display = "none"; el.setAttribute("aria-hidden", "true"); + el.dataset.keycapeOverlayHidden = "true"; }); document @@ -94,7 +113,10 @@ .forEach(hideNode); document.querySelectorAll("h1.title.is-3").forEach((heading) => { - if (/Sign in to OpenBao|Authenticate/.test(heading.textContent)) { + if ( + /Sign in to OpenBao|Authenticate/.test(heading.textContent) && + heading.textContent !== presets.title + ) { heading.textContent = presets.title; } }); @@ -102,7 +124,9 @@ document .querySelectorAll('#auth-submit, button[data-test="auth-submit"]') .forEach((button) => { - button.textContent = presets.signInLabel; + if (button.textContent !== presets.signInLabel) { + button.textContent = presets.signInLabel; + } }); document @@ -111,7 +135,9 @@ document .querySelectorAll('#role, input[name="role"]') - .forEach((input) => setInputValue(input, presets.role || "platform-admin")); + .forEach((input) => + setInputValue(input, presets.role || "platform-admin") + ); if (!document.getElementById("keycape-overlay-banner")) { const banner = document.createElement("div"); @@ -125,6 +151,36 @@ } document.documentElement.classList.add("keycape-overlay-active"); + + if (loginShellReady()) { + overlayApplied = true; + return true; + } + + return false; + } + + function stopApplyLoop() { + if (applyTimer !== null) { + window.clearInterval(applyTimer); + applyTimer = null; + } + } + + function scheduleApply() { + stopApplyLoop(); + applyAttempts = 0; + + const tick = () => { + applyAttempts += 1; + if (applyDom() || applyAttempts >= MAX_APPLY_ATTEMPTS) { + stopApplyLoop(); + return; + } + }; + + tick(); + applyTimer = window.setInterval(tick, APPLY_INTERVAL_MS); } async function loadPresets() { @@ -138,12 +194,6 @@ } } - function observe() { - const observer = new MutationObserver(() => applyDom()); - observer.observe(document.body, { childList: true, subtree: true }); - applyDom(); - } - async function init() { await loadPresets(); if (!isAuthPage()) return; @@ -151,9 +201,11 @@ ensureAuthMountSelected(); if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", observe); + document.addEventListener("DOMContentLoaded", scheduleApply, { + once: true, + }); } else { - observe(); + scheduleApply(); } }