LibreChat/client/src/hooks/AuthContext.tsx
Dan Orlando 04e4259005
Move data provider to shared package (#582)
* create data-provider package and move code from data-provider folder to be shared between apps

* fix type issues

* add packages to ignore

* add new data-provider package to apps

* refactor: change client imports to use @librechat/data-provider package

* include data-provider build script in frontend build

* fix type issue after rebasing

* delete admin/package.json from this branch

* update test ci script to include building of data-provider package

* Try using regular build for test action

* Switch frontend-review back to build:ci

* Remove loginRedirect from Login.tsx

* Add ChatGPT back to EModelEndpoint
2023-07-04 15:47:41 -04:00

205 lines
5.1 KiB
TypeScript

import {
useState,
useEffect,
useMemo,
ReactNode,
useCallback,
createContext,
useContext
} from 'react';
import {
TUser,
TLoginResponse,
setTokenHeader,
useLoginUserMutation,
useLogoutUserMutation,
useGetUserQuery,
useRefreshTokenMutation,
TLoginUser
} from '@librechat/data-provider';
import { useNavigate } from 'react-router-dom';
export type TAuthContext = {
user: TUser | undefined;
token: string | undefined;
isAuthenticated: boolean;
error: string | undefined;
login: (data: TLoginUser) => void;
logout: () => void;
};
export type TUserContext = {
user?: TUser | undefined;
token: string | undefined;
isAuthenticated: boolean;
redirect?: string;
};
export type TAuthConfig = {
loginRedirect: string;
};
//@ts-ignore - index expression is not of type number
window['errorTimeout'] = undefined;
const AuthContext = createContext<TAuthContext | undefined>(undefined);
const AuthContextProvider = ({
authConfig,
children
}: {
authConfig: TAuthConfig;
children: ReactNode;
}) => {
const [user, setUser] = useState<TUser | undefined>(undefined);
const [token, setToken] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const navigate = useNavigate();
const loginUser = useLoginUserMutation();
const logoutUser = useLogoutUserMutation();
const userQuery = useGetUserQuery({ enabled: !!token });
const refreshToken = useRefreshTokenMutation();
// This seems to prevent the error flashing issue
const doSetError = (error: string | undefined) => {
if (error) {
console.log(error);
// set timeout to ensure we don't get a flash of the error message
window['errorTimeout'] = setTimeout(() => {
setError(error);
}, 400);
}
};
const setUserContext = useCallback(
(userContext: TUserContext) => {
const { token, isAuthenticated, user, redirect } = userContext;
if (user) {
setUser(user);
}
setToken(token);
//@ts-ignore - ok for token to be undefined initially
setTokenHeader(token);
setIsAuthenticated(isAuthenticated);
if (redirect) {
navigate(redirect);
}
},
[navigate]
);
const getCookieValue = (key: string) => {
let keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
};
const login = (data: TLoginUser) => {
loginUser.mutate(data, {
onSuccess: (data: TLoginResponse) => {
const { user, token } = data;
setUserContext({ token, isAuthenticated: true, user, redirect: '/chat/new' });
},
onError: (error) => {
doSetError(error.message);
}
});
};
const logout = () => {
document.cookie.split(';').forEach((c) => {
document.cookie = c
.replace(/^ +/, '')
.replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');
});
logoutUser.mutate(undefined, {
onSuccess: () => {
setUserContext({
token: undefined,
isAuthenticated: false,
user: undefined,
redirect: '/login'
});
},
onError: (error) => {
doSetError(error.message);
}
});
};
useEffect(() => {
if (userQuery.data) {
setUser(userQuery.data);
} else if (userQuery.isError) {
//@ts-ignore - userQuery.error is of type unknown
doSetError(userQuery?.error.message);
navigate('/login');
}
if (error && isAuthenticated) {
doSetError(undefined);
}
if (!token || !isAuthenticated) {
const tokenFromCookie = getCookieValue('token');
if (tokenFromCookie) {
setUserContext({ token: tokenFromCookie, isAuthenticated: true, user: userQuery.data });
} else {
navigate('/login');
}
}
}, [
token,
isAuthenticated,
userQuery.data,
userQuery.isError,
userQuery.error,
error,
navigate,
setUserContext
]);
// const silentRefresh = useCallback(() => {
// refreshToken.mutate(undefined, {
// onSuccess: (data: TLoginResponse) => {
// const { user, token } = data;
// setUserContext({ token, isAuthenticated: true, user });
// },
// onError: error => {
// setError(error.message);
// }
// });
//
// }, [setUserContext]);
// useEffect(() => {
// if (token)
// silentRefresh();
// }, [token, silentRefresh]);
// Make the provider update only when it should
const memoedValue = useMemo(
() => ({
user,
token,
isAuthenticated,
error,
login,
logout
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[user, error, isAuthenticated, token]
);
return <AuthContext.Provider value={memoedValue}>{children}</AuthContext.Provider>;
};
const useAuthContext = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuthContext should be used inside AuthProvider');
}
return context;
};
export { AuthContextProvider, useAuthContext };