From 8125ddd2f8e66c8eaf6bb449d46c2d6f7ea445a1 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 20 Jun 2026 17:48:35 -0700 Subject: [PATCH] fix(ui): source api-keys identity from useAuthorized to stop "User ID is not set" (#30903) The migrated /ui/api-keys route gates rendering on useAuthorized() but read userID from the AuthContext (useAuth), which hydrates asynchronously. On a hard refresh or deep link the route could render UserDashboard before AuthContext had populated userID, so UserDashboard hit its `userID == null` guard and showed "User ID is not set". The legacy index page avoided this by gating on AuthContext's own authLoading; the migration switched the gate to useAuthorized without aligning the identity source. Read identity from useAuthorized (a synchronous cookie decode) so userID is populated whenever the route is authorized. useAuth is kept only for the backfill setters UserDashboard still expects, until the planned AuthContext consolidation removes them. Refs LIT-3687 --- .../api-keys/ApiKeysDashboard.test.tsx | 63 +++++++++++++++++++ .../(dashboard)/api-keys/ApiKeysDashboard.tsx | 8 ++- 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.test.tsx 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() {