mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 05:31:47 +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
|
||||
// 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
|
||||
const PROTECTED_API_PATHS = [
|
||||
@@ -240,6 +244,11 @@ export async function proxy(request) {
|
||||
|
||||
// Protect all dashboard routes
|
||||
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 tunnelDashboardAccess = true;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const navItems = [
|
||||
{ href: "/dashboard/endpoint", label: "Endpoint & Key", icon: "api" },
|
||||
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
|
||||
// { 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/quota", label: "Quota Tracker", icon: "data_usage" },
|
||||
{ href: "/dashboard/token-saver", label: "Token Saver", icon: "savings" },
|
||||
@@ -165,7 +165,7 @@ export default function Sidebar({ onClose }) {
|
||||
|
||||
{/* Navigation */}
|
||||
<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
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
|
||||
@@ -7,8 +7,10 @@ const mocks = vi.hoisted(() => ({
|
||||
body,
|
||||
})),
|
||||
getSettings: vi.fn(),
|
||||
getUserById: vi.fn(),
|
||||
validateApiKey: vi.fn(),
|
||||
getConsistentMachineId: vi.fn(),
|
||||
getDashboardAuthSession: vi.fn(),
|
||||
verifyDashboardAuthToken: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -22,6 +24,7 @@ vi.mock("next/server", () => ({
|
||||
|
||||
vi.mock("@/lib/localDb", () => ({
|
||||
getSettings: mocks.getSettings,
|
||||
getUserById: mocks.getUserById,
|
||||
validateApiKey: mocks.validateApiKey,
|
||||
}));
|
||||
|
||||
@@ -30,17 +33,18 @@ vi.mock("@/shared/utils/machineId", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth/dashboardSession", () => ({
|
||||
getDashboardAuthSession: mocks.getDashboardAuthSession,
|
||||
verifyDashboardAuthToken: mocks.verifyDashboardAuthToken,
|
||||
}));
|
||||
|
||||
const { proxy, __test__ } = await import("../../src/dashboardGuard.js");
|
||||
|
||||
function request(pathname, headers = {}) {
|
||||
function request(pathname, headers = {}, authToken) {
|
||||
const normalizedHeaders = new Headers(headers);
|
||||
return {
|
||||
nextUrl: { pathname, searchParams: new URL(`http://localhost${pathname}`).searchParams },
|
||||
headers: normalizedHeaders,
|
||||
cookies: { get: vi.fn(() => undefined) },
|
||||
cookies: { get: vi.fn(() => authToken ? { value: authToken } : undefined) },
|
||||
url: `http://localhost${pathname}`,
|
||||
};
|
||||
}
|
||||
@@ -49,8 +53,10 @@ describe("dashboard guard public LLM API access", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.getSettings.mockResolvedValue({ requireLogin: true });
|
||||
mocks.getUserById.mockResolvedValue(null);
|
||||
mocks.validateApiKey.mockResolvedValue(false);
|
||||
mocks.getConsistentMachineId.mockResolvedValue("cli-token");
|
||||
mocks.getDashboardAuthSession.mockResolvedValue(null);
|
||||
mocks.verifyDashboardAuthToken.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
@@ -192,8 +198,10 @@ describe("dashboard guard local-only access", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.getSettings.mockResolvedValue({ requireLogin: true });
|
||||
mocks.getUserById.mockResolvedValue(null);
|
||||
mocks.validateApiKey.mockResolvedValue(false);
|
||||
mocks.getConsistentMachineId.mockResolvedValue("cli-token");
|
||||
mocks.getDashboardAuthSession.mockResolvedValue(null);
|
||||
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", () => {
|
||||
it("extracts bearer API keys before x-api-key", () => {
|
||||
const apiRequest = request("/v1/chat/completions", {
|
||||
|
||||
Reference in New Issue
Block a user