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(undefined); const AuthContextProvider = ({ authConfig, children, }: { authConfig: TAuthConfig; children: ReactNode; }) => { const [user, setUser] = useState(undefined); const [token, setToken] = useState(undefined); const [error, setError] = useState(undefined); const [isAuthenticated, setIsAuthenticated] = useState(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, { replace: true }); } }, [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 as Error).message); navigate('/login', { replace: true }); }, }); }; 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 as Error).message); }, }); }; useEffect(() => { if (userQuery.data) { setUser(userQuery.data); } else if (userQuery.isError) { doSetError((userQuery?.error as Error).message); navigate('/login', { replace: true }); } 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', { replace: true }); } } }, [ 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 {children}; }; const useAuthContext = () => { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuthContext should be used inside AuthProvider'); } return context; }; export { AuthContextProvider, useAuthContext };