From d03404d21e040a4187f850c3e8057596fec93ba6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Mar 2026 07:48:10 +0000 Subject: [PATCH] feat(ui): add userGetInfoV2 networking function and migrate useCurrentUser hook - Add UserInfoV2Response type and userGetInfoV2() function in networking.tsx - Migrate useCurrentUser hook from userInfoCall to userGetInfoV2 - Update useCurrentUser.test.ts to test new v2 API integration - The hook no longer needs userRole since the endpoint handles auth itself Co-authored-by: yuneng-jiang --- .../hooks/users/useCurrentUser.test.ts | 94 +++++++------------ .../(dashboard)/hooks/users/useCurrentUser.ts | 14 ++- .../src/components/networking.tsx | 59 ++++++++++++ 3 files changed, 101 insertions(+), 66 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.test.ts index a392a940f98..0b37b605460 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.test.ts @@ -3,12 +3,12 @@ import { renderHook, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import React, { ReactNode } from "react"; import { useCurrentUser } from "./useCurrentUser"; -import { userInfoCall } from "@/components/networking"; -import type { UserInfo } from "@/components/view_users/types"; +import { userGetInfoV2 } from "@/components/networking"; +import type { UserInfoV2Response } from "@/components/networking"; // Mock the networking function vi.mock("@/components/networking", () => ({ - userInfoCall: vi.fn(), + userGetInfoV2: vi.fn(), })); // Mock the queryKeysFactory - we'll mock the specific return value @@ -28,21 +28,22 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => mockUseAuthorized(), })); -// Mock data - response from userInfoCall should have user_info property -const mockUserInfoResponse = { - user_info: { - user_id: "test-user-id", - user_email: "test@example.com", - user_alias: "Test User", - user_role: "Admin", - spend: 150.75, - max_budget: 1000.0, - key_count: 5, - created_at: "2024-01-01T00:00:00Z", - updated_at: "2024-01-01T00:00:00Z", - sso_user_id: null, - budget_duration: "monthly", - } as UserInfo, +// Mock data - response from userGetInfoV2 is the user object directly +const mockUserInfoV2Response: UserInfoV2Response = { + user_id: "test-user-id", + user_email: "test@example.com", + user_alias: "Test User", + user_role: "internal_user", + spend: 150.75, + max_budget: 1000.0, + models: ["gpt-4"], + budget_duration: "monthly", + budget_reset_at: null, + metadata: null, + created_at: "2024-01-01T00:00:00Z", + updated_at: "2024-01-01T00:00:00Z", + sso_user_id: null, + teams: ["team-1"], }; describe("useCurrentUser", () => { @@ -77,8 +78,8 @@ describe("useCurrentUser", () => { React.createElement(QueryClientProvider, { client: queryClient }, children); it("should return user info data when query is successful", async () => { - // Mock successful API call - (userInfoCall as any).mockResolvedValue(mockUserInfoResponse); + // Mock successful API call - v2 returns user object directly + (userGetInfoV2 as any).mockResolvedValue(mockUserInfoV2Response); const { result } = renderHook(() => useCurrentUser(), { wrapper }); @@ -92,18 +93,19 @@ describe("useCurrentUser", () => { expect(result.current.isSuccess).toBe(true); }); - expect(result.current.data).toEqual(mockUserInfoResponse.user_info); + expect(result.current.data).toEqual(mockUserInfoV2Response); expect(result.current.error).toBeNull(); - expect(userInfoCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin", false, null, null); - expect(userInfoCall).toHaveBeenCalledTimes(1); + // v2 call only needs accessToken (no userId for self-lookup) + expect(userGetInfoV2).toHaveBeenCalledWith("test-access-token"); + expect(userGetInfoV2).toHaveBeenCalledTimes(1); }); - it("should handle error when userInfoCall fails", async () => { + it("should handle error when userGetInfoV2 fails", async () => { const errorMessage = "Failed to fetch user info"; const testError = new Error(errorMessage); // Mock failed API call - (userInfoCall as any).mockRejectedValue(testError); + (userGetInfoV2 as any).mockRejectedValue(testError); const { result } = renderHook(() => useCurrentUser(), { wrapper }); @@ -118,8 +120,8 @@ describe("useCurrentUser", () => { expect(result.current.error).toEqual(testError); expect(result.current.data).toBeUndefined(); - expect(userInfoCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin", false, null, null); - expect(userInfoCall).toHaveBeenCalledTimes(1); + expect(userGetInfoV2).toHaveBeenCalledWith("test-access-token"); + expect(userGetInfoV2).toHaveBeenCalledTimes(1); }); it("should not execute query when accessToken is missing", async () => { @@ -143,7 +145,7 @@ describe("useCurrentUser", () => { expect(result.current.isFetched).toBe(false); // API should not be called - expect(userInfoCall).not.toHaveBeenCalled(); + expect(userGetInfoV2).not.toHaveBeenCalled(); }); it("should not execute query when userId is missing", async () => { @@ -167,31 +169,7 @@ describe("useCurrentUser", () => { expect(result.current.isFetched).toBe(false); // API should not be called - expect(userInfoCall).not.toHaveBeenCalled(); - }); - - it("should not execute query when userRole is missing", async () => { - // Mock missing userRole - mockUseAuthorized.mockReturnValue({ - accessToken: "test-access-token", - userId: "test-user-id", - userRole: null, - token: "test-token", - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, - }); - - const { result } = renderHook(() => useCurrentUser(), { wrapper }); - - // Query should not execute - expect(result.current.isLoading).toBe(false); - expect(result.current.data).toBeUndefined(); - expect(result.current.isFetched).toBe(false); - - // API should not be called - expect(userInfoCall).not.toHaveBeenCalled(); + expect(userGetInfoV2).not.toHaveBeenCalled(); }); it("should not execute query when all auth values are missing", async () => { @@ -215,12 +193,12 @@ describe("useCurrentUser", () => { expect(result.current.isFetched).toBe(false); // API should not be called - expect(userInfoCall).not.toHaveBeenCalled(); + expect(userGetInfoV2).not.toHaveBeenCalled(); }); it("should execute query when all auth values are present", async () => { // Mock successful API call - (userInfoCall as any).mockResolvedValue(mockUserInfoResponse); + (userGetInfoV2 as any).mockResolvedValue(mockUserInfoV2Response); // Ensure all auth values are present (already set in beforeEach) const { result } = renderHook(() => useCurrentUser(), { wrapper }); @@ -230,15 +208,15 @@ describe("useCurrentUser", () => { expect(result.current.isLoading).toBe(false); }); - expect(userInfoCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin", false, null, null); - expect(userInfoCall).toHaveBeenCalledTimes(1); + expect(userGetInfoV2).toHaveBeenCalledWith("test-access-token"); + expect(userGetInfoV2).toHaveBeenCalledTimes(1); }); it("should handle network timeout error", async () => { const timeoutError = new Error("Network timeout"); // Mock network timeout - (userInfoCall as any).mockRejectedValue(timeoutError); + (userGetInfoV2 as any).mockRejectedValue(timeoutError); const { result } = renderHook(() => useCurrentUser(), { wrapper }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.ts index f4028ada0dc..793f37feb5f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/users/useCurrentUser.ts @@ -1,19 +1,17 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { UserInfo, userInfoCall } from "@/components/networking"; +import { UserInfoV2Response, userGetInfoV2 } from "@/components/networking"; import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { createQueryKeys } from "../common/queryKeysFactory"; const userKeys = createQueryKeys("users"); -export const useCurrentUser = (): UseQueryResult => { - const { accessToken, userId, userRole } = useAuthorized(); - return useQuery({ +export const useCurrentUser = (): UseQueryResult => { + const { accessToken, userId } = useAuthorized(); + return useQuery({ queryKey: userKeys.detail(userId!), queryFn: async () => { - const data = await userInfoCall(accessToken!, userId!, userRole!, false, null, null); - console.log(`userInfo: ${JSON.stringify(data)}`); - return data.user_info; + return await userGetInfoV2(accessToken!); }, - enabled: Boolean(accessToken && userId && userRole), + enabled: Boolean(accessToken && userId), }); }; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 70156a7d2b0..b63c6d9278f 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1199,6 +1199,65 @@ export const userListCall = async ( } }; +/** + * Response type for /v2/user/info — lightweight endpoint that returns only the user object. + */ +export interface UserInfoV2Response { + user_id: string; + user_email: string | null; + user_alias: string | null; + user_role: string | null; + spend: number; + max_budget: number | null; + models: string[]; + budget_duration: string | null; + budget_reset_at: string | null; + metadata: Record | null; + created_at: string | null; + updated_at: string | null; + sso_user_id: string | null; + teams: string[]; +} + +/** + * Lightweight user info fetch from /v2/user/info. + * Returns only the user object — no keys, no teams objects. + * + * @param accessToken - Bearer token for auth + * @param userId - Optional user ID to look up. If omitted, returns the caller's own info. + */ +export const userGetInfoV2 = async ( + accessToken: string, + userId?: string, +): Promise => { + try { + let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/user/info` : `/v2/user/info`; + if (userId) { + url += `?user_id=${encodeURIComponent(userId)}`; + } + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + console.error("Failed to fetch user info v2:", error); + throw error; + } +}; + export const userInfoCall = async ( accessToken: string, userID: string | null,