add better 403 screens and start with bun dev mode. (going to rewrite to fit the bun standard in a bit).

This commit is contained in:
KingKDot
2026-07-23 13:29:32 -05:00
parent 49bf1438a8
commit a68172538d
10 changed files with 487 additions and 22 deletions
+8
View File
@@ -278,6 +278,7 @@ function parseMaxHttpBodyBytes(): number {
}
const MAX_HTTP_BODY_BYTES = parseMaxHttpBodyBytes();
const IS_DEVELOPMENT = String(process.env.NODE_ENV || "").trim().toLowerCase() === "development";
const pluginLoadedByClient = new Map<string, Set<string>>();
const pendingPluginEvents = new Map<string, Array<{ event: string; payload: any }>>();
@@ -710,6 +711,13 @@ async function startServer() {
port: PORT,
hostname: HOST,
...(tls ? { tls: tls.tlsOptions } : {}),
development: IS_DEVELOPMENT
? {
hmr: true,
console: true,
chromeDevToolsAutomaticWorkspaceFolders: true,
}
: false,
idleTimeout: 255,
maxRequestBodySize: MAX_HTTP_BODY_BYTES,
fetch: createHttpFetchHandler({
@@ -1,8 +1,10 @@
import { authenticateRequest } from "../../auth";
import { getConfig } from "../../config";
import { consumeLoginPageRateLimit } from "../../rateLimit";
import { requirePermission, type Permission } from "../../rbac";
import { getPermissionDescription, requirePermission, type Permission } from "../../rbac";
import { canUserAccessClient, canUserAccessFeature, getUserById, type FeatureName, type UserRole } from "../../users";
import { renderAccessDeniedPage, type AccessDeniedKind } from "../ui/access-denied-view";
import { htmlResponse } from "../ui/html";
type PageRouteDeps = {
PUBLIC_ROOT: string;
@@ -120,23 +122,60 @@ const PATH_CLIENT_PAGES: ClientPageDef[] = [
{ pattern: /^\/(.+)\/keylogger$/, file: "keylogger.html", clientIdGroup: 1, feature: "keylogger" },
];
function checkAccess(role: UserRole, access: AccessLevel): Response | null {
function accessDeniedResponse(
deps: PageRouteDeps,
options: {
kind: AccessDeniedKind;
title?: string;
message: string;
detail?: string;
detailLabel?: string;
},
): Response {
return htmlResponse(renderAccessDeniedPage(options), {
status: 403,
headers: deps.secureHeaders("text/html; charset=utf-8"),
});
}
function permissionDeniedResponse(deps: PageRouteDeps, permission: Permission): Response {
return accessDeniedResponse(deps, {
kind: "permission",
message: `This page requires permission to ${getPermissionDescription(permission).toLowerCase()}.`,
detail: permission,
detailLabel: "Missing permission",
});
}
function checkAccess(deps: PageRouteDeps, role: UserRole, access: AccessLevel): Response | null {
switch (access) {
case "any":
return null;
case "no-viewer":
if (role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
return accessDeniedResponse(deps, {
kind: "role",
message: "Viewer accounts can inspect data, but cannot open interactive control features.",
detail: "Operator or administrator role",
});
}
return null;
case "admin":
if (role !== "admin") {
return new Response("Forbidden: Admin access required", { status: 403 });
return accessDeniedResponse(deps, {
kind: "role",
message: "This area is reserved for administrators.",
detail: "Administrator role",
});
}
return null;
case "admin-or-operator":
if (role !== "admin" && role !== "operator") {
return new Response("Forbidden: Admin or operator access required", { status: 403 });
return accessDeniedResponse(deps, {
kind: "role",
message: "This interactive area is available to operators and administrators.",
detail: "Operator or administrator role",
});
}
return null;
}
@@ -195,7 +234,12 @@ export async function handlePageRoutes(
if (!user) return serveLoginOrUnauthorized(req, deps);
if (page.path === "/screenshots" && !getConfig().thumbnails.wallEnabled) {
return new Response("Screenshot wall is disabled by the administrator", { status: 403 });
return accessDeniedResponse(deps, {
kind: "unavailable",
message: "The screenshot wall has been disabled in server settings.",
detail: "Screenshot wall disabled",
detailLabel: "Current status",
});
}
if (page.checkPasswordChange) {
@@ -203,14 +247,16 @@ export async function handlePageRoutes(
if (maybeChange) return maybeChange;
}
const accessDenied = checkAccess(user.role, page.access);
const accessDenied = checkAccess(deps, user.role, page.access);
if (accessDenied) return accessDenied;
if (page.permission) {
try {
requirePermission(user, page.permission);
} catch (error) {
if (error instanceof Response) return error;
if (error instanceof Response && error.status === 403) {
return permissionDeniedResponse(deps, page.permission);
}
return new Response("Forbidden", { status: 403 });
}
}
@@ -232,18 +278,34 @@ export async function handlePageRoutes(
try {
requirePermission(user, page.permission);
} catch (error) {
if (error instanceof Response) return error;
if (error instanceof Response && error.status === 403) {
return permissionDeniedResponse(deps, page.permission);
}
return new Response("Forbidden", { status: 403 });
}
}
if (page.feature && !canUserAccessFeature(user.userId, user.role, page.feature)) {
return new Response(`Forbidden: feature access denied for ${page.feature}`, { status: 403 });
return accessDeniedResponse(deps, {
kind: "feature",
title: "This feature isn't enabled for you",
message: "Your account's feature access does not include this client tool.",
detail: page.feature,
detailLabel: "Restricted feature",
});
}
const clientId = (url.searchParams.get("clientId") || "").trim();
if (!canAccessClientPage(user.userId, user.role, clientId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
return accessDeniedResponse(deps, {
kind: "client",
title: "Client access restricted",
message: clientId
? "This client is outside the scope assigned to your account."
: "Choose a client from the dashboard before opening this tool.",
detail: clientId || "No client selected",
detailLabel: "Client",
});
}
return serveFile(deps, page.file);
@@ -261,18 +323,32 @@ export async function handlePageRoutes(
try {
requirePermission(user, page.permission);
} catch (error) {
if (error instanceof Response) return error;
if (error instanceof Response && error.status === 403) {
return permissionDeniedResponse(deps, page.permission);
}
return new Response("Forbidden", { status: 403 });
}
}
if (page.feature && !canUserAccessFeature(user.userId, user.role, page.feature)) {
return new Response(`Forbidden: feature access denied for ${page.feature}`, { status: 403 });
return accessDeniedResponse(deps, {
kind: "feature",
title: "This feature isn't enabled for you",
message: "Your account's feature access does not include this client tool.",
detail: page.feature,
detailLabel: "Restricted feature",
});
}
const clientId = match[page.clientIdGroup];
if (!canAccessClientPage(user.userId, user.role, clientId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
return accessDeniedResponse(deps, {
kind: "client",
title: "Client access restricted",
message: "This client is outside the scope assigned to your account.",
detail: clientId,
detailLabel: "Client",
});
}
return serveFile(deps, page.file);
@@ -0,0 +1,33 @@
import { describe, expect, test } from "bun:test";
import { renderAccessDeniedPage } from "./access-denied-view";
describe("renderAccessDeniedPage", () => {
test("renders a complete permission screen with recovery actions", () => {
const html = renderAccessDeniedPage({
kind: "permission",
message: "This page requires additional access.",
detail: "users:manage",
detailLabel: "Missing permission",
});
expect(html).toContain("<!doctype html>");
expect(html).toContain("You don&#39;t have access");
expect(html).toContain("users:manage");
expect(html).toContain('href="/"');
expect(html).toContain('href="/settings"');
});
test("escapes dynamic denial details", () => {
const html = renderAccessDeniedPage({
kind: "client",
title: "<script>bad()</script>",
message: "Client <not-allowed>",
detail: '"><img src=x onerror=bad()>',
});
expect(html).not.toContain("<script>bad()</script>");
expect(html).not.toContain("<img src=x");
expect(html).toContain("&lt;script&gt;bad()&lt;/script&gt;");
expect(html).toContain("&lt;not-allowed&gt;");
});
});
@@ -0,0 +1,78 @@
import { escapeHtml } from "./html";
export type AccessDeniedKind =
| "permission"
| "role"
| "feature"
| "client"
| "unavailable";
type AccessDeniedViewOptions = {
kind: AccessDeniedKind;
title?: string;
message: string;
detail?: string;
detailLabel?: string;
};
const ICONS: Record<AccessDeniedKind, string> = {
permission: "fa-user-lock",
role: "fa-shield-halved",
feature: "fa-toggle-off",
client: "fa-desktop",
unavailable: "fa-circle-pause",
};
export function renderAccessDeniedPage(options: AccessDeniedViewOptions): string {
const title = options.title || (
options.kind === "unavailable" ? "This feature is unavailable" : "You don't have access"
);
const detail = options.detail
? `
<div class="access-detail">
<span>${escapeHtml(options.detailLabel || "Required access")}</span>
<code>${escapeHtml(options.detail)}</code>
</div>`
: "";
return `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="turbo-visit-control" content="reload" />
<title>${escapeHtml(title)} · Overlord</title>
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />
<link rel="stylesheet" href="/vendor/inter/400.css" />
<link rel="stylesheet" href="/vendor/inter/600.css" />
<link rel="stylesheet" href="/vendor/inter/700.css" />
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css" />
<link rel="stylesheet" href="/assets/access-denied.css" />
</head>
<body>
<main class="access-shell">
<section class="access-card" aria-labelledby="access-title">
<div class="access-status">403 · Access restricted</div>
<div class="access-icon" aria-hidden="true">
<i class="fa-solid ${ICONS[options.kind]}"></i>
</div>
<h1 id="access-title">${escapeHtml(title)}</h1>
<p class="access-message">${escapeHtml(options.message)}</p>
${detail}
<p class="access-help">If you believe you should have access, ask an administrator to update your account.</p>
<div class="access-actions">
<a class="access-primary" href="/">
<i class="fa-solid fa-border-all" aria-hidden="true"></i>
Return to dashboard
</a>
<a class="access-secondary" href="/settings">
<i class="fa-solid fa-gear" aria-hidden="true"></i>
Account settings
</a>
</div>
</section>
<p class="access-footer"><i class="fa-solid fa-crown" aria-hidden="true"></i> Overlord</p>
</main>
</body>
</html>`;
}