mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
feat: update the permission for view combos page
This commit is contained in:
+10
-1
@@ -46,7 +46,11 @@ const ALWAYS_PROTECTED = [
|
|||||||
|
|
||||||
// User administration is never exposed to normal users, even if dashboard login
|
// User administration is never exposed to normal users, even if dashboard login
|
||||||
// is disabled for local single-user deployments.
|
// is disabled for local single-user deployments.
|
||||||
const ADMIN_ONLY_PATHS = ["/api/users", "/api/tunnel"];
|
const ADMIN_ONLY_PATHS = ["/api/users", "/api/tunnel", "/api/combos"];
|
||||||
|
|
||||||
|
// Combo definitions affect routing and fallback behavior, so only administrators
|
||||||
|
// may view or change them.
|
||||||
|
const ADMIN_ONLY_DASHBOARD_PATHS = ["/dashboard/combos"];
|
||||||
|
|
||||||
// Require auth, but allow through if requireLogin is disabled
|
// Require auth, but allow through if requireLogin is disabled
|
||||||
const PROTECTED_API_PATHS = [
|
const PROTECTED_API_PATHS = [
|
||||||
@@ -240,6 +244,11 @@ export async function proxy(request) {
|
|||||||
|
|
||||||
// Protect all dashboard routes
|
// Protect all dashboard routes
|
||||||
if (pathname.startsWith("/dashboard")) {
|
if (pathname.startsWith("/dashboard")) {
|
||||||
|
if (ADMIN_ONLY_DASHBOARD_PATHS.some((p) => pathname === p || pathname.startsWith(`${p}/`))) {
|
||||||
|
if (await isAdmin(request)) return NextResponse.next();
|
||||||
|
return NextResponse.redirect(new URL("/dashboard", request.url));
|
||||||
|
}
|
||||||
|
|
||||||
let requireLogin = true;
|
let requireLogin = true;
|
||||||
let tunnelDashboardAccess = true;
|
let tunnelDashboardAccess = true;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const navItems = [
|
|||||||
{ href: "/dashboard/endpoint", label: "Endpoint & Key", icon: "api" },
|
{ href: "/dashboard/endpoint", label: "Endpoint & Key", icon: "api" },
|
||||||
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
|
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
|
||||||
// { href: "/dashboard/basic-chat", label: "Basic Chat", icon: "chat" }, // Hidden
|
// { href: "/dashboard/basic-chat", label: "Basic Chat", icon: "chat" }, // Hidden
|
||||||
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
|
{ href: "/dashboard/combos", label: "Combos", icon: "layers", adminOnly: true },
|
||||||
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
|
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
|
||||||
{ href: "/dashboard/quota", label: "Quota Tracker", icon: "data_usage" },
|
{ 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" },
|
||||||
@@ -165,7 +165,7 @@ export default function Sidebar({ onClose }) {
|
|||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 px-4 py-2 space-y-0.5 overflow-y-auto custom-scrollbar">
|
<nav className="flex-1 px-4 py-2 space-y-0.5 overflow-y-auto custom-scrollbar">
|
||||||
{navItems.map((item) => (
|
{navItems.filter((item) => !item.adminOnly || user?.role === "admin").map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ const mocks = vi.hoisted(() => ({
|
|||||||
body,
|
body,
|
||||||
})),
|
})),
|
||||||
getSettings: vi.fn(),
|
getSettings: vi.fn(),
|
||||||
|
getUserById: vi.fn(),
|
||||||
validateApiKey: vi.fn(),
|
validateApiKey: vi.fn(),
|
||||||
getConsistentMachineId: vi.fn(),
|
getConsistentMachineId: vi.fn(),
|
||||||
|
getDashboardAuthSession: vi.fn(),
|
||||||
verifyDashboardAuthToken: vi.fn(),
|
verifyDashboardAuthToken: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ vi.mock("next/server", () => ({
|
|||||||
|
|
||||||
vi.mock("@/lib/localDb", () => ({
|
vi.mock("@/lib/localDb", () => ({
|
||||||
getSettings: mocks.getSettings,
|
getSettings: mocks.getSettings,
|
||||||
|
getUserById: mocks.getUserById,
|
||||||
validateApiKey: mocks.validateApiKey,
|
validateApiKey: mocks.validateApiKey,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -30,17 +33,18 @@ vi.mock("@/shared/utils/machineId", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/auth/dashboardSession", () => ({
|
vi.mock("@/lib/auth/dashboardSession", () => ({
|
||||||
|
getDashboardAuthSession: mocks.getDashboardAuthSession,
|
||||||
verifyDashboardAuthToken: mocks.verifyDashboardAuthToken,
|
verifyDashboardAuthToken: mocks.verifyDashboardAuthToken,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { proxy, __test__ } = await import("../../src/dashboardGuard.js");
|
const { proxy, __test__ } = await import("../../src/dashboardGuard.js");
|
||||||
|
|
||||||
function request(pathname, headers = {}) {
|
function request(pathname, headers = {}, authToken) {
|
||||||
const normalizedHeaders = new Headers(headers);
|
const normalizedHeaders = new Headers(headers);
|
||||||
return {
|
return {
|
||||||
nextUrl: { pathname, searchParams: new URL(`http://localhost${pathname}`).searchParams },
|
nextUrl: { pathname, searchParams: new URL(`http://localhost${pathname}`).searchParams },
|
||||||
headers: normalizedHeaders,
|
headers: normalizedHeaders,
|
||||||
cookies: { get: vi.fn(() => undefined) },
|
cookies: { get: vi.fn(() => authToken ? { value: authToken } : undefined) },
|
||||||
url: `http://localhost${pathname}`,
|
url: `http://localhost${pathname}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -49,8 +53,10 @@ describe("dashboard guard public LLM API access", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mocks.getSettings.mockResolvedValue({ requireLogin: true });
|
mocks.getSettings.mockResolvedValue({ requireLogin: true });
|
||||||
|
mocks.getUserById.mockResolvedValue(null);
|
||||||
mocks.validateApiKey.mockResolvedValue(false);
|
mocks.validateApiKey.mockResolvedValue(false);
|
||||||
mocks.getConsistentMachineId.mockResolvedValue("cli-token");
|
mocks.getConsistentMachineId.mockResolvedValue("cli-token");
|
||||||
|
mocks.getDashboardAuthSession.mockResolvedValue(null);
|
||||||
mocks.verifyDashboardAuthToken.mockResolvedValue(false);
|
mocks.verifyDashboardAuthToken.mockResolvedValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,8 +198,10 @@ describe("dashboard guard local-only access", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mocks.getSettings.mockResolvedValue({ requireLogin: true });
|
mocks.getSettings.mockResolvedValue({ requireLogin: true });
|
||||||
|
mocks.getUserById.mockResolvedValue(null);
|
||||||
mocks.validateApiKey.mockResolvedValue(false);
|
mocks.validateApiKey.mockResolvedValue(false);
|
||||||
mocks.getConsistentMachineId.mockResolvedValue("cli-token");
|
mocks.getConsistentMachineId.mockResolvedValue("cli-token");
|
||||||
|
mocks.getDashboardAuthSession.mockResolvedValue(null);
|
||||||
mocks.verifyDashboardAuthToken.mockResolvedValue(false);
|
mocks.verifyDashboardAuthToken.mockResolvedValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,6 +266,41 @@ describe("dashboard guard local-only access", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("dashboard guard combo administration access", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.getSettings.mockResolvedValue({ requireLogin: true });
|
||||||
|
mocks.getUserById.mockResolvedValue({ id: "user-1", isActive: true, role: "user" });
|
||||||
|
mocks.validateApiKey.mockResolvedValue(false);
|
||||||
|
mocks.getConsistentMachineId.mockResolvedValue("cli-token");
|
||||||
|
mocks.getDashboardAuthSession.mockResolvedValue({ userId: "user-1" });
|
||||||
|
mocks.verifyDashboardAuthToken.mockResolvedValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects normal users from every combos API operation", async () => {
|
||||||
|
for (const pathname of ["/api/combos", "/api/combos/combo-1"]) {
|
||||||
|
const response = await proxy(request(pathname, { host: "localhost:20128" }, "user-token"));
|
||||||
|
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.body.error).toBe("Administrator access required");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects normal users away from the combos dashboard page", async () => {
|
||||||
|
const response = await proxy(request("/dashboard/combos", { host: "localhost:20128" }, "user-token"));
|
||||||
|
|
||||||
|
expect(response.status).toBe(307);
|
||||||
|
expect(response.url).toBe("http://localhost/dashboard");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows administrators to access the combos page and API", async () => {
|
||||||
|
mocks.getUserById.mockResolvedValue({ id: "user-1", isActive: true, role: "admin" });
|
||||||
|
|
||||||
|
expect(await proxy(request("/dashboard/combos", { host: "localhost:20128" }, "admin-token"))).toBe(mocks.nextResponse);
|
||||||
|
expect(await proxy(request("/api/combos", { host: "localhost:20128" }, "admin-token"))).toBe(mocks.nextResponse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("dashboard guard helpers", () => {
|
describe("dashboard guard helpers", () => {
|
||||||
it("extracts bearer API keys before x-api-key", () => {
|
it("extracts bearer API keys before x-api-key", () => {
|
||||||
const apiRequest = request("/v1/chat/completions", {
|
const apiRequest = request("/v1/chat/completions", {
|
||||||
|
|||||||
Reference in New Issue
Block a user