mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-02 22:07:19 +02:00
🔑 fix: Auth-Aware Startup Config Caching for Fresh Sessions (#12505)
* fix: auth-aware config caching for fresh sessions - Add auth state to startup config query key via shared `startupConfigKey` builder so login (unauthenticated) and chat (authenticated) configs are cached independently - Disable queries during login onMutate to prevent premature unauthenticated refetches after cache clear - Re-enable queries in setUserContext only after setTokenHeader runs, with positive-only guard to avoid redundant disable on logout - Update all getQueryData call sites to use the shared key builder - Fall back to getConfigDefaults().interface in useEndpoints, hoisted to module-level constant to avoid per-render recomputation * fix: address review findings for auth-aware config caching - Move defaultInterface const after all imports in ModelSelector.tsx - Remove dead QueryKeys import, use import type for TStartupConfig in ImportConversations.tsx - Spread real exports in useQueryParams.spec.ts mock to preserve startupConfigKey, fixing TypeError in all 6 tests * chore: import order * fix: re-enable queries on login failure When login fails, onSuccess never fires so queriesEnabled stays false. Re-enable in onError so the login page can re-fetch config (needed for LDAP username validation and social login options).
This commit is contained in:
parent
c4b5dedb77
commit
7b368916d5
11 changed files with 59 additions and 30 deletions
|
|
@ -15,6 +15,8 @@ import { CustomMenu as Menu } from './CustomMenu';
|
|||
import DialogManager from './DialogManager';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
function ModelSelectorContent() {
|
||||
const localize = useLocalize();
|
||||
|
||||
|
|
@ -122,7 +124,7 @@ function ModelSelectorContent() {
|
|||
}
|
||||
|
||||
export default function ModelSelector({ startupConfig }: ModelSelectorProps) {
|
||||
const interfaceConfig = startupConfig?.interface ?? getConfigDefaults().interface;
|
||||
const interfaceConfig = startupConfig?.interface ?? defaultInterface;
|
||||
const modelSpecs = startupConfig?.modelSpecs?.list ?? [];
|
||||
|
||||
// Hide the selector when modelSelect is false and there are no model specs to show
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useState, useRef, useCallback } from 'react';
|
||||
import { Import } from 'lucide-react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryKeys, TStartupConfig } from 'librechat-data-provider';
|
||||
import type { TStartupConfig } from 'librechat-data-provider';
|
||||
import { Spinner, useToastContext, Label, Button } from '@librechat/client';
|
||||
import { useUploadConversationsMutation } from '~/data-provider';
|
||||
import { startupConfigKey, useUploadConversationsMutation } from '~/data-provider';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn, logger } from '~/utils';
|
||||
|
|
@ -51,7 +51,7 @@ function ImportConversations() {
|
|||
const handleFileUpload = useCallback(
|
||||
async (file: File) => {
|
||||
try {
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>(startupConfigKey(true));
|
||||
const maxFileSize = startupConfig?.conversationImportMaxFileSize;
|
||||
if (maxFileSize && file.size > maxFileSize) {
|
||||
const size = (maxFileSize / (1024 * 1024)).toFixed(2);
|
||||
|
|
|
|||
|
|
@ -40,15 +40,20 @@ export const useLoginUserMutation = (
|
|||
mutationFn: (payload: t.TLoginUser) => dataService.login(payload),
|
||||
...(options || {}),
|
||||
onMutate: (vars) => {
|
||||
setQueriesEnabled(false);
|
||||
resetDefaultPreset();
|
||||
clearStates();
|
||||
queryClient.removeQueries();
|
||||
options?.onMutate?.(vars);
|
||||
},
|
||||
// Queries re-enabled in setUserContext (AuthContext) after setTokenHeader runs
|
||||
onSuccess: (...args) => {
|
||||
setQueriesEnabled(true);
|
||||
options?.onSuccess?.(...args);
|
||||
},
|
||||
onError: (...args) => {
|
||||
setQueriesEnabled(true);
|
||||
options?.onError?.(...args);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,21 @@ export const useGetEndpointsQuery = <TData = t.TEndpointsConfig>(
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Auth-aware query key so unauthenticated (login page) and authenticated
|
||||
* (chat page) configs are cached independently, preventing stale
|
||||
* unauthenticated config from persisting after login.
|
||||
*/
|
||||
export const startupConfigKey = (isAuthenticated: boolean) =>
|
||||
[QueryKeys.startupConfig, isAuthenticated] as const;
|
||||
|
||||
export const useGetStartupConfig = (
|
||||
config?: UseQueryOptions<t.TStartupConfig>,
|
||||
): QueryObserverResult<t.TStartupConfig> => {
|
||||
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
|
||||
const user = useRecoilValue<t.TUser | undefined>(store.user);
|
||||
return useQuery<t.TStartupConfig>(
|
||||
[QueryKeys.startupConfig],
|
||||
startupConfigKey(!!user),
|
||||
() => dataService.getStartupConfig(),
|
||||
{
|
||||
staleTime: Infinity,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
createContext,
|
||||
} from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
apiBaseUrl,
|
||||
|
|
@ -45,6 +45,7 @@ const AuthContextProvider = ({
|
|||
const [token, setToken] = useState<string | undefined>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const setQueriesEnabled = useSetRecoilState<boolean>(store.queriesEnabled);
|
||||
|
||||
const { data: userRole = null } = useGetRole(SystemRoles.USER, {
|
||||
enabled: !!(isAuthenticated && (user?.role ?? '')),
|
||||
|
|
@ -63,6 +64,9 @@ const AuthContextProvider = ({
|
|||
setToken(token);
|
||||
setTokenHeader(token);
|
||||
setIsAuthenticated(isAuthenticated);
|
||||
if (isAuthenticated) {
|
||||
setQueriesEnabled(true);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const postLoginRedirect = getPostLoginRedirect(searchParams);
|
||||
|
|
@ -81,7 +85,7 @@ const AuthContextProvider = ({
|
|||
|
||||
navigate(finalRedirect, { replace: true });
|
||||
}, 50),
|
||||
[navigate, setUser],
|
||||
[navigate, setUser, setQueriesEnabled],
|
||||
);
|
||||
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete';
|
|||
import useGetSender from '~/hooks/Conversations/useGetSender';
|
||||
import { logger, createDualMessageContent } from '~/utils';
|
||||
import store, { useGetEphemeralAgent } from '~/store';
|
||||
import { startupConfigKey } from '~/data-provider';
|
||||
import useUserKey from '~/hooks/Input/useUserKey';
|
||||
import { useAuthContext } from '~/hooks';
|
||||
|
||||
|
|
@ -172,7 +173,7 @@ export default function useChatFunctions({
|
|||
}
|
||||
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>(startupConfigKey(true));
|
||||
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
||||
const iconURL = conversation?.iconURL;
|
||||
const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, endpoint);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
logger,
|
||||
} from '~/utils';
|
||||
import { useApplyModelSpecEffects } from '~/hooks/Agents';
|
||||
import { startupConfigKey } from '~/data-provider';
|
||||
import store from '~/store';
|
||||
|
||||
const useNavigateToConvo = (index = 0) => {
|
||||
|
|
@ -41,7 +42,7 @@ const useNavigateToConvo = (index = 0) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>(startupConfigKey(true));
|
||||
applyModelSpecEffects({
|
||||
startupConfig,
|
||||
specName: conversation?.spec,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
EModelEndpoint,
|
||||
PermissionTypes,
|
||||
getEndpointField,
|
||||
getConfigDefaults,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
TEndpointsConfig,
|
||||
|
|
@ -20,6 +21,8 @@ import { mapEndpoints, getIconKey } from '~/utils';
|
|||
import { useHasAccess } from '~/hooks';
|
||||
import { icons } from './Icons';
|
||||
|
||||
const defaultInterface = getConfigDefaults().interface;
|
||||
|
||||
export const useEndpoints = ({
|
||||
agents,
|
||||
assistantsMap,
|
||||
|
|
@ -33,7 +36,7 @@ export const useEndpoints = ({
|
|||
}) => {
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const { data: endpoints = [] } = useGetEndpointsQuery({ select: mapEndpoints });
|
||||
const interfaceConfig = startupConfig?.interface ?? {};
|
||||
const interfaceConfig = startupConfig?.interface ?? defaultInterface;
|
||||
const includedEndpoints = useMemo(
|
||||
() => new Set(startupConfig?.modelSpecs?.addedEndpoints ?? []),
|
||||
[startupConfig?.modelSpecs?.addedEndpoints],
|
||||
|
|
|
|||
|
|
@ -93,19 +93,23 @@ jest.mock('librechat-data-provider', () => {
|
|||
};
|
||||
});
|
||||
|
||||
// Mock data-provider hooks
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useGetAgentByIdQuery: jest.fn(() => ({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
useListAgentsQuery: jest.fn(() => ({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
}));
|
||||
// Mock data-provider hooks while preserving real exports like startupConfigKey
|
||||
jest.mock('~/data-provider', () => {
|
||||
const actual = jest.requireActual<typeof import('~/data-provider')>('~/data-provider');
|
||||
return {
|
||||
...actual,
|
||||
useGetAgentByIdQuery: jest.fn(() => ({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
useListAgentsQuery: jest.fn(() => ({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock global window.history
|
||||
global.window = Object.create(window);
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import {
|
|||
logger,
|
||||
} from '~/utils';
|
||||
import { useAuthContext, useAgentsMap, useDefaultConvo, useSubmitMessage } from '~/hooks';
|
||||
import { startupConfigKey, useGetAgentByIdQuery } from '~/data-provider';
|
||||
import { useChatContext, useChatFormContext } from '~/Providers';
|
||||
import { useGetAgentByIdQuery } from '~/data-provider';
|
||||
import store from '~/store';
|
||||
|
||||
const injectAgentIntoAgentsMap = (queryClient: QueryClient, agent: any) => {
|
||||
|
|
@ -84,7 +84,7 @@ export default function useQueryParams({
|
|||
}
|
||||
let newPreset = removeUnavailableTools(_newPreset, availableTools);
|
||||
if (newPreset.spec != null && newPreset.spec !== '') {
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>(startupConfigKey(true));
|
||||
const modelSpecs = startupConfig?.modelSpecs?.list ?? [];
|
||||
const spec = modelSpecs.find((s) => s.name === newPreset.spec);
|
||||
if (!spec) {
|
||||
|
|
@ -258,7 +258,7 @@ export default function useQueryParams({
|
|||
if (!textAreaRef.current) {
|
||||
return;
|
||||
}
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]);
|
||||
const startupConfig = queryClient.getQueryData<TStartupConfig>(startupConfigKey(true));
|
||||
if (!startupConfig) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import {
|
|||
removeConvoFromAllQueries,
|
||||
findConversationInInfinite,
|
||||
} from '~/utils';
|
||||
import { queueTitleGeneration } from '~/data-provider/SSE/queries';
|
||||
import { startupConfigKey, queueTitleGeneration } from '~/data-provider';
|
||||
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
||||
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
||||
import useStepHandler from '~/hooks/SSE/useStepHandler';
|
||||
|
|
@ -406,7 +406,7 @@ export default function useEventHandlers({
|
|||
sourceId: submission.conversation?.conversationId,
|
||||
ephemeralAgent: submission.ephemeralAgent,
|
||||
specName: submission.conversation?.spec,
|
||||
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
|
||||
startupConfig: queryClient.getQueryData<TStartupConfig>(startupConfigKey(true)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -588,7 +588,7 @@ export default function useEventHandlers({
|
|||
sourceId: submissionConvo.conversationId,
|
||||
ephemeralAgent: submission.ephemeralAgent,
|
||||
specName: submission.conversation?.spec,
|
||||
startupConfig: queryClient.getQueryData<TStartupConfig>([QueryKeys.startupConfig]),
|
||||
startupConfig: queryClient.getQueryData<TStartupConfig>(startupConfigKey(true)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue