Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion web/src/routes/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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({
Expand Down
64 changes: 64 additions & 0 deletions web/tests/auth-user-switch.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof import('@tanstack/react-router')>()),
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<ReturnType<typeof api.getSessionInfo>>);

await expect(
queryClient.fetchQuery({
...getLicenseInfoQueryOptions,
queryFn: async () => {
throw forbidden;
},
}),
).rejects.toThrow(forbidden);
expect(queryClient.getQueryState(getLicenseInfoQueryOptions.queryKey)?.status).toBe(
'error',
);

render(
<QueryClientProvider client={queryClient}>
{createElement(Route.options.component as ComponentType)}
</QueryClientProvider>,
);

await act(async () => {
useAuth.getState().authSubject.next({
user: { username: 'admin', is_admin: true } as User,
});
});

expect(
queryClient.getQueryState(getLicenseInfoQueryOptions.queryKey),
).toBeUndefined();
queryClient.clear();
});
});
Loading