// shared/elevation.js — admin elevation state machine. // // Sudo-style model: admins behave as normal users by default; elevating // the session turns on admin escape hatches (WORM bypass, recursive // delete, rearranging records, profile admin scaffolds — NOT config-edit, // which is standing). State is carried in a `zddc-elevate=1` cookie that // the server reads via handler.ACLMiddleware → zddc.Principal{Elevated}. // // This module owns the STATE (cookie, armed chrome/banner, ephemeral // lifecycle, the change event) + exposes setOn/setOff/isElevated. The // on-page elevate CONTROL lives in the shared profile menu // (shared/profile-menu.js) — an "Admin mode" item shown only to // can_elevate users — which calls setOn/setOff. `?admin=true|false` typed // into any URL is also honoured (gated on can_elevate), for deep links / // scripting. // // Admin mode is EPHEMERAL — scoped to the page you turned it on: // * the cookie is a SESSION cookie (no Max-Age), and // * we clear it on `pagehide`, so navigating away / closing the tab // drops admin (you re-arm deliberately on the next page). // Because of that we apply state IN PLACE (no reload — a reload's pagehide // would race the clear). SPAs that server-render elevation-dependent data // (e.g. browse's listing verbs) listen for the `zddc:elevationchange` // event we emit and re-fetch. The red viewport border + banner // (applyArmedChrome) reflect the cookie, kept in sync on every change. (function () { 'use strict'; if (!window.zddc) window.zddc = {}; if (window.zddc.elevation) return; var COOKIE_NAME = 'zddc-elevate'; function isElevated() { var parts = document.cookie.split(';'); for (var i = 0; i < parts.length; i++) { var kv = parts[i].trim().split('='); if (kv[0] === COOKIE_NAME && kv[1] === '1') return true; } return false; } function setElevated(on) { if (on) { // SameSite=Lax blocks cross-site form-post / image-tag CSRF // shapes. No Max-Age → a SESSION cookie: it dies with the tab // and, combined with the pagehide handler below, is cleared the // moment you leave the page. Admin powers never silently // outlive the page you armed them on. document.cookie = COOKIE_NAME + '=1; Path=/; SameSite=Lax'; } else { document.cookie = COOKIE_NAME + '=; Path=/; SameSite=Lax; Max-Age=0'; } } // emitChange notifies same-page listeners (SPAs that server-render // elevation-dependent data, e.g. browse's listing verbs / editor // affordances) so they can re-fetch without a full reload. function emitChange() { try { window.dispatchEvent(new CustomEvent('zddc:elevationchange', { detail: { elevated: isElevated() } })); } catch (_e) { /* CustomEvent unsupported — non-fatal */ } } // setOn / setOff are the single funnel for every arm/drop path (the // profile menu's Admin mode item, the ?admin= URL param, the banner's // Drop button). Each flips the cookie, re-paints the armed chrome, and // emits the change — no reload. The profile menu listens for the change // event to keep its checkbox + armed indicator in sync. function setOn() { setElevated(true); applyArmedChrome(true); emitChange(); } function setOff() { setElevated(false); applyArmedChrome(false); emitChange(); } async function fetchAccess() { try { var resp = await fetch('/.profile/access', { headers: { 'Accept': 'application/json' }, credentials: 'same-origin', cache: 'no-cache' }); if (!resp.ok) return null; return await resp.json(); } catch (_e) { return null; } } // ── URL toggle: ?admin=true | ?admin=false (typeable anywhere) ────── // // Admin mode is toggled via a URL query param rather than an on-screen // checkbox, so it's reachable from any zddc-server page. The param only // SETS the cookie; the cookie is the sticky state (it persists across // navigation for its Max-Age window and is what the server reads), so // there's no need to keep ?admin= in the URL once applied. // adminParam returns true/false for a recognised ?admin= value, or null // when absent / unrecognised (ignored). function adminParam() { try { var v = new URLSearchParams(window.location.search).get('admin'); if (v === null) return null; v = v.toLowerCase(); if (v === 'true' || v === '1' || v === 'on' || v === 'yes') return true; if (v === 'false' || v === '0' || v === 'off' || v === 'no') return false; return null; } catch (_e) { return null; } } // urlWithoutAdmin returns the current URL with the admin param stripped // (other params + hash preserved) — what we navigate/replace to so the // dirty param isn't bookmarked and Back doesn't re-trigger it. function urlWithoutAdmin() { var u = new URL(window.location.href); u.searchParams.delete('admin'); var qs = u.searchParams.toString(); return u.pathname + (qs ? '?' + qs : '') + u.hash; } // handleAdminParam applies a ?admin= request IN PLACE (no reload — see // the module header on why reloads would race the pagehide-clear). // Enabling is gated on can_elevate — a non-admin who types ?admin=true // just gets the param stripped, never a misleading red border. // Disabling is open (anyone may drop a cookie they somehow hold). // `access` (a prefetched /.profile/access, may be null) lets init reuse // its single fetch instead of issuing a second one. async function handleAdminParam(access) { var want = adminParam(); if (want === null) return; var clean = urlWithoutAdmin(); try { history.replaceState(history.state, '', clean); } catch (_e) {} if (want === isElevated()) return; // already in the requested state if (want === true) { if (access === undefined) access = await fetchAccess(); if (!access || !access.can_elevate) return; // silent no-op setOn(); } else { setOff(); } } // Page-wide affordances when elevation is active. The toggle alone // is easy to miss — admin mode silently bypasses WORM and ACL // restrictions, which produces surprising "I shouldn't have been // able to do that" moments. A body class + a sticky banner with a // one-click disable make the armed state unmistakable. function applyArmedChrome(elevated) { var b = document.body; if (!b) return; if (elevated) b.classList.add('is-elevated'); else b.classList.remove('is-elevated'); var banner = document.getElementById('elevation-banner'); if (elevated) { if (!banner) { banner = document.createElement('div'); banner.id = 'elevation-banner'; banner.className = 'elevation-banner'; banner.setAttribute('role', 'alert'); banner.innerHTML = '' + '' + 'Admin mode is on — write access bypasses WORM and ACL safeguards.' + '' + ''; document.body.insertBefore(banner, document.body.firstChild); var off = banner.querySelector('#elevation-banner-off'); if (off) off.addEventListener('click', function () { setOff(); }); } } else if (banner) { banner.parentNode.removeChild(banner); } } async function init() { // file:// (offline FS-Access mode) has no server to elevate against. if (window.location.protocol === 'file:') return; // Reflect the cookie's armed chrome on every load (a leftover from a // not-yet-fired pagehide, or an arrived-with ?admin link). applyArmedChrome(isElevated()); // Honour ?admin=true|false typed into any URL — handleAdminParam // fetches /.profile/access itself to gate arming on can_elevate. The // on-page elevate control lives in the shared profile menu // (shared/profile-menu.js), which calls setOn/setOff and listens for // zddc:elevationchange to keep its checkbox + armed ring in sync. await handleAdminParam(); // Admin mode is per-page: clear the cookie when the page goes away so // it never persists past a navigation. window.addEventListener('pagehide', function () { if (isElevated()) setElevated(false); }); // bfcache can restore a page whose pagehide already cleared the // cookie — re-sync the armed chrome so chrome ≠ cookie can't happen. window.addEventListener('pageshow', function (e) { if (e.persisted) applyArmedChrome(isElevated()); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.zddc.elevation = { isElevated: isElevated, setElevated: setElevated, setOn: setOn, setOff: setOff }; })();