mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🔒 fix: Robust Cache Reset on User Logout (#1324)
* refactor(Logout): rely on hooks for mutation behavior * fix: logging out now correctly resets cache, disallowing any cache mixing between the next logged in user on the same browser * chore: remove additional localStorage values on logout
This commit is contained in:
parent
583e978a82
commit
968b8ccdbd
10 changed files with 109 additions and 90 deletions
|
|
@ -7,18 +7,10 @@ const Logout = forwardRef(() => {
|
||||||
const { logout } = useAuthContext();
|
const { logout } = useAuthContext();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
localStorage.removeItem('lastConversationSetup');
|
|
||||||
localStorage.removeItem('lastSelectedTools');
|
|
||||||
localStorage.removeItem('lastAssistant');
|
|
||||||
localStorage.removeItem('autoScroll');
|
|
||||||
logout();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
|
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
|
||||||
onClick={handleLogout}
|
onClick={() => logout()}
|
||||||
>
|
>
|
||||||
<LogOutIcon />
|
<LogOutIcon />
|
||||||
{localize('com_nav_log_out')}
|
{localize('com_nav_log_out')}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import type { UseMutationResult } from '@tanstack/react-query';
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
import type {
|
import type {
|
||||||
FileUploadResponse,
|
FileUploadResponse,
|
||||||
|
|
@ -10,9 +10,13 @@ import type {
|
||||||
UpdatePresetOptions,
|
UpdatePresetOptions,
|
||||||
DeletePresetOptions,
|
DeletePresetOptions,
|
||||||
PresetDeleteResponse,
|
PresetDeleteResponse,
|
||||||
|
LogoutOptions,
|
||||||
TPreset,
|
TPreset,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
|
|
||||||
import { dataService, MutationKeys } from 'librechat-data-provider';
|
import { dataService, MutationKeys } from 'librechat-data-provider';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
export const useUploadImageMutation = (
|
export const useUploadImageMutation = (
|
||||||
options?: UploadMutationOptions,
|
options?: UploadMutationOptions,
|
||||||
|
|
@ -69,3 +73,29 @@ export const useDeletePresetMutation = (
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* login/logout */
|
||||||
|
export const useLogoutUserMutation = (
|
||||||
|
options?: LogoutOptions,
|
||||||
|
): UseMutationResult<unknown, unknown, undefined, unknown> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const setDefaultPreset = useSetRecoilState(store.defaultPreset);
|
||||||
|
return useMutation([MutationKeys.logoutUser], {
|
||||||
|
mutationFn: () => dataService.logout(),
|
||||||
|
|
||||||
|
...(options || {}),
|
||||||
|
onSuccess: (...args) => {
|
||||||
|
options?.onSuccess?.(...args);
|
||||||
|
},
|
||||||
|
onMutate: (...args) => {
|
||||||
|
setDefaultPreset(null);
|
||||||
|
queryClient.removeQueries();
|
||||||
|
localStorage.removeItem('lastConversationSetup');
|
||||||
|
localStorage.removeItem('lastSelectedModel');
|
||||||
|
localStorage.removeItem('lastSelectedTools');
|
||||||
|
localStorage.removeItem('filesToDelete');
|
||||||
|
localStorage.removeItem('lastAssistant');
|
||||||
|
options?.onMutate?.(...args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
import { UseQueryOptions, useQuery, QueryObserverResult } from '@tanstack/react-query';
|
import { UseQueryOptions, useQuery, QueryObserverResult } from '@tanstack/react-query';
|
||||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||||
|
import type { TPreset } from 'librechat-data-provider';
|
||||||
|
export const useGetPresetsQuery = (
|
||||||
|
config?: UseQueryOptions<TPreset[]>,
|
||||||
|
): QueryObserverResult<TPreset[], unknown> => {
|
||||||
|
return useQuery<TPreset[]>([QueryKeys.presets], () => dataService.getPresets(), {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetEndpointsConfigOverride = <TData = unknown | boolean>(
|
export const useGetEndpointsConfigOverride = <TData = unknown | boolean>(
|
||||||
config?: UseQueryOptions<unknown | boolean, unknown, TData>,
|
config?: UseQueryOptions<unknown | boolean, unknown, TData>,
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@ import {
|
||||||
TLoginResponse,
|
TLoginResponse,
|
||||||
setTokenHeader,
|
setTokenHeader,
|
||||||
useLoginUserMutation,
|
useLoginUserMutation,
|
||||||
useLogoutUserMutation,
|
|
||||||
useGetUserQuery,
|
useGetUserQuery,
|
||||||
useRefreshTokenMutation,
|
useRefreshTokenMutation,
|
||||||
TLoginUser,
|
TLoginUser,
|
||||||
} from 'librechat-data-provider';
|
} from 'librechat-data-provider';
|
||||||
import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common';
|
||||||
|
import { useLogoutUserMutation } from '~/data-provider';
|
||||||
import useTimeout from './useTimeout';
|
import useTimeout from './useTimeout';
|
||||||
|
|
||||||
const AuthContext = createContext<TAuthContext | undefined>(undefined);
|
const AuthContext = createContext<TAuthContext | undefined>(undefined);
|
||||||
|
|
@ -30,20 +30,11 @@ const AuthContextProvider = ({
|
||||||
authConfig?: TAuthConfig;
|
authConfig?: TAuthConfig;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [user, setUser] = useState<TUser | undefined>(undefined);
|
const [user, setUser] = useState<TUser | undefined>(undefined);
|
||||||
const [token, setToken] = useState<string | undefined>(undefined);
|
const [token, setToken] = useState<string | undefined>(undefined);
|
||||||
const [error, setError] = useState<string | undefined>(undefined);
|
const [error, setError] = useState<string | undefined>(undefined);
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const loginUser = useLoginUserMutation();
|
|
||||||
const logoutUser = useLogoutUserMutation();
|
|
||||||
const userQuery = useGetUserQuery({ enabled: !!token });
|
|
||||||
const refreshToken = useRefreshTokenMutation();
|
|
||||||
|
|
||||||
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });
|
|
||||||
|
|
||||||
const setUserContext = useCallback(
|
const setUserContext = useCallback(
|
||||||
(userContext: TUserContext) => {
|
(userContext: TUserContext) => {
|
||||||
const { token, isAuthenticated, user, redirect } = userContext;
|
const { token, isAuthenticated, user, redirect } = userContext;
|
||||||
|
|
@ -60,6 +51,32 @@ const AuthContextProvider = ({
|
||||||
},
|
},
|
||||||
[navigate],
|
[navigate],
|
||||||
);
|
);
|
||||||
|
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });
|
||||||
|
|
||||||
|
const loginUser = useLoginUserMutation();
|
||||||
|
const logoutUser = useLogoutUserMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
setUserContext({
|
||||||
|
token: undefined,
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: undefined,
|
||||||
|
redirect: '/login',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
doSetError((error as Error).message);
|
||||||
|
setUserContext({
|
||||||
|
token: undefined,
|
||||||
|
isAuthenticated: false,
|
||||||
|
user: undefined,
|
||||||
|
redirect: '/login',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const logout = useCallback(() => logoutUser.mutate(undefined), [logoutUser]);
|
||||||
|
const userQuery = useGetUserQuery({ enabled: !!token });
|
||||||
|
const refreshToken = useRefreshTokenMutation();
|
||||||
|
|
||||||
const login = (data: TLoginUser) => {
|
const login = (data: TLoginUser) => {
|
||||||
loginUser.mutate(data, {
|
loginUser.mutate(data, {
|
||||||
|
|
@ -75,28 +92,6 @@ const AuthContextProvider = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
logoutUser.mutate(undefined, {
|
|
||||||
onSuccess: () => {
|
|
||||||
setUserContext({
|
|
||||||
token: undefined,
|
|
||||||
isAuthenticated: false,
|
|
||||||
user: undefined,
|
|
||||||
redirect: '/login',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
doSetError((error as Error).message);
|
|
||||||
setUserContext({
|
|
||||||
token: undefined,
|
|
||||||
isAuthenticated: false,
|
|
||||||
user: undefined,
|
|
||||||
redirect: '/login',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [setUserContext, doSetError, logoutUser]);
|
|
||||||
|
|
||||||
const silentRefresh = useCallback(() => {
|
const silentRefresh = useCallback(() => {
|
||||||
if (authConfig?.test) {
|
if (authConfig?.test) {
|
||||||
console.log('Test mode. Skipping silent refresh.');
|
console.log('Test mode. Skipping silent refresh.');
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import {
|
import { QueryKeys, modularEndpoints, useCreatePresetMutation } from 'librechat-data-provider';
|
||||||
QueryKeys,
|
|
||||||
modularEndpoints,
|
|
||||||
useGetPresetsQuery,
|
|
||||||
useCreatePresetMutation,
|
|
||||||
} from 'librechat-data-provider';
|
|
||||||
import filenamify from 'filenamify';
|
import filenamify from 'filenamify';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import exportFromJSON from 'export-from-json';
|
import exportFromJSON from 'export-from-json';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import type { TPreset } from 'librechat-data-provider';
|
import type { TPreset } from 'librechat-data-provider';
|
||||||
import { useUpdatePresetMutation, useDeletePresetMutation } from '~/data-provider';
|
import {
|
||||||
|
useUpdatePresetMutation,
|
||||||
|
useDeletePresetMutation,
|
||||||
|
useGetPresetsQuery,
|
||||||
|
} from '~/data-provider';
|
||||||
import { useChatContext, useToastContext } from '~/Providers';
|
import { useChatContext, useToastContext } from '~/Providers';
|
||||||
import useNavigateToConvo from '~/hooks/useNavigateToConvo';
|
import useNavigateToConvo from '~/hooks/useNavigateToConvo';
|
||||||
import useDefaultConvo from '~/hooks/useDefaultConvo';
|
import useDefaultConvo from '~/hooks/useDefaultConvo';
|
||||||
|
|
@ -22,22 +21,28 @@ import store from '~/store';
|
||||||
|
|
||||||
export default function usePresets() {
|
export default function usePresets() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { user } = useAuthContext();
|
const hasLoaded = useRef(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const hasLoaded = useRef(false);
|
const { user, isAuthenticated } = useAuthContext();
|
||||||
|
|
||||||
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
|
const [_defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
|
||||||
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
|
const setPresetModalVisible = useSetRecoilState(store.presetModalVisible);
|
||||||
const { preset, conversation, newConversation, setPreset } = useChatContext();
|
const { preset, conversation, newConversation, setPreset } = useChatContext();
|
||||||
const presetsQuery = useGetPresetsQuery({ enabled: !!user });
|
const presetsQuery = useGetPresetsQuery({ enabled: !!user && isAuthenticated });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (_defaultPreset || !presetsQuery.data || hasLoaded.current) {
|
const { data: presets } = presetsQuery;
|
||||||
|
if (_defaultPreset || !presets || hasLoaded.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPreset = presetsQuery.data.find((p) => p.defaultPreset);
|
if (presets && presets.length > 0 && user && presets[0].user !== user?.id) {
|
||||||
|
presetsQuery.refetch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPreset = presets.find((p) => p.defaultPreset);
|
||||||
if (!defaultPreset) {
|
if (!defaultPreset) {
|
||||||
hasLoaded.current = true;
|
hasLoaded.current = true;
|
||||||
return;
|
return;
|
||||||
|
|
@ -49,7 +54,7 @@ export default function usePresets() {
|
||||||
hasLoaded.current = true;
|
hasLoaded.current = true;
|
||||||
// dependencies are stable and only needed once
|
// dependencies are stable and only needed once
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [presetsQuery.data]);
|
}, [presetsQuery.data, user]);
|
||||||
|
|
||||||
const setPresets = useCallback(
|
const setPresets = useCallback(
|
||||||
(presets: TPreset[]) => {
|
(presets: TPreset[]) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as f from './types/files';
|
import * as f from './types/files';
|
||||||
import * as p from './types/presets';
|
import * as m from './types/mutations';
|
||||||
import * as a from './types/assistants';
|
import * as a from './types/assistants';
|
||||||
import * as t from './types';
|
import * as t from './types';
|
||||||
import * as s from './schemas';
|
import * as s from './schemas';
|
||||||
|
|
@ -82,7 +82,7 @@ export function updatePreset(payload: s.TPreset): Promise<s.TPreset> {
|
||||||
return request.post(endpoints.presets(), payload);
|
return request.post(endpoints.presets(), payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deletePreset(arg: s.TPreset | undefined): Promise<p.PresetDeleteResponse> {
|
export function deletePreset(arg: s.TPreset | undefined): Promise<m.PresetDeleteResponse> {
|
||||||
return request.post(endpoints.deletePreset(), arg);
|
return request.post(endpoints.deletePreset(), arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './types/assistants';
|
export * from './types/assistants';
|
||||||
export * from './types/files';
|
export * from './types/files';
|
||||||
export * from './types/presets';
|
export * from './types/mutations';
|
||||||
/*
|
/*
|
||||||
* react query
|
* react query
|
||||||
* TODO: move to client, or move schemas/types to their own package
|
* TODO: move to client, or move schemas/types to their own package
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,5 @@ export enum MutationKeys {
|
||||||
fileDelete = 'fileDelete',
|
fileDelete = 'fileDelete',
|
||||||
updatePreset = 'updatePreset',
|
updatePreset = 'updatePreset',
|
||||||
deletePreset = 'deletePreset',
|
deletePreset = 'deletePreset',
|
||||||
|
logoutUser = 'logoutUser',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from '@tanstack/react-query';
|
} from '@tanstack/react-query';
|
||||||
import * as t from './types';
|
import * as t from './types';
|
||||||
import * as s from './schemas';
|
import * as s from './schemas';
|
||||||
import * as p from './types/presets';
|
import * as m from './types/mutations';
|
||||||
import * as dataService from './data-service';
|
import * as dataService from './data-service';
|
||||||
import request from './request';
|
import request from './request';
|
||||||
import { QueryKeys } from './keys';
|
import { QueryKeys } from './keys';
|
||||||
|
|
@ -306,19 +306,8 @@ export const useUpdatePresetMutation = (): UseMutationResult<
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetPresetsQuery = (
|
|
||||||
config?: UseQueryOptions<s.TPreset[]>,
|
|
||||||
): QueryObserverResult<s.TPreset[], unknown> => {
|
|
||||||
return useQuery<s.TPreset[]>([QueryKeys.presets], () => dataService.getPresets(), {
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
refetchOnMount: false,
|
|
||||||
...config,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useDeletePresetMutation = (): UseMutationResult<
|
export const useDeletePresetMutation = (): UseMutationResult<
|
||||||
p.PresetDeleteResponse,
|
m.PresetDeleteResponse,
|
||||||
unknown,
|
unknown,
|
||||||
s.TPreset | undefined,
|
s.TPreset | undefined,
|
||||||
unknown
|
unknown
|
||||||
|
|
@ -370,14 +359,8 @@ export const useLoginUserMutation = (): UseMutationResult<
|
||||||
> => {
|
> => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation((payload: t.TLoginUser) => dataService.login(payload), {
|
return useMutation((payload: t.TLoginUser) => dataService.login(payload), {
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries([QueryKeys.user]);
|
|
||||||
queryClient.invalidateQueries([QueryKeys.presets]);
|
|
||||||
queryClient.invalidateQueries([QueryKeys.conversation]);
|
|
||||||
queryClient.invalidateQueries([QueryKeys.allConversations]);
|
|
||||||
},
|
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
queryClient.invalidateQueries([QueryKeys.models]);
|
queryClient.removeQueries();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -396,15 +379,6 @@ export const useRegisterUserMutation = (): UseMutationResult<
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLogoutUserMutation = (): UseMutationResult<unknown> => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation(() => dataService.logout(), {
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries([QueryKeys.user]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRefreshTokenMutation = (): UseMutationResult<
|
export const useRefreshTokenMutation = (): UseMutationResult<
|
||||||
t.TRefreshTokenResponse,
|
t.TRefreshTokenResponse,
|
||||||
unknown,
|
unknown,
|
||||||
|
|
@ -414,7 +388,12 @@ export const useRefreshTokenMutation = (): UseMutationResult<
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation(() => request.refreshToken(), {
|
return useMutation(() => request.refreshToken(), {
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
queryClient.invalidateQueries([QueryKeys.models]);
|
queryClient.removeQueries();
|
||||||
|
localStorage.removeItem('lastConversationSetup');
|
||||||
|
localStorage.removeItem('lastSelectedModel');
|
||||||
|
localStorage.removeItem('lastSelectedTools');
|
||||||
|
localStorage.removeItem('filesToDelete');
|
||||||
|
localStorage.removeItem('lastAssistant');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,3 +20,9 @@ export type DeletePresetOptions = {
|
||||||
onMutate?: (variables: TPreset | undefined) => void | Promise<unknown>;
|
onMutate?: (variables: TPreset | undefined) => void | Promise<unknown>;
|
||||||
onError?: (error: unknown, variables: TPreset | undefined, context?: unknown) => void;
|
onError?: (error: unknown, variables: TPreset | undefined, context?: unknown) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LogoutOptions = {
|
||||||
|
onSuccess?: (data: unknown, variables: undefined, context?: unknown) => void;
|
||||||
|
onMutate?: (variables: undefined) => void | Promise<unknown>;
|
||||||
|
onError?: (error: unknown, variables: undefined, context?: unknown) => void;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue