From da705c947512e436bc740f8637f34246a1ee7c47 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 5 Sep 2026 17:49:29 -0700 Subject: [PATCH] refactor(ui): render the Virtual Keys page without the legacy user dashboard The API Keys route mounted the pre-App-Router UserDashboard component, whose beforeunload handler cleared sessionStorage on every refresh of the Virtual Keys page. That wiped the Playground chat history and model, the logs live-tail preference, and everything else other pages keep in session storage. The same component also re-decoded the login token, re-fetched teams, and wrote cache entries nothing read. ApiKeysDashboard now renders VirtualKeysTable and the Create Key button directly, taking identity and role from useAuthorized like every other page. Create Key is hidden for view-only roles, which the proxy already rejects on /key/generate. The legacy component, its test, the fetch_teams helper, and their grandfathered eslint suppressions are removed, and the ProxySettings type moves to useProxySettings. --- ui/litellm-dashboard/eslint-budgets.json | 4 +- ui/litellm-dashboard/eslint-suppressions.json | 24 +- .../api-keys/ApiKeysDashboard.test.tsx | 109 +++++--- .../(dashboard)/api-keys/ApiKeysDashboard.tsx | 44 ++-- .../hooks/proxySettings/useProxySettings.ts | 2 + .../old-usage/_components/usage.tsx | 2 +- .../common_components/fetch_teams.tsx | 18 -- .../src/components/networking.tsx | 9 +- .../src/components/user_dashboard.test.tsx | 133 ---------- .../src/components/user_dashboard.tsx | 239 ------------------ 10 files changed, 99 insertions(+), 485 deletions(-) delete mode 100644 ui/litellm-dashboard/src/components/common_components/fetch_teams.tsx delete mode 100644 ui/litellm-dashboard/src/components/user_dashboard.test.tsx delete mode 100644 ui/litellm-dashboard/src/components/user_dashboard.tsx diff --git a/ui/litellm-dashboard/eslint-budgets.json b/ui/litellm-dashboard/eslint-budgets.json index e98cea9261e..44294b5fa97 100644 --- a/ui/litellm-dashboard/eslint-budgets.json +++ b/ui/litellm-dashboard/eslint-budgets.json @@ -3,8 +3,8 @@ "no-console": { "max": 12, "target": 0 }, "complexity": { "max": 140, "target": 80 }, "max-depth": { "max": 70, "target": 30 }, - "local/no-large-inline-object-arg": { "max": 554, "target": 300 }, - "local/no-long-condition-chain": { "max": 265, "target": 120 }, + "local/no-large-inline-object-arg": { "max": 551, "target": 300 }, + "local/no-long-condition-chain": { "max": 196, "target": 120 }, "testing-library/no-container": { "max": 133, "target": 50 }, "testing-library/no-node-access": { "max": 707, "target": 500 }, "testing-library/prefer-screen-queries": { "max": 18, "target": 18 } diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index d7475173b90..76ac60a6453 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1619,14 +1619,6 @@ "count": 1 } }, - "src/components/common_components/fetch_teams.tsx": { - "local/filename-pascal-case": { - "count": 1 - }, - "max-params": { - "count": 1 - } - }, "src/components/common_components/simple_table.tsx": { "local/filename-pascal-case": { "count": 1 @@ -1823,7 +1815,7 @@ "count": 5 }, "no-restricted-syntax": { - "count": 152 + "count": 150 }, "prefer-const": { "count": 32 @@ -1871,9 +1863,6 @@ "src/components/per_user_usage.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 } }, "src/components/permissions/MCPServerPermissions.tsx": { @@ -2303,17 +2292,6 @@ "count": 1 } }, - "src/components/user_dashboard.tsx": { - "local/filename-pascal-case": { - "count": 1 - }, - "prefer-const": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/vector_store_management/types.tsx": { "local/filename-pascal-case": { "count": 1 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 index 13689afcb52..2e3b000dfec 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.test.tsx @@ -1,59 +1,90 @@ -import { render } from "@testing-library/react"; -import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -const { userDashboardSpy } = vi.hoisted(() => ({ - userDashboardSpy: vi.fn((_props: Record) => null), +const { teamListCall, authorizedSession } = vi.hoisted(() => ({ + teamListCall: vi.fn(() => new Promise(() => {})), + authorizedSession: vi.fn(), })); -vi.mock("@/components/user_dashboard", () => ({ - default: (props: Record) => userDashboardSpy(props), -})); +const session = (overrides: { userRole?: string; isViewOnly?: boolean } = {}) => ({ + isLoading: false, + isAuthorized: true, + token: "jwt", + accessToken: "sk-access", + userId: "u-123", + userEmail: "admin@example.com", + userRole: "Admin", + isViewOnly: false, + premiumUser: false, + disabledPersonalKeyCreation: false, + showSSOBanner: false, + ...overrides, +}); -// 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, - }), + default: () => authorizedSession(), })); vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ - teamListCall: vi.fn(() => new Promise(() => {})), + teamListCall, })); vi.mock("next/navigation", () => ({ useSearchParams: () => new URLSearchParams(""), })); +vi.mock("@/components/VirtualKeysPage/VirtualKeysTable", () => ({ + VirtualKeysTable: ({ headerActions }: { headerActions?: React.ReactNode }) => ( +
+ {headerActions} + + + ), +})); + +vi.mock("@/components/organisms/create_key_button", () => ({ + default: () => , +})); + import ApiKeysDashboard from "./ApiKeysDashboard"; -describe("ApiKeysDashboard identity source", () => { - it("passes the useAuthorized userID through even while AuthContext.userID is still null", () => { +describe("ApiKeysDashboard", () => { + beforeEach(() => { + teamListCall.mockClear(); + authorizedSession.mockReturnValue(session()); + sessionStorage.clear(); + }); + + it("scopes the team list to the signed-in user for non-admin roles", () => { + authorizedSession.mockReturnValue(session({ userRole: "Internal User" })); render(); - expect(userDashboardSpy).toHaveBeenCalled(); - const props = userDashboardSpy.mock.calls[0][0]; - expect(props.userID).toBe("u-123"); + expect(teamListCall).toHaveBeenCalledWith("sk-access", 1, 100, { userID: "u-123" }); + }); + + it("renders the keys table with a Create Key action for roles that can write", () => { + render(); + + expect(screen.getByRole("table", { name: "Virtual Keys" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Create Key" })).toBeInTheDocument(); + }); + + it("hides Create Key for view-only roles", () => { + authorizedSession.mockReturnValue(session({ isViewOnly: true })); + render(); + + expect(screen.getByRole("table", { name: "Virtual Keys" })).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Create Key" })).not.toBeInTheDocument(); + }); + + it("leaves other pages' session state intact when the tab reloads", () => { + sessionStorage.setItem("chatHistory", '[{"role":"user","content":"hi"}]'); + sessionStorage.setItem("selectedModel", "gpt-5.5"); + render(); + + window.dispatchEvent(new Event("beforeunload")); + + expect(sessionStorage.getItem("chatHistory")).toBe('[{"role":"user","content":"hi"}]'); + expect(sessionStorage.getItem("selectedModel")).toBe("gpt-5.5"); }); }); 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 ae0c443910a..376fee72b88 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx @@ -3,22 +3,17 @@ 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 { CreateKeyPrefillData } from "@/components/organisms/create_key_button"; -import UserDashboard from "@/components/user_dashboard"; -import { useAuth } from "@/contexts/AuthContext"; +import CreateKey, { CreateKeyPrefillData } from "@/components/organisms/create_key_button"; +import { VirtualKeysTable } from "@/components/VirtualKeysPage/VirtualKeysTable"; import { useSearchParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; export default function ApiKeysDashboard() { - // 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 { userId: userID, userRole, accessToken, isViewOnly } = useAuthorized(); const searchParams = useSearchParams()!; const [teams, setTeams] = useState(null); const [keys, setKeys] = useState([]); - const [createClicked, setCreateClicked] = useState(false); const autoOpenCreate = searchParams.get("create") === "true"; const prefillData: CreateKeyPrefillData | undefined = useMemo(() => { @@ -63,7 +58,6 @@ export default function ApiKeysDashboard() { const addKey = (data: KeyResponse) => { setKeys((prevData) => (prevData ? [...prevData, data] : [data])); - setCreateClicked((prev) => !prev); }; useEffect(() => { @@ -77,21 +71,21 @@ export default function ApiKeysDashboard() { }, [accessToken, userID, userRole]); return ( - +
+ + ) + } + /> +
); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/proxySettings/useProxySettings.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/proxySettings/useProxySettings.ts index 82cefd800f4..7925af223ae 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/proxySettings/useProxySettings.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/proxySettings/useProxySettings.ts @@ -8,6 +8,8 @@ export interface ProxySettings { PROXY_BASE_URL: string; PROXY_LOGOUT_URL: string; LITELLM_UI_API_DOC_BASE_URL?: string | null; + DISABLE_EXPENSIVE_DB_QUERIES?: boolean; + NUM_SPEND_LOGS_ROWS?: number; } const EMPTY_PROXY_SETTINGS: ProxySettings = { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx index 42be1b34f07..889a17bc88d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import ViewUserSpend from "@/components/view_user_spend"; -import { ProxySettings } from "@/components/user_dashboard"; +import { ProxySettings } from "@/app/(dashboard)/hooks/proxySettings/useProxySettings"; import AdvancedDatePicker from "@/components/shared/advanced_date_picker"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; diff --git a/ui/litellm-dashboard/src/components/common_components/fetch_teams.tsx b/ui/litellm-dashboard/src/components/common_components/fetch_teams.tsx deleted file mode 100644 index ca82fdfb144..00000000000 --- a/ui/litellm-dashboard/src/components/common_components/fetch_teams.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { teamListCall, Organization } from "../networking"; - -export const fetchTeams = async ( - accessToken: string, - userID: string | null, - userRole: string | null, - currentOrg: Organization | null, - setTeams: (teams: any[]) => void, -) => { - let givenTeams; - if (userRole != "Admin" && userRole != "Admin Viewer") { - givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null, userID); - } else { - givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || null); - } - - setTeams(givenTeams); -}; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 144c02dfcd1..a7597c30404 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -140,7 +140,7 @@ const resolveDefaultBase = (fallback: string | null): string | null => const defaultProxyBaseUrl = resolveDefaultBase(null); const WORKER_URL_KEY = "litellm_worker_url"; // If a worker URL is in localStorage, use it as the initial proxyBaseUrl. -// This survives page navigation and the sessionStorage.clear() in user_dashboard. +// This survives page navigation. const _rawWorkerUrl = typeof window !== "undefined" ? window.localStorage.getItem(WORKER_URL_KEY) : null; // Validate stored worker URL — reject non-HTTP schemes to prevent exfiltration const _initialWorkerUrl = (() => { @@ -195,10 +195,9 @@ export const getProxyBaseUrl = (): string => { /** * Switch API calls to point at a worker (or back to the control plane). - * Persists to localStorage so it survives page navigation and the - * sessionStorage.clear() in user_dashboard. Also updates the module-level - * proxyBaseUrl so in-flight code in this JS execution sees the new value - * immediately. + * Persists to localStorage so it survives page navigation. Also updates the + * module-level proxyBaseUrl so in-flight code in this JS execution sees the + * new value immediately. */ function isValidHttpUrl(url: string): boolean { try { diff --git a/ui/litellm-dashboard/src/components/user_dashboard.test.tsx b/ui/litellm-dashboard/src/components/user_dashboard.test.tsx deleted file mode 100644 index 01764613bf5..00000000000 --- a/ui/litellm-dashboard/src/components/user_dashboard.test.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { vi, describe, it, expect, beforeEach, afterEach } from "vitest"; -import { cleanup } from "@testing-library/react"; -import React from "react"; -import { renderWithProviders } from "../../tests/test-utils"; - -// Track addEventListener/removeEventListener calls for "beforeunload" -const addEventListenerSpy = vi.spyOn(window, "addEventListener"); -const removeEventListenerSpy = vi.spyOn(window, "removeEventListener"); - -// Mock next/navigation -vi.mock("next/navigation", () => ({ - useSearchParams: () => new URLSearchParams(), -})); - -// Mock networking with importOriginal so all exports are available -vi.mock("./networking", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getProxyBaseUrl: vi.fn().mockReturnValue("http://localhost:4000"), - getProxyUISettings: vi.fn().mockResolvedValue({}), - keyInfoCall: vi.fn().mockResolvedValue({}), - modelAvailableCall: vi.fn().mockResolvedValue({ data: [] }), - userGetInfoV2: vi.fn().mockResolvedValue({ - user_id: "user-1", - user_email: "test@example.com", - spend: 0, - max_budget: null, - models: [], - teams: [], - }), - }; -}); - -// Mock jwt-decode to return a valid token structure -vi.mock("jwt-decode", () => ({ - jwtDecode: vi.fn().mockReturnValue({ - key: "test-access-token", - user_role: "proxy_admin", - user_email: "test@example.com", - exp: Math.floor(Date.now() / 1000) + 3600, - }), -})); - -// Mock cookie utility -vi.mock("@/utils/cookieUtils", () => ({ - clearTokenCookies: vi.fn(), - getCookie: vi.fn().mockReturnValue("fake-jwt-token"), -})); - -// Mock fetchTeams -vi.mock("./common_components/fetch_teams", () => ({ - fetchTeams: vi.fn(), -})); - -// Mock heavy child components to isolate UserDashboard behavior -vi.mock("./organisms/create_key_button", () => ({ - default: () =>
, -})); - -vi.mock("./VirtualKeysPage/VirtualKeysTable", () => ({ - VirtualKeysTable: () =>
, -})); - -vi.mock("../app/onboarding/page", () => ({ - default: () =>
, -})); - -// Provide a token cookie so the component doesn't redirect to login -Object.defineProperty(document, "cookie", { - writable: true, - value: "token=fake-jwt-token", -}); - -import UserDashboard from "./user_dashboard"; - -const defaultProps = { - userID: "user-1", - userRole: "Admin", - userEmail: "test@example.com", - teams: [] as any[], - keys: [] as any[], - setUserRole: vi.fn(), - setUserEmail: vi.fn(), - setTeams: vi.fn(), - setKeys: vi.fn(), - premiumUser: false, - addKey: vi.fn(), - createClicked: false, -}; - -function renderDashboard(props = {}) { - return renderWithProviders(); -} - -describe("UserDashboard beforeunload listener", () => { - beforeEach(() => { - addEventListenerSpy.mockClear(); - removeEventListenerSpy.mockClear(); - }); - - afterEach(() => { - cleanup(); - }); - - it("registers exactly one beforeunload listener on mount", () => { - renderDashboard(); - - const beforeUnloadCalls = addEventListenerSpy.mock.calls.filter(([event]) => event === "beforeunload"); - expect(beforeUnloadCalls).toHaveLength(1); - }); - - it("does not add duplicate listeners on re-render", () => { - const { rerender } = renderWithProviders(); - - addEventListenerSpy.mockClear(); - - // Re-render with different props to trigger a render cycle - rerender(); - - const beforeUnloadCalls = addEventListenerSpy.mock.calls.filter(([event]) => event === "beforeunload"); - expect(beforeUnloadCalls).toHaveLength(0); - }); - - it("removes the beforeunload listener on unmount", () => { - const { unmount } = renderDashboard(); - - unmount(); - - const removeCalls = removeEventListenerSpy.mock.calls.filter(([event]) => event === "beforeunload"); - expect(removeCalls).toHaveLength(1); - }); -}); diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx deleted file mode 100644 index dcadc141103..00000000000 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ /dev/null @@ -1,239 +0,0 @@ -"use client"; -import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; -import { jwtDecode } from "jwt-decode"; -import React, { useEffect, useState } from "react"; -import { fetchTeams } from "./common_components/fetch_teams"; -import { KeyResponse, Team } from "./key_team_helpers/key_list"; -import { effectiveSessionRole } from "@/utils/roles"; -import { getProxyBaseUrl, keyInfoCall, modelAvailableCall, Organization, userGetInfoV2 } from "./networking"; -import CreateKey, { CreateKeyPrefillData } from "./organisms/create_key_button"; -import { VirtualKeysTable } from "./VirtualKeysPage/VirtualKeysTable"; - -export interface ProxySettings { - PROXY_BASE_URL: string | null; - PROXY_LOGOUT_URL: string | null; - LITELLM_UI_API_DOC_BASE_URL?: string | null; - DEFAULT_TEAM_DISABLED: boolean; - SSO_ENABLED: boolean; - DISABLE_EXPENSIVE_DB_QUERIES: boolean; - NUM_SPEND_LOGS_ROWS: number; -} - -export type UserInfo = { - models: string[]; - max_budget?: number | null; - spend: number; -}; - -interface UserDashboardProps { - userID: string | null; - userRole: string | null; - userEmail: string | null; - teams: Team[] | null; - keys: any[] | null; - setUserRole: React.Dispatch>; - setUserEmail: React.Dispatch>; - setTeams: React.Dispatch>; - setKeys: (keys: KeyResponse[]) => void; - premiumUser: boolean; - addKey: (data: any) => void; - createClicked: boolean; - autoOpenCreate?: boolean; - prefillData?: CreateKeyPrefillData; -} - -const UserDashboard: React.FC = ({ - userID, - userRole, - teams, - keys, - setUserRole, - userEmail, - setUserEmail, - setTeams, - setKeys, - premiumUser, - addKey, - createClicked, - autoOpenCreate, - prefillData, -}) => { - const [userSpendData, setUserSpendData] = useState(null); - const [currentOrg] = useState(null); - - const token = getCookie("token"); - - const [accessToken, setAccessToken] = useState(null); - const [selectedTeam] = useState(null); - - // Clear session storage on page unload so next load fetches fresh data. - // Note: MCP auth tokens are persistent and should not be cleared on page refresh - // They are only cleared on logout - useEffect(() => { - const handleBeforeUnload = () => { - const token = sessionStorage.getItem("token"); - sessionStorage.clear(); - if (token) { - sessionStorage.setItem("token", token); - } - }; - window.addEventListener("beforeunload", handleBeforeUnload); - return () => window.removeEventListener("beforeunload", handleBeforeUnload); - }, []); - - // console.log(`selectedTeam: ${Object.entries(selectedTeam)}`); - // Moved useEffect inside the component and used a condition to run fetch only if the params are available - useEffect(() => { - if (token) { - const decoded = jwtDecode(token) as { [key: string]: any }; - if (decoded) { - // cast decoded to dictionary - - // set accessToken - setAccessToken(decoded.key); - - // check if userRole is defined - if (decoded.user_role) { - setUserRole(effectiveSessionRole(decoded.user_role)); - } else { - } - - if (decoded.user_email) { - setUserEmail(decoded.user_email); - } else { - } - } - } - if (userID && accessToken && userRole && !userSpendData) { - const cachedUserModels = sessionStorage.getItem("userModels" + userID); - if (!cachedUserModels) { - const fetchData = async () => { - try { - const response = await userGetInfoV2(accessToken, userID); - - setUserSpendData(response); - - sessionStorage.setItem("userSpendData" + userID, JSON.stringify(response)); - - const model_available = await modelAvailableCall(accessToken, userID, userRole); - // loop through model_info["data"] and create an array of element.model_name - let available_model_names = model_available["data"].map((element: { id: string }) => element.id); - - sessionStorage.setItem("userModels" + userID, JSON.stringify(available_model_names)); - } catch (error: any) { - console.error("There was an error fetching the data", error); - if (error.message.includes("Invalid proxy server token passed")) { - gotoLogin(); - } - // Optionally, update your UI to reflect the error state here as well - } - }; - fetchData(); - fetchTeams(accessToken, userID, userRole, currentOrg, setTeams); - } - } - }, [userID, token, accessToken, userRole]); - - useEffect(() => { - // check key health - if it's invalid, redirect to login - if (accessToken) { - const fetchKeyInfo = async () => { - try { - await keyInfoCall(accessToken, [accessToken]); - } catch (error: any) { - if (error.message.includes("Invalid proxy server token passed")) { - gotoLogin(); - } - } - }; - fetchKeyInfo(); - } - }, [accessToken]); - - useEffect(() => { - if (accessToken) { - fetchTeams(accessToken, userID, userRole, currentOrg, setTeams); - } - }, [currentOrg]); - - function gotoLogin() { - // Clear token cookies using the utility function - clearTokenCookies(); - - const baseUrl = getProxyBaseUrl(); - - const url = baseUrl ? `${baseUrl}/sso/key/generate` : `/sso/key/generate`; - - window.location.href = url; - - return null; - } - - if (token == null) { - // user is not logged in as yet - - // Clear token cookies using the utility function - gotoLogin(); - return null; - } else { - // Check if token is expired - try { - const decoded = jwtDecode(token) as { [key: string]: any }; - const expTime = decoded.exp; - const currentTime = Math.floor(Date.now() / 1000); - - if (expTime && currentTime >= expTime) { - gotoLogin(); - - return null; - } - } catch (error) { - console.error("Error decoding token:", error); - // If there's an error decoding the token, consider it invalid - clearTokenCookies(); - - gotoLogin(); - - return null; - } - - if (accessToken == null) { - return null; - } - } - - if (userID == null) { - return

User ID is not set

; - } - - if (userRole == null) { - setUserRole("App Owner"); - } - - // Admin Viewer can view keys read-only — gate "Create Key" but render the - // virtual-keys table the same as for Proxy Admin (read parity). Every - // other role keeps its existing ability to create keys. - const canCreateKey = userRole !== "Admin Viewer" && userRole !== "proxy_admin_viewer"; - - return ( -
- - ) : undefined - } - /> -
- ); -}; - -export default UserDashboard;