| | import { |
| | useRef, |
| | useMemo, |
| | useState, |
| | useEffect, |
| | ReactNode, |
| | useContext, |
| | useCallback, |
| | createContext, |
| | } from 'react'; |
| | import { debounce } from 'lodash'; |
| | import { useRecoilState } from 'recoil'; |
| | import { useNavigate } from 'react-router-dom'; |
| | import { setTokenHeader, SystemRoles } from 'librechat-data-provider'; |
| | import type * as t from 'librechat-data-provider'; |
| | import { |
| | useGetRole, |
| | useGetUserQuery, |
| | useLoginUserMutation, |
| | useLogoutUserMutation, |
| | useRefreshTokenMutation, |
| | } from '~/data-provider'; |
| | import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common'; |
| | import useTimeout from './useTimeout'; |
| | import store from '~/store'; |
| |
|
| | const AuthContext = createContext<TAuthContext | undefined>(undefined); |
| |
|
| | const AuthContextProvider = ({ |
| | authConfig, |
| | children, |
| | }: { |
| | authConfig?: TAuthConfig; |
| | children: ReactNode; |
| | }) => { |
| | const [user, setUser] = useRecoilState(store.user); |
| | const [token, setToken] = useState<string | undefined>(undefined); |
| | const [error, setError] = useState<string | undefined>(undefined); |
| | const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false); |
| | const logoutRedirectRef = useRef<string | undefined>(undefined); |
| |
|
| | const { data: userRole = null } = useGetRole(SystemRoles.USER, { |
| | enabled: !!(isAuthenticated && (user?.role ?? '')), |
| | }); |
| | const { data: adminRole = null } = useGetRole(SystemRoles.ADMIN, { |
| | enabled: !!(isAuthenticated && user?.role === SystemRoles.ADMIN), |
| | }); |
| |
|
| | const navigate = useNavigate(); |
| |
|
| | const setUserContext = useMemo( |
| | () => |
| | debounce((userContext: TUserContext) => { |
| | const { token, isAuthenticated, user, redirect } = userContext; |
| | setUser(user); |
| | setToken(token); |
| | |
| | setTokenHeader(token); |
| | setIsAuthenticated(isAuthenticated); |
| |
|
| | |
| | const finalRedirect = logoutRedirectRef.current || redirect; |
| | |
| | logoutRedirectRef.current = undefined; |
| |
|
| | if (finalRedirect == null) { |
| | return; |
| | } |
| |
|
| | if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) { |
| | window.location.href = finalRedirect; |
| | } else { |
| | navigate(finalRedirect, { replace: true }); |
| | } |
| | }, 50), |
| | [navigate, setUser], |
| | ); |
| | const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) }); |
| |
|
| | const loginUser = useLoginUserMutation({ |
| | onSuccess: (data: t.TLoginResponse) => { |
| | const { user, token, twoFAPending, tempToken } = data; |
| | if (twoFAPending) { |
| | |
| | navigate(`/login/2fa?tempToken=${tempToken}`, { replace: true }); |
| | return; |
| | } |
| | setError(undefined); |
| | 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 logoutUser = useLogoutUserMutation({ |
| | onSuccess: (data) => { |
| | setUserContext({ |
| | token: undefined, |
| | isAuthenticated: false, |
| | user: undefined, |
| | redirect: data.redirect ?? '/login', |
| | }); |
| | }, |
| | onError: (error) => { |
| | doSetError((error as Error).message); |
| | setUserContext({ |
| | token: undefined, |
| | isAuthenticated: false, |
| | user: undefined, |
| | redirect: '/login', |
| | }); |
| | }, |
| | }); |
| | const refreshToken = useRefreshTokenMutation(); |
| |
|
| | const logout = useCallback( |
| | (redirect?: string) => { |
| | if (redirect) { |
| | logoutRedirectRef.current = redirect; |
| | } |
| | logoutUser.mutate(undefined); |
| | }, |
| | [logoutUser], |
| | ); |
| |
|
| | const userQuery = useGetUserQuery({ enabled: !!(token ?? '') }); |
| |
|
| | const login = (data: t.TLoginUser) => { |
| | loginUser.mutate(data); |
| | }; |
| |
|
| | const silentRefresh = useCallback(() => { |
| | if (authConfig?.test === true) { |
| | console.log('Test mode. Skipping silent refresh.'); |
| | return; |
| | } |
| | refreshToken.mutate(undefined, { |
| | onSuccess: (data: t.TRefreshTokenResponse | undefined) => { |
| | const { user, token = '' } = data ?? {}; |
| | if (token) { |
| | setUserContext({ token, isAuthenticated: true, user }); |
| | } else { |
| | console.log('Token is not present. User is not authenticated.'); |
| | if (authConfig?.test === true) { |
| | return; |
| | } |
| | navigate('/login'); |
| | } |
| | }, |
| | onError: (error) => { |
| | console.log('refreshToken mutation error:', error); |
| | if (authConfig?.test === true) { |
| | return; |
| | } |
| | navigate('/login'); |
| | }, |
| | }); |
| | }, []); |
| |
|
| | useEffect(() => { |
| | if (userQuery.data) { |
| | setUser(userQuery.data); |
| | } else if (userQuery.isError) { |
| | doSetError((userQuery.error as Error).message); |
| | navigate('/login', { replace: true }); |
| | } |
| | if (error != null && error && isAuthenticated) { |
| | doSetError(undefined); |
| | } |
| | if (token == null || !token || !isAuthenticated) { |
| | silentRefresh(); |
| | } |
| | }, [ |
| | token, |
| | isAuthenticated, |
| | userQuery.data, |
| | userQuery.isError, |
| | userQuery.error, |
| | error, |
| | setUser, |
| | navigate, |
| | silentRefresh, |
| | setUserContext, |
| | ]); |
| |
|
| | useEffect(() => { |
| | const handleTokenUpdate = (event) => { |
| | console.log('tokenUpdated event received event'); |
| | const newToken = event.detail; |
| | setUserContext({ |
| | token: newToken, |
| | isAuthenticated: true, |
| | user: user, |
| | }); |
| | }; |
| |
|
| | window.addEventListener('tokenUpdated', handleTokenUpdate); |
| |
|
| | return () => { |
| | window.removeEventListener('tokenUpdated', handleTokenUpdate); |
| | }; |
| | }, [setUserContext, user]); |
| |
|
| | |
| | const memoedValue = useMemo( |
| | () => ({ |
| | user, |
| | token, |
| | error, |
| | login, |
| | logout, |
| | setError, |
| | roles: { |
| | [SystemRoles.USER]: userRole, |
| | [SystemRoles.ADMIN]: adminRole, |
| | }, |
| | isAuthenticated, |
| | }), |
| |
|
| | [user, error, isAuthenticated, token, userRole, adminRole], |
| | ); |
| |
|
| | 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, AuthContext }; |
| |
|