diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.test.tsx new file mode 100644 index 00000000000..3a3251ea76a --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.test.tsx @@ -0,0 +1,63 @@ +import { render } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; + +const { userDashboardSpy } = vi.hoisted(() => ({ + userDashboardSpy: vi.fn((_props: Record) => null), +})); + +vi.mock("@/components/user_dashboard", () => ({ + default: (props: Record) => userDashboardSpy(props), +})); + +// AuthContext is still hydrating: userID has not been populated yet (the regression). +vi.mock("@/contexts/AuthContext", () => ({ + useAuth: () => ({ + userID: null, + userRole: "", + userEmail: null, + accessToken: null, + premiumUser: false, + setUserRole: vi.fn(), + setUserEmail: vi.fn(), + }), +})); + +// useAuthorized decodes the cookie synchronously, so identity is already available. +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => ({ + isLoading: false, + isAuthorized: true, + token: "jwt", + accessToken: "sk-access", + userId: "u-123", + userEmail: "admin@example.com", + userRole: "Admin", + premiumUser: false, + disabledPersonalKeyCreation: false, + showSSOBanner: false, + }), +})); + +vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ + teamListCall: vi.fn(() => new Promise(() => {})), +})); + +vi.mock("@/components/organizations", () => ({ + fetchOrganizations: vi.fn(), +})); + +vi.mock("next/navigation", () => ({ + useSearchParams: () => new URLSearchParams(""), +})); + +import ApiKeysDashboard from "./ApiKeysDashboard"; + +describe("ApiKeysDashboard identity source", () => { + it("passes the useAuthorized userID through even while AuthContext.userID is still null", () => { + render(); + + expect(userDashboardSpy).toHaveBeenCalled(); + const props = userDashboardSpy.mock.calls[0][0]; + expect(props.userID).toBe("u-123"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx index 9c8bdd5c56f..54f4bf41a21 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx @@ -1,6 +1,7 @@ "use client"; import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; import { Organization } from "@/components/networking"; import { CreateKeyPrefillData } from "@/components/organisms/create_key_button"; @@ -11,7 +12,10 @@ import { useSearchParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; export default function ApiKeysDashboard() { - const { userID, userRole, userEmail, accessToken, premiumUser, setUserRole, setUserEmail } = useAuth(); + // Identity comes from useAuthorized (synchronous cookie decode) so userID is set whenever the + // route is authorized; useAuth only supplies the backfill setters UserDashboard still expects. + const { userId: userID, userRole, userEmail, accessToken, premiumUser } = useAuthorized(); + const { setUserRole, setUserEmail } = useAuth(); const searchParams = useSearchParams()!; const [teams, setTeams] = useState(null); @@ -82,7 +86,7 @@ export default function ApiKeysDashboard() {