mirror of
https://github.com/penpot/penpot.git
synced 2026-11-03 00:20:41 +00:00
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
139 lines
5.7 KiB
JavaScript
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);
|