fix: update the permission for token saver page

This commit is contained in:
2026-07-12 17:24:33 +07:00
parent 2427852593
commit be7c210161
5 changed files with 65 additions and 5 deletions
@@ -1,5 +1,10 @@
import TokenSaverClient from "./TokenSaverClient";
import { redirect } from "next/navigation";
import { getCurrentDashboardUser } from "@/lib/auth/currentUser";
export default async function TokenSaverPage() {
const user = await getCurrentDashboardUser();
if (user?.role !== "admin") redirect("/dashboard");
export default function TokenSaverPage() {
return <TokenSaverClient />;
}
+21 -1
View File
@@ -16,6 +16,24 @@ const SETTINGS_RESPONSE_HEADERS = {
// Secrets must never be mass-assigned from request body (CWE-915)
const PROTECTED_SETTING_KEYS = ["password", "mitmSudoEncrypted"];
// Token savers change gateway-wide request processing and can start or manage
// local helper processes. They are therefore administrator-only settings.
const TOKEN_SAVER_SETTING_KEYS = [
"rtkEnabled",
"headroomEnabled",
"headroomUrl",
"headroomCodeAware",
"headroomKompress",
"cavemanEnabled",
"cavemanLevel",
"ponytailEnabled",
"ponytailLevel",
"pxpipeEnabled",
"pxpipeAutoInstall",
"pxpipeMinChars",
"pxpipeTimeoutMs",
];
export async function GET() {
try {
const settings = await getSettings();
@@ -26,6 +44,7 @@ export async function GET() {
safeSettings.comboStrategies = Object.fromEntries(
Object.entries(safeSettings.comboStrategies || {}).filter(([comboId]) => ownedComboIds.has(comboId))
);
for (const key of TOKEN_SAVER_SETTING_KEYS) delete safeSettings[key];
}
safeSettings.oidcConfigured = !!(safeSettings.oidcIssuerUrl && safeSettings.oidcClientId && oidcClientSecret);
@@ -51,7 +70,8 @@ export async function PATCH(request) {
if (
Object.prototype.hasOwnProperty.call(body, "requireApiKey") ||
Object.prototype.hasOwnProperty.call(body, "tunnelDashboardAccess") ||
Object.prototype.hasOwnProperty.call(body, "comboStrategies")
Object.prototype.hasOwnProperty.call(body, "comboStrategies") ||
TOKEN_SAVER_SETTING_KEYS.some((key) => Object.prototype.hasOwnProperty.call(body, key))
) {
let user;
try {
+2 -2
View File
@@ -46,11 +46,11 @@ const ALWAYS_PROTECTED = [
// User administration is never exposed to normal users, even if dashboard login
// is disabled for local single-user deployments.
const ADMIN_ONLY_PATHS = ["/api/users", "/api/tunnel"];
const ADMIN_ONLY_PATHS = ["/api/users", "/api/tunnel", "/api/headroom", "/api/pxpipe"];
// Dashboard paths requiring an administrator. Combo access is handled by its
// owner-scoped API routes and is available to authenticated users.
const ADMIN_ONLY_DASHBOARD_PATHS = [];
const ADMIN_ONLY_DASHBOARD_PATHS = ["/dashboard/token-saver", "/dashboard/pxpipe"];
// Require auth, but allow through if requireLogin is disabled
const PROTECTED_API_PATHS = [
+1 -1
View File
@@ -26,7 +26,7 @@ const navItems = [
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
{ href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" },
{ href: "/dashboard/token-saver", label: "Token Saver", icon: "savings" },
{ href: "/dashboard/token-saver", label: "Token Saver", icon: "savings", adminOnly: true },
// { href: "/dashboard/pxpipe", label: "PXPIPE", icon: "image" },
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
];
+35
View File
@@ -301,6 +301,41 @@ describe("dashboard guard combo administration access", () => {
});
});
describe("dashboard guard token saver administration access", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.getSettings.mockResolvedValue({ requireLogin: true });
mocks.getUserById.mockResolvedValue({ id: "user-1", isActive: true, role: "user" });
mocks.getConsistentMachineId.mockResolvedValue("cli-token");
mocks.getDashboardAuthSession.mockResolvedValue({ userId: "user-1" });
mocks.verifyDashboardAuthToken.mockResolvedValue(true);
});
it("rejects normal users from Token Saver pages and APIs", async () => {
for (const pathname of [
"/api/headroom/status",
"/api/pxpipe/status",
]) {
const response = await proxy(request(pathname, { host: "localhost:20128" }, "user-token"));
expect(response.status).toBe(403);
expect(response.body.error).toBe("Administrator access required");
}
const response = await proxy(request("/dashboard/token-saver", { host: "localhost:20128" }, "user-token"));
expect(response.status).toBe(307);
expect(response.url.href).toBe("http://localhost/dashboard");
});
it("allows administrators to access Token Saver pages and APIs", async () => {
mocks.getUserById.mockResolvedValue({ id: "user-1", isActive: true, role: "admin" });
expect(await proxy(request("/dashboard/token-saver", { host: "localhost:20128" }, "admin-token"))).toBe(mocks.nextResponse);
expect(await proxy(request("/api/headroom/status", { host: "localhost:20128" }, "admin-token"))).toBe(mocks.nextResponse);
expect(await proxy(request("/api/pxpipe/status", { host: "localhost:20128" }, "admin-token"))).toBe(mocks.nextResponse);
});
});
describe("dashboard guard helpers", () => {
it("extracts bearer API keys before x-api-key", () => {
const apiRequest = request("/v1/chat/completions", {