Files
computer-playbook/roles/web-app-baserow/templates/javascript.js.j2
kevinveenbirkenbachandClaude Opus 5 9d0f618e23 refactor(sso): compute the app logout target once in the sso lookup
The previous commit wrote the same proxy-aware logout expression into
four javascript.js.j2 templates. The rule it encodes - sign out of
oauth2-proxy first when the app is proxy-gated, else go straight to the
provider - belongs where proxy-gating is already decided.

utils/roles/applications/services/sso.py gains logout_url(url,
is_proxy_gated) next to get_sso_config, with OAUTH2_SIGN_OUT_PATH as the
path constant; the rd value is quoted exactly as Jinja's urlencode
filter does it. The sso lookup exposes it as the logout_url term: it
renders OIDC through the templar, takes is_proxy_gated from the same
resolution, and refuses a context without OIDC. Prometheus, Checkmk,
Baserow and Jitsi now render lookup('sso', application_id, 'logout_url').

Behaviour is unchanged: the resolver tests pin the rd encoding against
jinja2's do_urlencode, and the lookup tests cover a proxy-gated app, an
OIDC-native app, a disabled SSO and a missing OIDC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 13:06:19 +02:00

122 lines
3.9 KiB
Django/Jinja

(function () {
const LOGOUT_URL = {{ lookup('sso', application_id, 'logout_url') | to_json }};
const INJECTED_ATTR = "data-injected-logout";
const LOGOUT_SVG =
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' +
'<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>' +
'<polyline points="10 17 15 12 10 7"/>' +
'<line x1="15" y1="12" x2="3" y2="12"/>' +
"</svg>";
function findIconGroupAnchor() {
const banner =
document.querySelector(
"header, [role='banner'], .navbar, nav.navbar, nav.menu-header",
) || document.querySelector("nav");
if (!banner) return null;
let best = null;
let bestRight = -1;
banner.querySelectorAll("div, ul, nav").forEach((el) => {
const directControls = el.querySelectorAll(
":scope > button, :scope > a, :scope > li > button, :scope > li > a",
);
if (directControls.length < 2) return;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
if (rect.right > bestRight) {
best = el;
bestRight = rect.right;
}
});
return best || banner;
}
function ensureLogoutIcon() {
if (document.querySelector("[" + INJECTED_ATTR + "]")) return;
if (!document.body) return;
const anchor = findIconGroupAnchor();
const sibling = anchor
? anchor.querySelector(
":scope > button, :scope > a, :scope > li > button, :scope > li > a",
)
: null;
const btn = document.createElement(
sibling && sibling.tagName === "BUTTON" ? "button" : "a",
);
btn.setAttribute(INJECTED_ATTR, "true");
if (btn.tagName === "A") btn.setAttribute("href", LOGOUT_URL);
btn.setAttribute("title", "Logout");
btn.setAttribute("aria-label", "Logout");
btn.setAttribute("type", btn.tagName === "BUTTON" ? "button" : "");
if (sibling) {
btn.className = sibling.className;
const cs = window.getComputedStyle(sibling);
btn.style.cssText = [
"display:" + (cs.display === "inline" ? "inline-flex" : cs.display),
"align-items:center",
"justify-content:center",
"width:" + cs.width,
"height:" + cs.height,
"padding:" + cs.padding,
"background:" + cs.backgroundColor,
"color:" + cs.color,
"border:" + cs.border,
"border-radius:" + cs.borderRadius,
"cursor:pointer",
"text-decoration:none",
].join(";");
} else {
btn.style.cssText = [
anchor ? "" : "position:fixed",
anchor ? "" : "bottom:8px",
anchor ? "" : "left:8px",
anchor ? "" : "z-index:2147483647",
"display:inline-flex",
"align-items:center",
"justify-content:center",
"width:32px",
"height:32px",
"padding:4px",
"background:rgba(127,127,127,0.18)",
"color:inherit",
"border:1px solid rgba(127,127,127,0.25)",
"border-radius:6px",
"cursor:pointer",
"text-decoration:none",
]
.filter(Boolean)
.join(";");
}
btn.innerHTML = LOGOUT_SVG;
btn.addEventListener(
"click",
(e) => {
e.preventDefault();
window.location.href = LOGOUT_URL;
},
{ capture: true },
);
(anchor || document.body).appendChild(btn);
}
function start() {
ensureLogoutIcon();
if (!("MutationObserver" in window)) return;
new MutationObserver(() => ensureLogoutIcon()).observe(document.body, {
childList: true,
subtree: true,
});
setTimeout(ensureLogoutIcon, 750);
setTimeout(ensureLogoutIcon, 2500);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", start, { once: true });
} else {
start();
}
})();