Add an admin page for managing collector API keys
Admins (Microsoft 365 sign-in names in BACKUP_DASH_ADMINS) get an Admin link to /admin.html, where they can create, list and revoke the API keys collectors use, instead of running manage.py on the server. Backend - /api/v1/admin/keys (list, create, revoke), admins only (403 otherwise); the user comes from the headers nginx sets after sign-in - Changes need an X-Requested-By header, which another site's page can't add, so a signed-in admin's browser can't be used to forge them - A new key is returned once; only its hash is stored. Names are unique for good, so revoked keys' history stays unambiguous - Keys record who created/revoked them and the company they last reported for (schema v2; databases migrate automatically on start) - /api/v1/me reports is_admin; manage.py shares the key code and records itself as creator/revoker Frontend - admin.html / admin.js: name validation shown after interaction (:user-invalid, aria-invalid kept in sync), the one-time key with a copy button (falls back to selecting it if clipboard access is refused), a key table flagging keys quiet for over 24 hours, and revoking confirmed in a <dialog>; results announced in a status region - Shared el() and signed-in helpers moved to dom.js / signed-in.js - Mock mode (?mock=1) includes sample keys Tested: 15 new backend tests (access, forged requests, names, duplicates, revoking, last-used company, v1 -> v2 upgrade); in the browser, every admin flow in mock mode and at phone width; end to end through the nginx config with the real API (a key created on the API works for ingest; revoking it makes ingest 401; non-admins see an explanation and no Admin link). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+7
-28
@@ -1,5 +1,7 @@
|
||||
import { CONFIG } from './config.js';
|
||||
import { getApi } from './api.js';
|
||||
import { el } from './dom.js';
|
||||
import { showSignedInUser } from './signed-in.js';
|
||||
import {
|
||||
parseDate, formatDateTime, formatTime, formatRelative, formatDuration, formatBytes,
|
||||
} from './format.js';
|
||||
@@ -59,6 +61,7 @@ const dom = {
|
||||
signedIn: $('#signed-in'),
|
||||
signedInEmail: $('#signed-in-email'),
|
||||
signOut: $('#sign-out'),
|
||||
adminLink: $('#admin-link'),
|
||||
lastUpdated: $('#last-updated'),
|
||||
errorBanner: $('#error-banner'),
|
||||
dialog: $('#detail-dialog'),
|
||||
@@ -67,19 +70,6 @@ const dom = {
|
||||
detailBody: $('#detail-body'),
|
||||
};
|
||||
|
||||
// Tiny element builder. All text goes through append() → text nodes, so API data can never inject HTML.
|
||||
function el(tag, props = {}, ...children) {
|
||||
const node = document.createElement(tag);
|
||||
for (const [k, v] of Object.entries(props)) {
|
||||
if (v == null || v === false) continue;
|
||||
if (k === 'class') node.className = v;
|
||||
else if (k === 'dataset') Object.assign(node.dataset, v);
|
||||
else node.setAttribute(k, v === true ? '' : v);
|
||||
}
|
||||
node.append(...children.flat().filter((c) => c != null && c !== false));
|
||||
return node;
|
||||
}
|
||||
|
||||
// ---------- Rendering ----------
|
||||
function statusBadge(status, { asButton = false, opensDialog = false, dataset = null, title = null } = {}) {
|
||||
const { label, icon } = STATUS[status];
|
||||
@@ -527,20 +517,9 @@ dom.search.addEventListener('input', () => {
|
||||
|
||||
dom.refreshBtn.addEventListener('click', () => refresh().then(schedulePoll));
|
||||
|
||||
// ---------- Signed-in user ----------
|
||||
async function showSignedInUser() {
|
||||
try {
|
||||
const me = await api.getMe();
|
||||
const who = me?.email || me?.name;
|
||||
if (!who) return; // no Microsoft 365 sign-in in front of the dashboard (dev, mock)
|
||||
dom.signedInEmail.textContent = who;
|
||||
dom.signOut.href = CONFIG.signOutUrl;
|
||||
dom.signedIn.hidden = false;
|
||||
} catch (err) {
|
||||
console.error(err); // not worth a banner: the jobs request will surface real problems
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Boot ----------
|
||||
showSignedInUser();
|
||||
api.getMe().then(
|
||||
(me) => showSignedInUser(me, dom),
|
||||
(err) => console.error(err), // not worth a banner: the jobs request will surface real problems
|
||||
);
|
||||
refresh().then(schedulePoll);
|
||||
Reference in New Issue
Block a user