🔒 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:
Danny Avila 2023-12-10 17:13:42 -05:00 committed by GitHub
parent 583e978a82
commit 968b8ccdbd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 109 additions and 90 deletions

View file

@ -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')}

View file

@ -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);
},
});
};

View file

@ -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>,

View file

@ -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,23 +51,10 @@ const AuthContextProvider = ({
}, },
[navigate], [navigate],
); );
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });
const login = (data: TLoginUser) => { const loginUser = useLoginUserMutation();
loginUser.mutate(data, { const logoutUser = useLogoutUserMutation({
onSuccess: (data: TLoginResponse) => {
const { user, token } = data;
setUserContext({ token, isAuthenticated: true, user, redirect: '/c/new' });
},
onError: (error: TResError | unknown) => {
const resError = error as TResError;
doSetError(resError.message);
navigate('/login', { replace: true });
},
});
};
const logout = useCallback(() => {
logoutUser.mutate(undefined, {
onSuccess: () => { onSuccess: () => {
setUserContext({ setUserContext({
token: undefined, token: undefined,
@ -95,7 +73,24 @@ const AuthContextProvider = ({
}); });
}, },
}); });
}, [setUserContext, doSetError, logoutUser]);
const logout = useCallback(() => logoutUser.mutate(undefined), [logoutUser]);
const userQuery = useGetUserQuery({ enabled: !!token });
const refreshToken = useRefreshTokenMutation();
const login = (data: TLoginUser) => {
loginUser.mutate(data, {
onSuccess: (data: TLoginResponse) => {
const { user, token } = data;
setUserContext({ token, isAuthenticated: true, user, redirect: '/c/new' });
},
onError: (error: TResError | unknown) => {
const resError = error as TResError;
doSetError(resError.message);
navigate('/login', { replace: true });
},
});
};
const silentRefresh = useCallback(() => { const silentRefresh = useCallback(() => {
if (authConfig?.test) { if (authConfig?.test) {

View file

@ -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[]) => {

View file

@ -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);
} }

View file

@ -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

View file

@ -23,4 +23,5 @@ export enum MutationKeys {
fileDelete = 'fileDelete', fileDelete = 'fileDelete',
updatePreset = 'updatePreset', updatePreset = 'updatePreset',
deletePreset = 'deletePreset', deletePreset = 'deletePreset',
logoutUser = 'logoutUser',
} }

View file

@ -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');
}, },
}); });
}; };

View file

@ -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;
};