mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix: update the permission for token saver page
This commit is contained in:
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
|
||||
@@ -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", {
|
||||
|
||||
Reference in New Issue
Block a user