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>
This commit is contained in:
parent
28ebaa19cd
commit
143e26f337
1 changed files with 84 additions and 48 deletions
|
|
@ -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
|
// Sudo-style model: admins behave as normal users by default; elevating
|
||||||
// the header toggle elevates the session so admin escape hatches (WORM
|
// the session turns on admin escape hatches (WORM bypass, .zddc edit
|
||||||
// bypass, .zddc edit authority, profile admin scaffolds) start firing.
|
// authority, profile admin scaffolds). State is carried in a
|
||||||
// State is carried in a `zddc-elevate=1` cookie that the server reads
|
// `zddc-elevate=1` cookie that the server reads via handler.ACLMiddleware
|
||||||
// via handler.ACLMiddleware → zddc.Principal{Elevated}.
|
// → zddc.Principal{Elevated}.
|
||||||
//
|
//
|
||||||
// Only renders the toggle when /.profile/access reports the caller has
|
// Toggle is by URL query param — `?admin=true` to arm, `?admin=false`
|
||||||
// some admin scope — a non-admin sees nothing, which keeps the chrome
|
// (or the red banner's "Drop admin" button) to drop — so it's reachable
|
||||||
// quiet for the common case. The toggle fades in once access loads so
|
// from ANY zddc-server page, not just ones that render a header control.
|
||||||
// non-admins never even see the affordance flash.
|
// 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
|
// Applying the cookie reloads to the cleaned URL so the server re-renders
|
||||||
// sees the new state on the next render. The reload is intentional —
|
// under the new state (admin scaffolds in some tool HTML are server-
|
||||||
// admin scaffolds in tool HTML are server-rendered for some tools, so
|
// rendered, so a client-only flip wouldn't reach them). The red viewport
|
||||||
// a soft state flip on the client alone wouldn't reach those.
|
// border + banner (applyArmedChrome) reflect the cookie on every load.
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
@ -59,22 +62,65 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(host, elevated) {
|
// ── URL toggle: ?admin=true | ?admin=false (typeable anywhere) ──────
|
||||||
host.classList.remove('hidden');
|
//
|
||||||
host.innerHTML =
|
// Admin mode is toggled via a URL query param rather than an on-screen
|
||||||
'<input type="checkbox" id="elevation-checkbox"'
|
// checkbox, so it's reachable from any zddc-server page. The param only
|
||||||
+ (elevated ? ' checked' : '') + '>'
|
// SETS the cookie; the cookie is the sticky state (it persists across
|
||||||
+ '<label for="elevation-checkbox" class="elevation-toggle__label">'
|
// navigation for its Max-Age window and is what the server reads), so
|
||||||
+ 'Admin</label>';
|
// there's no need to keep ?admin= in the URL once applied.
|
||||||
var cb = host.querySelector('#elevation-checkbox');
|
|
||||||
cb.addEventListener('change', function () {
|
// adminParam returns true/false for a recognised ?admin= value, or null
|
||||||
setElevated(cb.checked);
|
// when absent / unrecognised (ignored).
|
||||||
// Hard reload so server-rendered admin surfaces (profile
|
function adminParam() {
|
||||||
// page scaffolds, hidden-entry listings) catch up. URL
|
try {
|
||||||
// and scroll state are preserved by the browser's normal
|
var v = new URLSearchParams(window.location.search).get('admin');
|
||||||
// back-forward cache rules.
|
if (v === null) return null;
|
||||||
window.location.reload();
|
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
|
// Page-wide affordances when elevation is active. The toggle alone
|
||||||
|
|
@ -116,26 +162,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
// Body chrome applies on every page load whether or not the
|
// Apply (or tear down) the red border + banner from the cookie on
|
||||||
// header has a toggle slot — the banner needs to surface in
|
// every page load — admin mode is toggled by URL, but the armed
|
||||||
// tools / pages that don't host the toggle (e.g. iframed
|
// chrome must surface everywhere so the user can't accidentally
|
||||||
// classifier inside browse's grid mode), so the user can't
|
// write through an elevated context on a page they didn't toggle.
|
||||||
// accidentally write through an elevated context elsewhere.
|
|
||||||
applyArmedChrome(isElevated());
|
applyArmedChrome(isElevated());
|
||||||
|
|
||||||
var host = document.getElementById('elevation-toggle');
|
// Honour ?admin=true|false typed into any zddc-server URL. There's
|
||||||
if (!host) return; // tool doesn't include the slot yet — no-op
|
// no on-screen toggle anymore — the URL is the enable path and the
|
||||||
var access = await fetchAccess();
|
// red banner's "Drop admin" button is the one-click disable.
|
||||||
if (!access) return; // anonymous / endpoint missing — no-op
|
await handleAdminParam();
|
||||||
// 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue