mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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>
120 lines
4.0 KiB
Django/Jinja
120 lines
4.0 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();
|
|
if (!anchor) return;
|
|
|
|
// Clone the styling of an existing sibling icon button so the
|
|
// injected logout matches the role's theme (box, padding, hover
|
|
// state, border-radius) without us guessing per-app design tokens.
|
|
const sibling = anchor.querySelector(
|
|
":scope > button, :scope > a, :scope > li > button, :scope > li > a",
|
|
);
|
|
|
|
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 = [
|
|
"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",
|
|
].join(";");
|
|
}
|
|
btn.innerHTML = LOGOUT_SVG;
|
|
btn.addEventListener(
|
|
"click",
|
|
(e) => {
|
|
e.preventDefault();
|
|
window.location.href = LOGOUT_URL;
|
|
},
|
|
{ capture: true },
|
|
);
|
|
anchor.appendChild(btn);
|
|
}
|
|
|
|
function start() {
|
|
ensureLogoutIcon();
|
|
if (!("MutationObserver" in window)) return;
|
|
new MutationObserver(() => ensureLogoutIcon()).observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
// React-mounted navbars need a retry — single synchronous pass misses them.
|
|
setTimeout(ensureLogoutIcon, 750);
|
|
setTimeout(ensureLogoutIcon, 2500);
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", start, { once: true });
|
|
} else {
|
|
start();
|
|
}
|
|
})();
|