Andrey Antukh 7704eef169 ✨ Add granular access tokens for the admin API
Admin methods now require an implicit admin:<method> permission
carried by an access token, with superuser bypassing every check.
Superusers can issue, list and revoke scoped tokens (including for
other profiles) from new admin commands and a panel screen; the
vocabulary is derived from the registered methods and validated on
issue. Token calls additionally require a live owner, and the share
link stays anonymous.

AI-assisted-by: muse-spark-1.3-contributor
2026-11-02 15:37:35 +01:00

139 lines
5.7 KiB
JavaScript

// Bootstrap: paint a loading state, resolve the session, and show
// the matching view. Navigation is a bare query string
// (`?screen=error-reports`, `?screen=error-report&id=…`): it resolves
// against the current page, so subpath deployments keep working and
// there is no router. A dead session (deleted profile, expired
// cookie) bounces to the product login at the deployment root
// (`<root>/?screen=auth-login`, resolved from the script url in
// `auth.js` so subpath deployments keep working).
import { checkSuperuserSession, loginUrl } from "./auth.js";
import { renderHeader } from "./components/header.js";
import { renderSidebar } from "./components/sidebar.js";
import { dashboardPage } from "./pages/dashboard.js";
import { errorDetailPage } from "./pages/error-detail.js";
import { errorReportsPage } from "./pages/error-reports.js";
import { userDetailPage } from "./pages/user-detail.js";
import { usersDeletePage } from "./pages/users-delete.js";
import { teamsPage } from "./pages/teams.js";
import { teamDetailPage } from "./pages/team-detail.js";
import { filesPage } from "./pages/files.js";
import { fileDetailPage } from "./pages/file-detail.js";
import { projectsPage } from "./pages/projects.js";
import { projectDetailPage } from "./pages/project-detail.js";
import { usersPage } from "./pages/users.js";
import { jobsPage } from "./pages/jobs.js";
import { jobDetailPage } from "./pages/job-detail.js";
import { tokensPage } from "./pages/tokens.js";
import { virtualClockPage } from "./pages/virtual-clock.js";
const NAV_ITEMS = [
// Home targets the path itself, not an empty query: pushing ""
// would inherit the current query string and go nowhere.
{ query: location.pathname, label: "Dashboard", screens: [""] },
{ query: "?screen=error-reports", label: "Error reports", screens: ["error-reports", "error-report"] },
{ query: "?screen=users", label: "Users", screens: ["users", "user", "users-delete"] },
{ query: "?screen=teams", label: "Teams", screens: ["teams", "team"] },
{ query: "?screen=projects", label: "Projects", screens: ["projects", "project"] },
{ query: "?screen=files", label: "Files", screens: ["files", "file"] },
{ query: "?screen=jobs", label: "Jobs", screens: ["jobs", "job"] },
{ query: "?screen=tokens", label: "Tokens", screens: ["tokens"] },
{ query: "?screen=virtual-clock", label: "Virtual clock", screens: ["virtual-clock"] },
];
let sessionStatus = "anonymous";
function screenParams() {
return new URLSearchParams(location.search);
}
function deniedView(root) {
root.appendChild(renderHeader("Access denied"));
const message = document.createElement("p");
message.textContent = "This panel is for instance superusers.";
root.appendChild(message);
const login = document.createElement("a");
login.textContent = "Go to login";
login.href = loginUrl();
root.appendChild(login);
}
function renderAuthed(root) {
root.replaceChildren();
if (sessionStatus !== "superuser") {
deniedView(root);
return;
}
const layout = document.createElement("div");
layout.className = "admin-layout";
const navigate = (query) => {
history.pushState({}, "", query);
renderAuthed(root);
};
layout.appendChild(renderSidebar(NAV_ITEMS, navigate, screenParams().get("screen") ?? ""));
const content = document.createElement("main");
content.className = "admin-content";
layout.appendChild(content);
root.appendChild(layout);
const params = screenParams();
if (params.get("screen") === "error-reports") {
errorReportsPage(content, { onNavigate: navigate });
} else if (params.get("screen") === "error-report" && params.get("id")) {
errorDetailPage(content, { id: params.get("id"), onNavigate: navigate });
} else if (params.get("screen") === "users") {
usersPage(content, { onNavigate: navigate });
} else if (params.get("screen") === "user" && params.get("id")) {
userDetailPage(content, { id: params.get("id"), onNavigate: navigate });
} else if (params.get("screen") === "users-delete") {
usersDeletePage(content, { onNavigate: navigate });
} else if (params.get("screen") === "teams") {
teamsPage(content, { onNavigate: navigate });
} else if (params.get("screen") === "team" && params.get("id")) {
teamDetailPage(content, { id: params.get("id"), onNavigate: navigate });
} else if (params.get("screen") === "files") {
filesPage(content, { onNavigate: navigate });
} else if (params.get("screen") === "projects") {
projectsPage(content, { onNavigate: navigate });
} else if (params.get("screen") === "project" && params.get("id")) {
projectDetailPage(content, { id: params.get("id"), onNavigate: navigate });
} else if (params.get("screen") === "file" && params.get("id")) {
fileDetailPage(content, { id: params.get("id"), onNavigate: navigate });
} else if (params.get("screen") === "jobs") {
jobsPage(content, { onNavigate: navigate });
} else if (params.get("screen") === "job" && params.get("id")) {
jobDetailPage(content, { id: params.get("id"), onNavigate: navigate });
} else if (params.get("screen") === "tokens") {
tokensPage(content);
} else if (params.get("screen") === "virtual-clock") {
virtualClockPage(content, { onNavigate: navigate });
} else {
dashboardPage(content, { onNavigate: navigate });
}
}
async function boot() {
const root = document.getElementById("app");
root.textContent = "Loading admin…";
try {
sessionStatus = await checkSuperuserSession();
} catch {
root.replaceChildren();
root.textContent = "Admin — cannot reach the server.";
return;
}
if (sessionStatus === "anonymous") {
location.assign(loginUrl());
return;
}
renderAuthed(root);
window.addEventListener("popstate", () => renderAuthed(root));
}
document.addEventListener("DOMContentLoaded", boot);