ZDDC/shared/elevation.js
ZDDC 143e26f337 feat(elevation): toggle admin mode via ?admin=true|false URL param
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) <noreply@anthropic.com>
2026-06-02 08:31:40 -05:00

184 lines
7.9 KiB
JavaScript

// shared/elevation.js — admin elevation via URL toggle.
//
// 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}.
//
// 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.
//
// 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';
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. Max-Age caps the elevation window so a forgotten
// tab doesn't leave admin powers active indefinitely (sudo's
// 5-minute precedent informs the number — 30 minutes is a
// reasonable trade between annoyance and exposure).
document.cookie = COOKIE_NAME + '=1; Path=/; SameSite=Lax; Max-Age=1800';
} else {
document.cookie = COOKIE_NAME + '=; Path=/; SameSite=Lax; Max-Age=0';
}
}
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. 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
// 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 =
'<span class="elevation-banner__dot" aria-hidden="true"></span>'
+ '<span class="elevation-banner__msg">'
+ 'Admin mode is on — write access bypasses WORM and ACL safeguards.'
+ '</span>'
+ '<button type="button" class="elevation-banner__off" id="elevation-banner-off">'
+ 'Drop admin'
+ '</button>';
document.body.insertBefore(banner, document.body.firstChild);
var off = banner.querySelector('#elevation-banner-off');
if (off) off.addEventListener('click', function () {
setElevated(false);
window.location.reload();
});
}
} else if (banner) {
banner.parentNode.removeChild(banner);
}
}
async function init() {
// 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());
// 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') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.zddc.elevation = { isElevated: isElevated, setElevated: setElevated };
})();