From 6b74e8223bf74daada1412882d107dccc774b777 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 5 Dec 2025 12:23:25 -0800 Subject: [PATCH 1/3] change useAuthorized Hook to redirect to new login page --- .../(dashboard)/hooks/useAuthorized.test.ts | 80 +++++++++++++++++++ .../app/(dashboard)/hooks/useAuthorized.ts | 5 +- 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts new file mode 100644 index 00000000000..5059d5d69d1 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts @@ -0,0 +1,80 @@ +/* @vitest-environment jsdom */ +import { renderHook } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import useAuthorized from "./useAuthorized"; + +const replaceMock = vi.fn(); +const clearTokenCookiesMock = vi.fn(); +const getProxyBaseUrlMock = vi.fn(() => "http://proxy.example"); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + replace: replaceMock, + }), +})); + +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: getProxyBaseUrlMock, +})); + +vi.mock("@/utils/cookieUtils", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + clearTokenCookies: clearTokenCookiesMock, + }; +}); + +const createJwt = (payload: Record) => { + const base64Url = btoa(JSON.stringify(payload)).replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_"); + return `eyJhbGciOiJub25lIn0.${base64Url}.signature`; +}; + +const clearCookie = () => { + document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; +}; + +describe("useAuthorized", () => { + afterEach(() => { + replaceMock.mockReset(); + clearTokenCookiesMock.mockReset(); + getProxyBaseUrlMock.mockClear(); + clearCookie(); + }); + + it("should decode the token and expose user details", () => { + const token = createJwt({ + key: "api-key-123", + user_id: "user-1", + user_email: "user@example.com", + user_role: "app_admin", + premium_user: true, + disabled_non_admin_personal_key_creation: false, + login_method: "username_password", + }); + document.cookie = `token=${token}; path=/;`; + + const { result } = renderHook(() => useAuthorized()); + + expect(result.current.token).toBe(token); + expect(result.current.accessToken).toBe("api-key-123"); + expect(result.current.userId).toBe("user-1"); + expect(result.current.userEmail).toBe("user@example.com"); + expect(result.current.userRole).toBe("Admin"); + expect(result.current.premiumUser).toBe(true); + expect(result.current.disabledPersonalKeyCreation).toBe(false); + expect(result.current.showSSOBanner).toBe(true); + expect(replaceMock).not.toHaveBeenCalled(); + }); + + it("should clear cookies and redirect on an invalid token", () => { + document.cookie = "token=invalid-token; path=/;"; + + const { result } = renderHook(() => useAuthorized()); + + expect(clearTokenCookiesMock).toHaveBeenCalled(); + expect(replaceMock).toHaveBeenCalledWith("http://proxy.example/ui/login"); + expect(result.current.accessToken).toBeNull(); + expect(result.current.userRole).toBe("Unknown Role"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts index cba7c1a3dc8..7610c6346be 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts @@ -4,6 +4,7 @@ import { useEffect, useMemo } from "react"; import { useRouter } from "next/navigation"; import { jwtDecode } from "jwt-decode"; import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; +import { getProxyBaseUrl } from "@/components/networking"; function formatUserRole(userRole: string) { if (!userRole) { @@ -42,7 +43,7 @@ const useAuthorized = () => { // Redirect after mount if missing/invalid token useEffect(() => { if (!token) { - router.replace("/sso/key/generate"); + router.replace(`${getProxyBaseUrl()}/ui/login`); } }, [token, router]); @@ -54,7 +55,7 @@ const useAuthorized = () => { } catch { // Bad token in cookie — clear and bounce clearTokenCookies(); - router.replace("/sso/key/generate"); + router.replace(`${getProxyBaseUrl()}/ui/login`); return null; } }, [token, router]); From ac9ce4390221da0ee7ee16c55ea96f00ab499ab3 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 5 Dec 2025 12:24:22 -0800 Subject: [PATCH 2/3] Fixing test --- .../src/app/(dashboard)/hooks/useAuthorized.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts index 5059d5d69d1..9198450a63d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.test.ts @@ -3,9 +3,11 @@ import { renderHook } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import useAuthorized from "./useAuthorized"; -const replaceMock = vi.fn(); -const clearTokenCookiesMock = vi.fn(); -const getProxyBaseUrlMock = vi.fn(() => "http://proxy.example"); +const { replaceMock, clearTokenCookiesMock, getProxyBaseUrlMock } = vi.hoisted(() => ({ + replaceMock: vi.fn(), + clearTokenCookiesMock: vi.fn(), + getProxyBaseUrlMock: vi.fn(() => "http://proxy.example"), +})); vi.mock("next/navigation", () => ({ useRouter: () => ({ @@ -75,6 +77,6 @@ describe("useAuthorized", () => { expect(clearTokenCookiesMock).toHaveBeenCalled(); expect(replaceMock).toHaveBeenCalledWith("http://proxy.example/ui/login"); expect(result.current.accessToken).toBeNull(); - expect(result.current.userRole).toBe("Unknown Role"); + expect(result.current.userRole).toBe("Undefined Role"); }); }); From e21bf1982cf687ace89b8bb1db8f34fb0eb9077f Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 5 Dec 2025 12:40:58 -0800 Subject: [PATCH 3/3] Fixing e2e --- .../e2e_ui_tests/view_internal_user.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts index 8be5ff0c540..832832d8ae8 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts @@ -39,9 +39,8 @@ test("view internal user page", async ({ page }) => { const rowCount = await page.locator("tbody tr").count(); expect(rowCount).toBeGreaterThan(0); - const userIdHeader = page.locator("th", { hasText: "User ID" }); - page.screenshot({ path: "test-results/user_id_header.png" }); - await expect(userIdHeader).toBeVisible(); + const userIdHeader = await page.locator("th", { hasText: "User ID" }); + await expect(userIdHeader).toBeVisible({ timeout: 10000 }); // test pagination // Wait for pagination controls to be visible