diff --git a/packages/ra-core/src/auth/useAuthState.spec.tsx b/packages/ra-core/src/auth/useAuthState.spec.tsx index 1f534cc4a83..4470881de6b 100644 --- a/packages/ra-core/src/auth/useAuthState.spec.tsx +++ b/packages/ra-core/src/auth/useAuthState.spec.tsx @@ -5,23 +5,21 @@ import { CoreAdminContext } from '../core/CoreAdminContext'; import useAuthState from './useAuthState'; -const UseAuth = ({ children, authParams }: any) => { - const res = useAuthState(authParams); - return children(res); +const UseAuth = (authParams: any) => { + const state = useAuthState(authParams); + return ( +
+ {state.isLoading && 'LOADING'} + {state.authenticated && 'AUTHENTICATED'} +
+ ); }; -const stateInpector = state => ( -
- {state.isLoading && 'LOADING'} - {state.authenticated && 'AUTHENTICATED'} -
-); - describe('useAuthState', () => { it('should return a loading state on mount', () => { render( - {stateInpector} + ); expect(screen.queryByText('LOADING')).not.toBeNull(); @@ -31,7 +29,7 @@ describe('useAuthState', () => { it('should return authenticated by default after a tick', async () => { render( - {stateInpector} + ); await waitFor(() => { @@ -50,9 +48,7 @@ describe('useAuthState', () => { }; render( - - {stateInpector} - + ); await waitFor(() => { diff --git a/packages/ra-core/src/auth/useAuthState.ts b/packages/ra-core/src/auth/useAuthState.ts index 635d6af4be4..3848eae1ccd 100644 --- a/packages/ra-core/src/auth/useAuthState.ts +++ b/packages/ra-core/src/auth/useAuthState.ts @@ -1,7 +1,9 @@ -import { useEffect } from 'react'; - -import { useCheckAuth } from './useCheckAuth'; -import { useSafeSetState } from '../util/hooks'; +import { useMemo } from 'react'; +import { useQuery, UseQueryOptions } from 'react-query'; +import useAuthProvider, { defaultAuthParams } from './useAuthProvider'; +import useLogout from './useLogout'; +import { removeDoubleSlashes, useBasename } from '../routing'; +import { useNotify } from '../notification'; interface State { isLoading: boolean; @@ -49,19 +51,62 @@ const emptyParams = {}; */ const useAuthState = ( params: any = emptyParams, - logoutOnFailure: boolean = false + logoutOnFailure: boolean = false, + queryOptions?: UseQueryOptions ): State => { - const [state, setState] = useSafeSetState({ - isLoading: true, - authenticated: true, // optimistic - }); - const checkAuth = useCheckAuth(); - useEffect(() => { - checkAuth(params, logoutOnFailure) - .then(() => setState({ isLoading: false, authenticated: true })) - .catch(() => setState({ isLoading: false, authenticated: false })); - }, [checkAuth, params, logoutOnFailure, setState]); - return state; + const authProvider = useAuthProvider(); + const logout = useLogout(); + const basename = useBasename(); + const notify = useNotify(); + + const result = useQuery( + ['auth', 'checkAuth', params], + () => { + // The authProvider is optional in react-admin + return authProvider?.checkAuth(params).then(() => true); + }, + { + onError: error => { + const loginUrl = removeDoubleSlashes( + `${basename}/${defaultAuthParams.loginUrl}` + ); + if (logoutOnFailure) { + logout( + {}, + error && error.redirectTo != null + ? error.redirectTo + : loginUrl + ); + const shouldSkipNotify = error && error.message === false; + !shouldSkipNotify && + notify( + getErrorMessage(error, 'ra.auth.auth_check_error'), + { type: 'warning' } + ); + } + }, + retry: false, + ...queryOptions, + } + ); + + return useMemo(() => { + return { + // If the data is undefined and the query isn't loading anymore, it means the query failed. + // In that case, we set authenticated to false unless there's no authProvider. + authenticated: + result.data ?? result.isLoading ? true : authProvider == null, // Optimisic + isLoading: result.isLoading, + error: result.error, + }; + }, [authProvider, result]); }; export default useAuthState; + +const getErrorMessage = (error, defaultMessage) => + typeof error === 'string' + ? error + : typeof error === 'undefined' || !error.message + ? defaultMessage + : error.message; diff --git a/packages/ra-core/src/auth/useAuthenticated.spec.tsx b/packages/ra-core/src/auth/useAuthenticated.spec.tsx index f44b2fb5c61..5c0c7f7272c 100644 --- a/packages/ra-core/src/auth/useAuthenticated.spec.tsx +++ b/packages/ra-core/src/auth/useAuthenticated.spec.tsx @@ -5,9 +5,14 @@ import { createMemoryHistory } from 'history'; import { Routes, Route, useLocation } from 'react-router-dom'; import { memoryStore } from '../store'; -import { Authenticated } from './Authenticated'; import { useNotificationContext } from '../notification'; import { CoreAdminContext } from '../core'; +import { useAuthenticated } from '.'; + +const Authenticated = ({ children, ...params }) => { + useAuthenticated({ params }); + return children; +}; describe('useAuthenticated', () => { const Foo = () =>
Foo
; @@ -27,6 +32,12 @@ describe('useAuthenticated', () => { + + + + + + ); expect(authProvider.checkAuth).toBeCalledTimes(1); @@ -53,7 +64,7 @@ describe('useAuthenticated', () => { ); const { rerender } = render(); - rerender(); + rerender(); expect(authProvider.checkAuth).toBeCalledTimes(2); expect(authProvider.checkAuth.mock.calls[1][0]).toEqual({ foo: 'bar' }); expect(reset).toHaveBeenCalledTimes(0); @@ -132,19 +143,21 @@ describe('useAuthenticated', () => { ); await waitFor(() => { - expect(authProvider.checkAuth.mock.calls[0][0]).toEqual({}); - expect(authProvider.logout.mock.calls[0][0]).toEqual({}); - expect(reset).toHaveBeenCalledTimes(1); - expect(notificationsSpy).toEqual([ - { - message: 'ra.auth.auth_check_error', - type: 'warning', - notificationOptions: {}, - }, - ]); - expect(screen.getByLabelText('nextPathname').innerHTML).toEqual( - '/' - ); + expect(authProvider.checkAuth).toHaveBeenCalledTimes(1); + }); + expect(authProvider.checkAuth.mock.calls[0][0]).toEqual({}); + await waitFor(() => { + expect(authProvider.logout).toHaveBeenCalledTimes(1); }); + expect(authProvider.logout.mock.calls[0][0]).toEqual({}); + expect(reset).toHaveBeenCalledTimes(1); + expect(notificationsSpy).toEqual([ + { + message: 'ra.auth.auth_check_error', + type: 'warning', + notificationOptions: {}, + }, + ]); + expect(screen.getByLabelText('nextPathname').innerHTML).toEqual('/'); }); }); diff --git a/packages/ra-core/src/auth/useAuthenticated.ts b/packages/ra-core/src/auth/useAuthenticated.ts index 83a3e1d573c..b7554ccdbb2 100644 --- a/packages/ra-core/src/auth/useAuthenticated.ts +++ b/packages/ra-core/src/auth/useAuthenticated.ts @@ -1,5 +1,5 @@ -import { useEffect } from 'react'; -import { useCheckAuth } from './useCheckAuth'; +import { UseQueryOptions } from 'react-query'; +import useAuthState from './useAuthState'; /** * Restrict access to authenticated users. @@ -26,20 +26,17 @@ import { useCheckAuth } from './useCheckAuth'; * * ); */ -export const useAuthenticated = ( - options: UseAuthenticatedOptions = {} -) => { - const { enabled = true, params = emptyParams } = options; - const checkAuth = useCheckAuth(); - useEffect(() => { - if (enabled) { - checkAuth(params).catch(() => {}); - } - }, [checkAuth, enabled, params]); +export const useAuthenticated = ({ + params, + ...options +}: UseAuthenticatedOptions = {}) => { + useAuthState(params ?? emptyParams, true, options); }; -export type UseAuthenticatedOptions = { - enabled?: boolean; +export type UseAuthenticatedOptions = UseQueryOptions< + boolean, + any +> & { params?: ParamsType; };