From a4687bb4f06b5f5e878f762a9d0f2277acc54b06 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:43:26 -0700 Subject: [PATCH 1/4] fix(admin): resolve recovery gate in loader and smooth org switching Resolve the admin capability check in the _app route loader via ensureQueryData and read it with useSuspenseQuery, so the layout renders a decided state instead of flashing the full panel before the access-denied screen. Type the root route context so the loader can reach the query client. Complete an org switch entirely client-side: invalidate every query against the newly minted session and re-run the loaders, so the panel transitions to the dashboard without a hard reload and without leaving the user stranded on the recovery screen. Make the recovery screen legible: a single Continue action when the actor administers one org, a select-then-confirm dropdown when they administer several (selecting no longer switches on its own), and a named "Switching to X" interstitial during the transition. Show the placeholder in the empty dropdown, vertically center the screen, and prefetch the org list so the control is present on first paint. --- .../shared/OrganizationSwitcher.tsx | 93 +++++++++++++++---- src/hooks/useCapabilities.ts | 41 +++++--- src/locales/en/translation.json | 4 + src/routes/__root.tsx | 5 +- src/routes/_app.tsx | 45 +++++++-- 5 files changed, 147 insertions(+), 41 deletions(-) diff --git a/src/components/shared/OrganizationSwitcher.tsx b/src/components/shared/OrganizationSwitcher.tsx index 6027dc82..ec3c30ec 100644 --- a/src/components/shared/OrganizationSwitcher.tsx +++ b/src/components/shared/OrganizationSwitcher.tsx @@ -1,40 +1,94 @@ import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useRouter } from '@tanstack/react-router'; -import { Select } from '@clickhouse/click-ui'; +import { Button, Icon, Select } from '@clickhouse/click-ui'; import { useLocalize } from '@/hooks'; import { adminOrganizationsQueryOptions, switchAdminOrganizationFn } from '@/server'; +/** Keep the switch interstitial on screen at least this long so a fast switch + * reads as a deliberate transition rather than a jarring flash. */ +const MIN_INTERSTITIAL_MS = 400; + export function OrganizationSwitcher() { const localize = useLocalize(); const router = useRouter(); const queryClient = useQueryClient(); const [selectedOrgId, setSelectedOrgId] = useState(''); + const [switchingTo, setSwitchingTo] = useState(null); const organizationsQuery = useQuery(adminOrganizationsQueryOptions); const switchMutation = useMutation({ mutationFn: (targetOrgId: string) => switchAdminOrganizationFn({ data: { targetOrgId } }), onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['adminOrganizations'] }); + // Hold the named interstitial briefly so a fast switch does not flash, + // then refetch every query against the new session (the overlay hides the + // refetch, so no previous-org data is ever shown) and re-run the loaders. + await new Promise((resolve) => setTimeout(resolve, MIN_INTERSTITIAL_MS)); + await queryClient.invalidateQueries(); await router.invalidate(); await router.navigate({ to: '/' }); }, - onError: () => setSelectedOrgId(''), + onError: () => { + setSwitchingTo(null); + setSelectedOrgId(''); + }, }); const organizations = organizationsQuery.data ?? []; if (organizations.length === 0) return null; + const startSwitch = (organization: { id: string; name: string }) => { + setSwitchingTo(organization.name); + switchMutation.mutate(organization.id); + }; + + // Named, full-screen transition that covers the mutation + refetch + reroute, + // so the context switch reads as intentional instead of snapping into place. + if (switchingTo) { + return ( +
+ + + {localize('com_admin_org_switcher_switching_to', { org: switchingTo })} + +
+ ); + } + + const error = switchMutation.isError && ( +

+ {localize('com_admin_org_switcher_error')} +

+ ); + + // Exactly one administered org: a single explicit action reads better than a + // one-item dropdown, and still requires a deliberate click (no silent switch). + if (organizations.length === 1) { + const organization = organizations[0]; + return ( +
+
+ ); + } + + // Multiple orgs: selecting only sets the target; the switch is a separate, + // deliberate click so a stray selection never navigates the user away. + const selected = organizations.find((organization) => organization.id === selectedOrgId); return (
- {switchMutation.isPending && ( -

- {localize('com_admin_org_switcher_switching')} -

- )} - {switchMutation.isError && ( -

- {localize('com_admin_org_switcher_error')} -

- )} +
); } diff --git a/src/hooks/useCapabilities.ts b/src/hooks/useCapabilities.ts index c359f46e..c9dc540c 100644 --- a/src/hooks/useCapabilities.ts +++ b/src/hooks/useCapabilities.ts @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { queryOptions, useQuery } from '@tanstack/react-query'; import { getRouteApi } from '@tanstack/react-router'; import { getEffectiveCapabilitiesFn } from '@/server'; import { hasImpliedCapability } from '@/constants'; @@ -11,26 +11,34 @@ const GRANTS_UNAVAILABLE_PATTERN = /\b(404|503)\b|endpoint not found|fetch faile const AUTH_DENIED_PATTERN = /\b(401|403)\b|forbidden|unauthorized|authentication required|no admin session token/i; -export function useCapabilities(): { +interface CapabilitiesData { + available: boolean; capabilities: string[]; - hasCapability: (cap: string) => boolean; - isLoading: boolean; - isError: boolean; -} { - const { user } = Route.useRouteContext(); - const query = useQuery({ - queryKey: ['effectiveCapabilities', user?.id ?? ''], - queryFn: async () => { +} + +/** + * Shared query for the current admin's effective capabilities. + * + * Errors are classified into `available` (grants system reachable) plus a + * capability list rather than thrown, so the admin gate can be resolved from + * data alone. Prefetch this in a route loader with `ensureQueryData` so the + * layout never renders in a loading state; `useCapabilities` reads the same + * cache entry for per-feature checks. + */ +export const capabilitiesQueryOptions = (userId: string) => + queryOptions({ + queryKey: ['effectiveCapabilities', userId], + queryFn: async (): Promise => { try { const res = await getEffectiveCapabilitiesFn(); return { available: true, capabilities: res.capabilities }; } catch (err) { const message = err instanceof Error ? err.message : String(err); if (GRANTS_UNAVAILABLE_PATTERN.test(message)) { - return { available: false, capabilities: [] as string[] }; + return { available: false, capabilities: [] }; } if (AUTH_DENIED_PATTERN.test(message)) { - return { available: true, capabilities: [] as string[] }; + return { available: true, capabilities: [] }; } throw err; } @@ -39,6 +47,15 @@ export function useCapabilities(): { retry: false, }); +export function useCapabilities(): { + capabilities: string[]; + hasCapability: (cap: string) => boolean; + isLoading: boolean; + isError: boolean; +} { + const { user } = Route.useRouteContext(); + const query = useQuery(capabilitiesQueryOptions(user?.id ?? '')); + const grantsAvailable = query.data?.available ?? false; const capabilities = query.data?.capabilities ?? []; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 89e07770..10885be8 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1232,6 +1232,10 @@ "com_admin_return_to_librechat": "Return to LibreChat", "com_admin_org_switcher_label": "Choose an organization where you have admin access", "com_admin_org_switcher_placeholder": "Select an organization", + "com_admin_org_switcher_continue": "Continue to {{org}}", + "com_admin_org_switcher_switch": "Switch organization", + "com_admin_org_switcher_switch_to": "Switch to {{org}}", + "com_admin_org_switcher_switching_to": "Switching to {{org}}...", "com_admin_org_switcher_switching": "Switching organization...", "com_admin_org_switcher_error": "We couldn't switch organizations. Please try again." } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 18d99af2..12981822 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -9,8 +9,9 @@ import { Outlet, ScriptOnce, Scripts, - createRootRoute, + createRootRouteWithContext, } from '@tanstack/react-router'; +import type { QueryClient } from '@tanstack/react-query'; import type { ErrorComponentProps } from '@tanstack/react-router'; import { ThemeProvider, useTheme } from '../contexts/ThemeContext'; import appCss from '../styles.css?url'; @@ -24,7 +25,7 @@ const themeScript = `(function(){ } catch(e) {} })();`; -export const Route = createRootRoute({ +export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ ssr: false, head: () => ({ meta: [ diff --git a/src/routes/_app.tsx b/src/routes/_app.tsx index 4eeb0421..95442bc4 100644 --- a/src/routes/_app.tsx +++ b/src/routes/_app.tsx @@ -1,13 +1,14 @@ import { useState, useEffect } from 'react'; import { Icon } from '@clickhouse/click-ui'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { createFileRoute, Outlet, useRouter, Link, redirect } from '@tanstack/react-router'; import type { ErrorComponentProps } from '@tanstack/react-router'; -import { useCapabilities, useCommandMenu, useLocalize } from '@/hooks'; +import { capabilitiesQueryOptions, useCommandMenu, useLocalize } from '@/hooks'; import { CommandMenu } from '@/components/CommandMenu'; -import { AccessDenied } from '@/components/shared'; -import { SystemCapabilities } from '@/constants'; +import { AccessDenied, LoadingState } from '@/components/shared'; +import { SystemCapabilities, hasImpliedCapability } from '@/constants'; import { Sidebar } from '@/components/Sidebar'; -import { verifyAdminTokenFn } from '@/server'; +import { adminOrganizationsQueryOptions, verifyAdminTokenFn } from '@/server'; import { Header } from '@/components/Header'; const ROUTE_TITLE_KEYS: Record = { @@ -19,7 +20,6 @@ const ROUTE_TITLE_KEYS: Record = { '/help': 'com_help_title', }; - export const Route = createFileRoute('/_app')({ beforeLoad: async ({ location }) => { const result = await verifyAdminTokenFn(); @@ -33,14 +33,37 @@ export const Route = createFileRoute('/_app')({ return { user: result.user }; }, + // Resolve the admin capability check at the route boundary so the layout + // renders a decided state (authorized or denied) and never flashes chrome. + // On the recovery (non-admin) path, also warm the admin-org list so the + // switcher is present on first paint instead of popping in. + loader: async ({ context }) => { + const caps = await context.queryClient.ensureQueryData( + capabilitiesQueryOptions(context.user.id), + ); + const hasAdmin = + caps.available && hasImpliedCapability(caps.capabilities, SystemCapabilities.ACCESS_ADMIN); + if (!hasAdmin) { + await context.queryClient.ensureQueryData(adminOrganizationsQueryOptions); + } + }, + pendingComponent: AppPending, component: AppLayout, errorComponent: AppError, notFoundComponent: AppNotFound, }); +function AppPending() { + return ( +
+ +
+ ); +} + function AppLayout() { const { user } = Route.useRouteContext(); - const { hasCapability, isLoading, isError } = useCapabilities(); + const { available, capabilities } = useSuspenseQuery(capabilitiesQueryOptions(user.id)).data; const router = useRouter(); const localize = useLocalize(); const { open, setOpen } = useCommandMenu(); @@ -68,8 +91,14 @@ function AppLayout() { return () => document.removeEventListener('keydown', handleKeyDown); }, []); - if (!isLoading && !isError && !hasCapability(SystemCapabilities.ACCESS_ADMIN)) { - return ; + // Capabilities are resolved in the route loader, so this renders a decided + // state — authorized or denied — with no in-component loading flash. + if (!available || !hasImpliedCapability(capabilities, SystemCapabilities.ACCESS_ADMIN)) { + return ( +
+ +
+ ); } const matchedKey = Object.keys(ROUTE_TITLE_KEYS).find((route) => From 85dbd17282e1f0cb395512731c64243ed19d3771 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:27:17 -0700 Subject: [PATCH 2/4] test(admin): align verifyAdminTokenFn 403 test with recovery behavior The 403 branch preserves the authenticated session and returns accessDenied so the panel can offer org recovery, rather than clearing the session. Update the test that still asserted the removed clear-session outcome. --- src/server/auth.oauth.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/server/auth.oauth.test.ts b/src/server/auth.oauth.test.ts index 569f4803..a9c471ef 100644 --- a/src/server/auth.oauth.test.ts +++ b/src/server/auth.oauth.test.ts @@ -170,7 +170,7 @@ describe('verifyAdminTokenFn', () => { expect(updateSession).toHaveBeenCalledWith({ lastActivity: expect.any(Number) }); }); - it('clears a delegated admin session when backend capability revalidation is denied', async () => { + it('keeps the authenticated session and flags access denied when the current org fails admin revalidation', async () => { const user = { id: 'user-4', role: 'department-admin', email: 'delegate4@example.com' }; sessionState.data = { user, @@ -182,16 +182,18 @@ describe('verifyAdminTokenFn', () => { const result = await verifyAdminTokenFn(); - expect(result).toEqual({ valid: false, error: 'Admin privileges have been revoked' }); + // A 403 for the current org is a recoverable non-admin state, not a revoked + // session: preserve authentication so the panel can offer org recovery. + expect(result).toEqual({ valid: true, user, accessDenied: true }); expect(fetchMock).toHaveBeenCalledWith('http://librechat.test/api/admin/verify', { headers: { Authorization: 'Bearer jwt-token-4' }, }); - expect(updateSession).toHaveBeenCalledWith( - expect.objectContaining({ - token: undefined, - user: undefined, - refreshToken: undefined, - }), + expect(updateSession).toHaveBeenCalledWith({ + lastVerified: expect.any(Number), + lastActivity: expect.any(Number), + }); + expect(updateSession).not.toHaveBeenCalledWith( + expect.objectContaining({ token: undefined, user: undefined }), ); }); }); From d436d43b94d45f84de92c028853fcc32e44fac15 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:35:28 -0700 Subject: [PATCH 3/4] refactor(admin): drop artificial switch delay, remove narration comments, sort imports The switch transition already spans the mutation and refetch, so the minimum-interstitial timer is removed. Imports sorted per the repo script. --- src/components/shared/OrganizationSwitcher.tsx | 18 ++---------------- src/hooks/useCapabilities.ts | 11 +---------- src/routes/__root.tsx | 2 +- src/routes/_app.tsx | 12 +++--------- src/server/auth.oauth.test.ts | 2 -- 5 files changed, 7 insertions(+), 38 deletions(-) diff --git a/src/components/shared/OrganizationSwitcher.tsx b/src/components/shared/OrganizationSwitcher.tsx index ec3c30ec..b669ceb9 100644 --- a/src/components/shared/OrganizationSwitcher.tsx +++ b/src/components/shared/OrganizationSwitcher.tsx @@ -1,13 +1,9 @@ import { useState } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useRouter } from '@tanstack/react-router'; import { Button, Icon, Select } from '@clickhouse/click-ui'; -import { useLocalize } from '@/hooks'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { adminOrganizationsQueryOptions, switchAdminOrganizationFn } from '@/server'; - -/** Keep the switch interstitial on screen at least this long so a fast switch - * reads as a deliberate transition rather than a jarring flash. */ -const MIN_INTERSTITIAL_MS = 400; +import { useLocalize } from '@/hooks'; export function OrganizationSwitcher() { const localize = useLocalize(); @@ -19,10 +15,6 @@ export function OrganizationSwitcher() { const switchMutation = useMutation({ mutationFn: (targetOrgId: string) => switchAdminOrganizationFn({ data: { targetOrgId } }), onSuccess: async () => { - // Hold the named interstitial briefly so a fast switch does not flash, - // then refetch every query against the new session (the overlay hides the - // refetch, so no previous-org data is ever shown) and re-run the loaders. - await new Promise((resolve) => setTimeout(resolve, MIN_INTERSTITIAL_MS)); await queryClient.invalidateQueries(); await router.invalidate(); await router.navigate({ to: '/' }); @@ -41,8 +33,6 @@ export function OrganizationSwitcher() { switchMutation.mutate(organization.id); }; - // Named, full-screen transition that covers the mutation + refetch + reroute, - // so the context switch reads as intentional instead of snapping into place. if (switchingTo) { return (
); - // Exactly one administered org: a single explicit action reads better than a - // one-item dropdown, and still requires a deliberate click (no silent switch). if (organizations.length === 1) { const organization = organizations[0]; return ( @@ -79,8 +67,6 @@ export function OrganizationSwitcher() { ); } - // Multiple orgs: selecting only sets the target; the switch is a separate, - // deliberate click so a stray selection never navigates the user away. const selected = organizations.find((organization) => organization.id === selectedOrgId); return (
diff --git a/src/hooks/useCapabilities.ts b/src/hooks/useCapabilities.ts index c9dc540c..dcdc685e 100644 --- a/src/hooks/useCapabilities.ts +++ b/src/hooks/useCapabilities.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; -import { queryOptions, useQuery } from '@tanstack/react-query'; import { getRouteApi } from '@tanstack/react-router'; +import { queryOptions, useQuery } from '@tanstack/react-query'; import { getEffectiveCapabilitiesFn } from '@/server'; import { hasImpliedCapability } from '@/constants'; @@ -16,15 +16,6 @@ interface CapabilitiesData { capabilities: string[]; } -/** - * Shared query for the current admin's effective capabilities. - * - * Errors are classified into `available` (grants system reachable) plus a - * capability list rather than thrown, so the admin gate can be resolved from - * data alone. Prefetch this in a route loader with `ensureQueryData` so the - * layout never renders in a loading state; `useCapabilities` reads the same - * cache entry for per-feature checks. - */ export const capabilitiesQueryOptions = (userId: string) => queryOptions({ queryKey: ['effectiveCapabilities', userId], diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 12981822..b8a3de07 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -11,8 +11,8 @@ import { Scripts, createRootRouteWithContext, } from '@tanstack/react-router'; -import type { QueryClient } from '@tanstack/react-query'; import type { ErrorComponentProps } from '@tanstack/react-router'; +import type { QueryClient } from '@tanstack/react-query'; import { ThemeProvider, useTheme } from '../contexts/ThemeContext'; import appCss from '../styles.css?url'; import { useLocalize } from '@/hooks'; diff --git a/src/routes/_app.tsx b/src/routes/_app.tsx index 95442bc4..19a6ba61 100644 --- a/src/routes/_app.tsx +++ b/src/routes/_app.tsx @@ -4,11 +4,11 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import { createFileRoute, Outlet, useRouter, Link, redirect } from '@tanstack/react-router'; import type { ErrorComponentProps } from '@tanstack/react-router'; import { capabilitiesQueryOptions, useCommandMenu, useLocalize } from '@/hooks'; -import { CommandMenu } from '@/components/CommandMenu'; -import { AccessDenied, LoadingState } from '@/components/shared'; +import { adminOrganizationsQueryOptions, verifyAdminTokenFn } from '@/server'; import { SystemCapabilities, hasImpliedCapability } from '@/constants'; +import { AccessDenied, LoadingState } from '@/components/shared'; +import { CommandMenu } from '@/components/CommandMenu'; import { Sidebar } from '@/components/Sidebar'; -import { adminOrganizationsQueryOptions, verifyAdminTokenFn } from '@/server'; import { Header } from '@/components/Header'; const ROUTE_TITLE_KEYS: Record = { @@ -33,10 +33,6 @@ export const Route = createFileRoute('/_app')({ return { user: result.user }; }, - // Resolve the admin capability check at the route boundary so the layout - // renders a decided state (authorized or denied) and never flashes chrome. - // On the recovery (non-admin) path, also warm the admin-org list so the - // switcher is present on first paint instead of popping in. loader: async ({ context }) => { const caps = await context.queryClient.ensureQueryData( capabilitiesQueryOptions(context.user.id), @@ -91,8 +87,6 @@ function AppLayout() { return () => document.removeEventListener('keydown', handleKeyDown); }, []); - // Capabilities are resolved in the route loader, so this renders a decided - // state — authorized or denied — with no in-component loading flash. if (!available || !hasImpliedCapability(capabilities, SystemCapabilities.ACCESS_ADMIN)) { return (
diff --git a/src/server/auth.oauth.test.ts b/src/server/auth.oauth.test.ts index a9c471ef..d8f13b5c 100644 --- a/src/server/auth.oauth.test.ts +++ b/src/server/auth.oauth.test.ts @@ -182,8 +182,6 @@ describe('verifyAdminTokenFn', () => { const result = await verifyAdminTokenFn(); - // A 403 for the current org is a recoverable non-admin state, not a revoked - // session: preserve authentication so the panel can offer org recovery. expect(result).toEqual({ valid: true, user, accessDenied: true }); expect(fetchMock).toHaveBeenCalledWith('http://librechat.test/api/admin/verify', { headers: { Authorization: 'Bearer jwt-token-4' }, From 7208a01efa5a512f5104344554a6c5a8df019480 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Wed, 5 Aug 2026 07:05:05 -0700 Subject: [PATCH 4/4] fix(admin): make org recovery resilient to bot-flagged switch edge cases Keep the recovery screen reachable when the admin-org prefetch fails by letting that warm-up reject silently, so a backend hiccup no longer routes non-admin users to the error page instead of the access-denied screen. Guard the switch itself: disable the switch actions while a switch is pending, clear the interstitial if the post-switch invalidation or navigation rejects, and evaluate the interstitial before the empty-list short-circuit so it stays visible for the whole transition. --- .../shared/OrganizationSwitcher.tsx | 29 +++++++++++-------- src/routes/_app.tsx | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/shared/OrganizationSwitcher.tsx b/src/components/shared/OrganizationSwitcher.tsx index b669ceb9..d10989a4 100644 --- a/src/components/shared/OrganizationSwitcher.tsx +++ b/src/components/shared/OrganizationSwitcher.tsx @@ -15,9 +15,13 @@ export function OrganizationSwitcher() { const switchMutation = useMutation({ mutationFn: (targetOrgId: string) => switchAdminOrganizationFn({ data: { targetOrgId } }), onSuccess: async () => { - await queryClient.invalidateQueries(); - await router.invalidate(); - await router.navigate({ to: '/' }); + try { + await queryClient.invalidateQueries(); + await router.invalidate(); + await router.navigate({ to: '/' }); + } catch { + setSwitchingTo(null); + } }, onError: () => { setSwitchingTo(null); @@ -25,14 +29,6 @@ export function OrganizationSwitcher() { }, }); - const organizations = organizationsQuery.data ?? []; - if (organizations.length === 0) return null; - - const startSwitch = (organization: { id: string; name: string }) => { - setSwitchingTo(organization.name); - switchMutation.mutate(organization.id); - }; - if (switchingTo) { return (
{ + setSwitchingTo(organization.name); + switchMutation.mutate(organization.id); + }; + const error = switchMutation.isError && (

{localize('com_admin_org_switcher_error')} @@ -61,6 +65,7 @@ export function OrganizationSwitcher() { type="primary" label={localize('com_admin_org_switcher_continue', { org: organization.name })} onClick={() => startSwitch(organization)} + disabled={switchMutation.isPending} /> {error}

@@ -90,7 +95,7 @@ export function OrganizationSwitcher() { : localize('com_admin_org_switcher_switch') } onClick={() => selected && startSwitch(selected)} - disabled={!selected} + disabled={!selected || switchMutation.isPending} /> {error}
diff --git a/src/routes/_app.tsx b/src/routes/_app.tsx index 19a6ba61..9175aeaf 100644 --- a/src/routes/_app.tsx +++ b/src/routes/_app.tsx @@ -40,7 +40,7 @@ export const Route = createFileRoute('/_app')({ const hasAdmin = caps.available && hasImpliedCapability(caps.capabilities, SystemCapabilities.ACCESS_ADMIN); if (!hasAdmin) { - await context.queryClient.ensureQueryData(adminOrganizationsQueryOptions); + await context.queryClient.ensureQueryData(adminOrganizationsQueryOptions).catch(() => {}); } }, pendingComponent: AppPending,