// shared/profile-menu.js — account menu in the header's upper-right. // // Replaces the old floating elevation toggle. Admin mode is now one item in // this dropdown, alongside the signed-in email, Profile, and Access tokens. // Mounts into the tool header's `.header-right` cluster (every tool ships one) // and drives elevation via window.zddc.elevation, so the cookie / armed-chrome // / ephemeral state machine stays in shared/elevation.js. // // No logout: authentication is the upstream proxy's concern (oauth2-proxy / // Authelia) — ZDDC owns no session, so it doesn't touch sign-out. // // Server mode only: it reads /.profile/access for the email + can_elevate. // On file:// (offline FS-Access mode) there's no server account, so nothing // renders. (function () { 'use strict'; if (!window.zddc) window.zddc = {}; if (window.zddc.profileMenu) return; function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; } async function fetchAccess() { try { var r = await fetch('/.profile/access', { headers: { Accept: 'application/json' }, credentials: 'same-origin', cache: 'no-cache' }); if (!r.ok) return null; return await r.json(); } catch (_e) { return null; } } var elevation = null; var panelEl = null, btnEl = null, adminInput = null; function isElevated() { return !!(elevation && elevation.isElevated && elevation.isElevated()); } // Keep the button's armed ring + the menu checkbox in lockstep with the // elevation cookie (flipped here, by ?admin=, or by the banner's Drop). function syncArmed() { if (btnEl) btnEl.classList.toggle('profile-btn--armed', isElevated()); if (adminInput) adminInput.checked = isElevated(); } function closeMenu() { if (panelEl) panelEl.classList.remove('open'); if (btnEl) btnEl.setAttribute('aria-expanded', 'false'); } // The panel is position:fixed (to escape the app's stacking contexts), so // anchor it to the button rect — top just below it, right-aligned. function positionPanel() { var r = btnEl.getBoundingClientRect(); panelEl.style.top = (r.bottom + 4) + 'px'; panelEl.style.right = Math.max(4, window.innerWidth - r.right) + 'px'; panelEl.style.left = 'auto'; } function toggleMenu() { if (!panelEl) return; var open = panelEl.classList.toggle('open'); if (open) positionPanel(); btnEl.setAttribute('aria-expanded', open ? 'true' : 'false'); } function linkItem(text, href) { var a = el('a', 'profile-menu__item', text); a.href = href; a.setAttribute('role', 'menuitem'); return a; } function build(access) { var wrap = el('div', 'profile-menu'); btnEl = el('button', 'btn btn-secondary profile-btn'); btnEl.type = 'button'; btnEl.id = 'profile-btn'; btnEl.title = 'Account: ' + (access.email || 'signed in'); btnEl.setAttribute('aria-haspopup', 'menu'); btnEl.setAttribute('aria-expanded', 'false'); var initial = ((access.email || '?').trim().charAt(0) || '?').toUpperCase(); btnEl.appendChild(el('span', 'profile-btn__avatar', initial)); btnEl.addEventListener('click', function (e) { e.stopPropagation(); toggleMenu(); }); wrap.appendChild(btnEl); panelEl = el('div', 'profile-menu__panel'); panelEl.setAttribute('role', 'menu'); var id = el('div', 'profile-menu__id'); id.appendChild(el('div', 'profile-menu__email', access.email || '(anonymous)')); if (access.is_super_admin) id.appendChild(el('div', 'profile-menu__role', 'super admin')); else if (access.has_any_admin_scope) id.appendChild(el('div', 'profile-menu__role', 'admin')); panelEl.appendChild(id); panelEl.appendChild(el('div', 'profile-menu__sep')); // Admin mode — only offered to principals who actually have admin // authority somewhere (can_elevate). Drops automatically on leave. if (access.can_elevate && elevation) { var row = el('label', 'profile-menu__item profile-menu__toggle'); adminInput = document.createElement('input'); adminInput.type = 'checkbox'; adminInput.className = 'profile-menu__check'; adminInput.checked = isElevated(); adminInput.addEventListener('change', function () { if (adminInput.checked) elevation.setOn(); else elevation.setOff(); }); row.appendChild(adminInput); var txt = el('span', 'profile-menu__toggle-label'); txt.appendChild(el('span', null, 'Admin mode')); txt.appendChild(el('span', 'profile-menu__hint', 'bypass WORM/ACL — drops when you leave the page')); row.appendChild(txt); panelEl.appendChild(row); panelEl.appendChild(el('div', 'profile-menu__sep')); } panelEl.appendChild(linkItem('Profile', '/.profile')); panelEl.appendChild(linkItem('Access tokens', '/.tokens')); // No "Sign out": authentication is the upstream proxy's concern // (oauth2-proxy / Authelia). ZDDC doesn't own a session, so it // doesn't render a logout affordance. // Portal the panel to , not inside the header: the app's // layout creates stacking contexts that trap even a fixed+high // z-index panel below the content. As a direct body child it sits in // the root stacking context and reliably overlays everything. // position:fixed + positionPanel() keep it anchored to the button. document.body.appendChild(panelEl); return wrap; } async function init() { if (window.location.protocol === 'file:') return; elevation = window.zddc.elevation || null; var access = await fetchAccess(); if (!access || !access.email) return; // unauthenticated / non-zddc backend var host = document.querySelector('.header-right'); if (!host) return; host.appendChild(build(access)); syncArmed(); document.addEventListener('click', function (e) { if (panelEl && panelEl.classList.contains('open') && !panelEl.contains(e.target) && !btnEl.contains(e.target)) closeMenu(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenu(); }); window.addEventListener('zddc:elevationchange', syncArmed); window.zddc.profileMenu = { close: closeMenu }; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();