From 143e26f337718029a5aad675838e434db0582ca2 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Tue, 2 Jun 2026 08:31:40 -0500 Subject: [PATCH] feat(elevation): toggle admin mode via ?admin=true|false URL param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the header "Admin" checkbox with a URL toggle reachable from any zddc-server page: ?admin=true arms, ?admin=false (or the red banner's "Drop admin" button) drops. Reuses the existing zddc-elevate cookie as the sticky state — it persists across navigation for its Max-Age window and is what the server reads — so the param is stripped from the URL once applied (via location.replace, so Back can't re-trigger it and it isn't bookmarked). Arming is gated on /.profile/access can_elevate: a non-admin who types ?admin=true just gets the param stripped, never a misleading red border. The red viewport border + banner (applyArmedChrome) are unchanged and still reflect the cookie on every page load. The now-unused #elevation-toggle header span stays hidden in templates (inert); render() removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- shared/elevation.js | 132 ++++++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 48 deletions(-) diff --git a/shared/elevation.js b/shared/elevation.js index f53c1b9..326c3d1 100644 --- a/shared/elevation.js +++ b/shared/elevation.js @@ -1,20 +1,23 @@ -// shared/elevation.js — admin elevation toggle. +// shared/elevation.js — admin elevation via URL toggle. // -// Sudo-style model: admins behave as normal users by default; clicking -// the header toggle elevates the session so admin escape hatches (WORM -// bypass, .zddc edit authority, profile admin scaffolds) start firing. -// State is carried in a `zddc-elevate=1` cookie that the server reads -// via handler.ACLMiddleware → zddc.Principal{Elevated}. +// Sudo-style model: admins behave as normal users by default; elevating +// the session turns on admin escape hatches (WORM bypass, .zddc edit +// authority, profile admin scaffolds). State is carried in a +// `zddc-elevate=1` cookie that the server reads via handler.ACLMiddleware +// → zddc.Principal{Elevated}. // -// Only renders the toggle when /.profile/access reports the caller has -// some admin scope — a non-admin sees nothing, which keeps the chrome -// quiet for the common case. The toggle fades in once access loads so -// non-admins never even see the affordance flash. +// Toggle is by URL query param — `?admin=true` to arm, `?admin=false` +// (or the red banner's "Drop admin" button) to drop — so it's reachable +// from ANY zddc-server page, not just ones that render a header control. +// The cookie is the sticky state: it persists across navigation for its +// Max-Age window, so the param need not stay in the URL (we strip it). +// Arming is gated on /.profile/access `can_elevate`, so only real admins +// can set it; a non-admin's ?admin=true is a silent no-op. // -// Click flow: set/clear the cookie, then reload the page so the server -// sees the new state on the next render. The reload is intentional — -// admin scaffolds in tool HTML are server-rendered for some tools, so -// a soft state flip on the client alone wouldn't reach those. +// Applying the cookie reloads to the cleaned URL so the server re-renders +// under the new state (admin scaffolds in some tool HTML are server- +// rendered, so a client-only flip wouldn't reach them). The red viewport +// border + banner (applyArmedChrome) reflect the cookie on every load. (function () { 'use strict'; @@ -59,22 +62,65 @@ } } - function render(host, elevated) { - host.classList.remove('hidden'); - host.innerHTML = - '' - + ''; - var cb = host.querySelector('#elevation-checkbox'); - cb.addEventListener('change', function () { - setElevated(cb.checked); - // Hard reload so server-rendered admin surfaces (profile - // page scaffolds, hidden-entry listings) catch up. URL - // and scroll state are preserved by the browser's normal - // back-forward cache rules. - window.location.reload(); - }); + // ── 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. Returns true when a + // navigation (reload) is underway so the caller can stop. 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). + async function handleAdminParam() { + var want = adminParam(); + if (want === null) return false; + var clean = urlWithoutAdmin(); + if (want === isElevated()) { + // Already in the requested state — just clean the URL, no reload. + try { history.replaceState(history.state, '', clean); } catch (_e) {} + return false; + } + if (want === true) { + var access = await fetchAccess(); + if (!access || !access.can_elevate) { + try { history.replaceState(history.state, '', clean); } catch (_e) {} + return false; + } + setElevated(true); + } else { + setElevated(false); + } + // Navigate to the clean URL (a real load, so the server re-renders + // under the new cookie) and replace history so Back is safe. + window.location.replace(clean); + return true; } // Page-wide affordances when elevation is active. The toggle alone @@ -116,26 +162,16 @@ } async function init() { - // Body chrome applies on every page load whether or not the - // header has a toggle slot — the banner needs to surface in - // tools / pages that don't host the toggle (e.g. iframed - // classifier inside browse's grid mode), so the user can't - // accidentally write through an elevated context elsewhere. + // Apply (or tear down) the red border + banner from the cookie on + // every page load — admin mode is toggled by URL, but the armed + // chrome must surface everywhere so the user can't accidentally + // write through an elevated context on a page they didn't toggle. applyArmedChrome(isElevated()); - var host = document.getElementById('elevation-toggle'); - if (!host) return; // tool doesn't include the slot yet — no-op - var access = await fetchAccess(); - if (!access) return; // anonymous / endpoint missing — no-op - // Surface ONLY for users who have admin authority somewhere. - // /.profile/access ships `can_elevate` as an elevation- - // INDEPENDENT signal — true for any user named in any admin - // list, regardless of current cookie state. The other flags - // (is_super_admin, has_any_admin_scope) reflect EFFECTIVE - // authority and would be false for an un-elevated admin - // who hasn't toggled yet — so we can't gate on those. - if (!access.can_elevate) return; - render(host, isElevated()); + // Honour ?admin=true|false typed into any zddc-server URL. There's + // no on-screen toggle anymore — the URL is the enable path and the + // red banner's "Drop admin" button is the one-click disable. + await handleAdminParam(); } if (document.readyState === 'loading') {