From 120fd7516a4e4eb9b4aa808bfaf53eb3072558e3 Mon Sep 17 00:00:00 2001 From: Chris Bagwell Date: Thu, 19 Mar 2026 10:33:01 -0500 Subject: [PATCH] Fix: Prevent auto-logout on 401 errors in oss mode (#13466) --- .../features/user/user-context-menu.test.tsx | 40 +++++++++++++++++-- .../features/user/user-context-menu.tsx | 17 ++++---- frontend/src/hooks/query/use-git-user.ts | 7 ++-- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/frontend/__tests__/components/features/user/user-context-menu.test.tsx b/frontend/__tests__/components/features/user/user-context-menu.test.tsx index 635f66e645..07895f547c 100644 --- a/frontend/__tests__/components/features/user/user-context-menu.test.tsx +++ b/frontend/__tests__/components/features/user/user-context-menu.test.tsx @@ -156,11 +156,19 @@ describe("UserContextMenu", () => { useSelectedOrganizationStore.setState({ organizationId: null }); }); - it("should render the default context items for a user", () => { + it("should render the default context items for a user", async () => { + vi.spyOn(OptionService, "getConfig").mockResolvedValue( + createMockWebClientConfig({ app_mode: "saas" }), + ); + renderUserContextMenu({ type: "member", onClose: vi.fn, onOpenInviteModal: vi.fn }); screen.getByTestId("org-selector"); - screen.getByText("ACCOUNT_SETTINGS$LOGOUT"); + + // Wait for config to load so logout button appears + await waitFor(() => { + expect(screen.getByText("ACCOUNT_SETTINGS$LOGOUT")).toBeInTheDocument(); + }); expect( screen.queryByText("ORG$INVITE_ORG_MEMBERS"), @@ -304,6 +312,20 @@ describe("UserContextMenu", () => { screen.queryByText("Organization Members"), ).not.toBeInTheDocument(); }); + + it("should not display logout button in OSS mode", async () => { + renderUserContextMenu({ type: "member", onClose: vi.fn, onOpenInviteModal: vi.fn }); + + // Wait for the config to load + await waitFor(() => { + expect(screen.getByText("SETTINGS$NAV_LLM")).toBeInTheDocument(); + }); + + // Verify logout button is NOT rendered in OSS mode + expect( + screen.queryByText("ACCOUNT_SETTINGS$LOGOUT"), + ).not.toBeInTheDocument(); + }); }); describe("HIDE_LLM_SETTINGS feature flag", () => { @@ -382,10 +404,15 @@ describe("UserContextMenu", () => { }); it("should call the logout handler when Logout is clicked", async () => { + vi.spyOn(OptionService, "getConfig").mockResolvedValue( + createMockWebClientConfig({ app_mode: "saas" }), + ); + const logoutSpy = vi.spyOn(AuthService, "logout"); renderUserContextMenu({ type: "member", onClose: vi.fn, onOpenInviteModal: vi.fn }); - const logoutButton = screen.getByText("ACCOUNT_SETTINGS$LOGOUT"); + // Wait for config to load so logout button appears + const logoutButton = await screen.findByText("ACCOUNT_SETTINGS$LOGOUT"); await userEvent.click(logoutButton); expect(logoutSpy).toHaveBeenCalledOnce(); @@ -488,6 +515,10 @@ describe("UserContextMenu", () => { }); it("should call the onClose handler after each action", async () => { + vi.spyOn(OptionService, "getConfig").mockResolvedValue( + createMockWebClientConfig({ app_mode: "saas" }), + ); + // Mock a team org so org management buttons are visible vi.spyOn(organizationService, "getOrganizations").mockResolvedValue({ items: [MOCK_TEAM_ORG_ACME], @@ -497,7 +528,8 @@ describe("UserContextMenu", () => { const onCloseMock = vi.fn(); renderUserContextMenu({ type: "owner", onClose: onCloseMock, onOpenInviteModal: vi.fn }); - const logoutButton = screen.getByText("ACCOUNT_SETTINGS$LOGOUT"); + // Wait for config to load so logout button appears + const logoutButton = await screen.findByText("ACCOUNT_SETTINGS$LOGOUT"); await userEvent.click(logoutButton); expect(onCloseMock).toHaveBeenCalledTimes(1); diff --git a/frontend/src/components/features/user/user-context-menu.tsx b/frontend/src/components/features/user/user-context-menu.tsx index b9094cc6d3..424dc7c0ec 100644 --- a/frontend/src/components/features/user/user-context-menu.tsx +++ b/frontend/src/components/features/user/user-context-menu.tsx @@ -156,13 +156,16 @@ export function UserContextMenu({ {t(I18nKey.SIDEBAR$DOCS)} - - - {t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)} - + {/* Only show logout in saas mode - oss mode has no session to invalidate */} + {isSaasMode && ( + + + {t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)} + + )} diff --git a/frontend/src/hooks/query/use-git-user.ts b/frontend/src/hooks/query/use-git-user.ts index 971999f25c..a239b2d18a 100644 --- a/frontend/src/hooks/query/use-git-user.ts +++ b/frontend/src/hooks/query/use-git-user.ts @@ -35,13 +35,14 @@ export const useGitUser = () => { } }, [user.data]); - // If we get a 401 here, it means that the integration tokens need to be + // In saas mode, a 401 means that the integration tokens need to be // refreshed. Since this happens at login, we log out. + // In oss mode, skip auto-logout since there's no token refresh mechanism React.useEffect(() => { - if (user?.error?.response?.status === 401) { + if (user?.error?.response?.status === 401 && config?.app_mode === "saas") { logout.mutate(); } - }, [user.status]); + }, [user.status, config?.app_mode]); return user; };