From 5443d045ac711dd3ae38fc1a204c7957166c6af1 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 26 Oct 2026 10:22:50 +0100 Subject: [PATCH 1/2] remove cached license on login --- .../UserProfilePage/UserProfilePage.tsx | 6 +- web/src/routes/auth.tsx | 3 +- web/tests/auth-user-switch.test.tsx | 64 +++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 web/tests/auth-user-switch.test.tsx diff --git a/web/src/pages/user-profile/UserProfilePage/UserProfilePage.tsx b/web/src/pages/user-profile/UserProfilePage/UserProfilePage.tsx index 393dbc6c41..09ce8a8690 100644 --- a/web/src/pages/user-profile/UserProfilePage/UserProfilePage.tsx +++ b/web/src/pages/user-profile/UserProfilePage/UserProfilePage.tsx @@ -32,6 +32,7 @@ const defaultTab = UserProfileTab.Details; export const UserProfilePage = () => { const navigate = useNavigate(); const authUsername = useAuth((s) => s.user?.username as string); + const isAdmin = useAuth((s) => s.isAdmin); const search = useSearch({ from: '/_authorized/_default/user/$username' }); const activeTab = search.tab ?? defaultTab; @@ -46,7 +47,10 @@ export const UserProfilePage = () => { const { data: userProfile } = useSuspenseQuery(userProfileQueryOptions(username)); const { data: userAuthKeys } = useSuspenseQuery(getUserAuthKeysQueryOptions(username)); - const { data: licenseInfo } = useQuery(getLicenseInfoQueryOptions); + const { data: licenseInfo } = useQuery({ + ...getLicenseInfoQueryOptions, + enabled: isAdmin, + }); const apiTokensTabAvailability = useMemo((): ApiTokensTabAvailabilityValue => { if (!userProfile.user.is_admin) { diff --git a/web/src/routes/auth.tsx b/web/src/routes/auth.tsx index 478a0e5cd5..4d4fe1629f 100644 --- a/web/src/routes/auth.tsx +++ b/web/src/routes/auth.tsx @@ -6,7 +6,7 @@ import api from '../shared/api/api'; import { ActiveWizard, type User, UserMfaMethod } from '../shared/api/types'; import { isPresent } from '../shared/defguard-ui/utils/isPresent'; import { useAuth } from '../shared/hooks/useAuth'; -import { getSessionInfoQueryOptions } from '../shared/query'; +import { getLicenseInfoQueryOptions, getSessionInfoQueryOptions } from '../shared/query'; const basicSchema = z.object({ url: z.string().nullable().optional(), @@ -73,6 +73,7 @@ function RouteComponent() { const basicResult = basicSchema.safeParse(state); const basicResponse = basicResult.data; if (isPresent(basicResponse) && basicResult.success) { + queryClient.removeQueries({ queryKey: getLicenseInfoQueryOptions.queryKey }); useAuth.getState().setUser(basicResponse.user); await Promise.all([ queryClient.invalidateQueries({ diff --git a/web/tests/auth-user-switch.test.tsx b/web/tests/auth-user-switch.test.tsx new file mode 100644 index 0000000000..2859aa4342 --- /dev/null +++ b/web/tests/auth-user-switch.test.tsx @@ -0,0 +1,64 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { act, render } from '@testing-library/react'; +import { type ComponentType, createElement } from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { Route } from '../src/routes/auth'; +import api from '../src/shared/api/api'; +import type { User } from '../src/shared/api/types'; +import { useAuth } from '../src/shared/hooks/useAuth'; +import { getLicenseInfoQueryOptions } from '../src/shared/query'; + +vi.mock('@tanstack/react-router', async (importOriginal) => ({ + ...(await importOriginal()), + useNavigate: () => vi.fn(), + Outlet: () => null, +})); + +afterEach(() => { + vi.clearAllTimers(); + vi.useRealTimers(); + vi.restoreAllMocks(); + useAuth.getState().reset(); +}); + +describe('switching users', () => { + it('discards the previous user license error when login succeeds', async () => { + vi.useFakeTimers(); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + const forbidden = new Error('Request failed with status code 403'); + vi.spyOn(api, 'getSessionInfo').mockResolvedValue({ + data: { authorized: true, is_admin: true, active_wizard: null, username: 'admin' }, + } as Awaited>); + + await expect( + queryClient.fetchQuery({ + ...getLicenseInfoQueryOptions, + queryFn: async () => { + throw forbidden; + }, + }), + ).rejects.toThrow(forbidden); + expect(queryClient.getQueryState(getLicenseInfoQueryOptions.queryKey)?.status).toBe( + 'error', + ); + + render( + + {createElement(Route.options.component as ComponentType)} + , + ); + + await act(async () => { + useAuth.getState().authSubject.next({ + user: { username: 'admin', is_admin: true } as User, + }); + }); + + expect( + queryClient.getQueryState(getLicenseInfoQueryOptions.queryKey), + ).toBeUndefined(); + queryClient.clear(); + }); +}); From c3df0bc663a9db6a8c3479cda2b840a9d3d10a57 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 26 Oct 2026 10:38:41 +0100 Subject: [PATCH 2/2] cargo deny ignore crypto-bigint cve --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 73920a59a2..e8ed37dd42 100644 --- a/deny.toml +++ b/deny.toml @@ -71,6 +71,7 @@ feature-depth = 1 # output a note when they are encountered. ignore = [ { id = "RUSTSEC-2023-0071", reason = "https://github.com/RustCrypto/RSA/issues/19" }, + { id = "RUSTSEC-2026-0387", reason = "Transitive dependency of jsonwebtoken" }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library.