diff --git a/UPGRADE.md b/UPGRADE.md index dc4e0a1a7d6..bcd89a0284c 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -965,3 +965,28 @@ const ExportButton = ({ sort, filter, maxResults = 1000, resource }) => { ); }; ``` + +## The `authProvider` no longer receives the Location pathname in AUTH_GET_PERMISSIONS + +When calling the `authProvider` for permissions (with the `AUTH_GET_PERMISSIONS` verb), react-admin used to include the pathname as second parameter. That allowed you to return different permissions based on the page. + +We believe that permissions should not vary depending on where you are in the application ; it's up to components to decide to do something or not depending on permissions. So we've removed the pathname parameter from the calls - the `authProvider` doesn't receive it anymore. + +If you want to keep location-dependent permissions logic, red the current location from the `window` object direclty in your `authProvider`: + +```diff +// in myauthProvider.js +import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_GET_PERMISSIONS } from 'react-admin'; +import decodeJwt from 'jwt-decode'; + +export default (type, params) => { + // ... + if (type === AUTH_GET_PERMISSIONS) { +- const { pathname } = params; ++ const pathname = window.location.pathname; + // pathname-dependent logic follows + // ... + } + return Promise.reject('Unknown method'); +}; +``` diff --git a/packages/ra-core/src/auth/useAuthState.ts b/packages/ra-core/src/auth/useAuthState.ts index 115113a4725..e83509b5909 100644 --- a/packages/ra-core/src/auth/useAuthState.ts +++ b/packages/ra-core/src/auth/useAuthState.ts @@ -29,7 +29,7 @@ const emptyParams = {}; * * @see useAuthenticated() * - * @param {Object} authParams Any params you want to pass to the authProvider + * @param {Object} params Any params you want to pass to the authProvider * * @returns The current auth check state. Destructure as { authenticated, error, loading, loaded }. * @@ -48,22 +48,22 @@ const emptyParams = {}; * * ); */ -const useAuthState = (authParams: any = emptyParams): State => { +const useAuthState = (params: any = emptyParams): State => { const [state, setState] = useSafeSetState({ loading: true, loaded: false, authenticated: true, // optimistic }); - const checkAuth = useCheckAuth(authParams); + const checkAuth = useCheckAuth(); useEffect(() => { - checkAuth(false) + checkAuth(params, false) .then(() => setState({ loading: false, loaded: true, authenticated: true }) ) .catch(() => setState({ loading: false, loaded: true, authenticated: false }) ); - }, [checkAuth, setState]); + }, [checkAuth, params, setState]); return state; }; diff --git a/packages/ra-core/src/auth/useAuthenticated.spec.tsx b/packages/ra-core/src/auth/useAuthenticated.spec.tsx index afabb44bb0f..d166010d8ef 100644 --- a/packages/ra-core/src/auth/useAuthenticated.spec.tsx +++ b/packages/ra-core/src/auth/useAuthenticated.spec.tsx @@ -24,9 +24,6 @@ describe('useAuthenticated', () => { ); expect(authProvider).toBeCalledTimes(1); expect(authProvider.mock.calls[0][0]).toBe('AUTH_CHECK'); - const payload = authProvider.mock.calls[0][1] as any; - expect(payload.afterLoginUrl).toBe('/'); - expect(payload.loginUrl).toBe('/login'); expect(dispatch).toHaveBeenCalledTimes(0); }); diff --git a/packages/ra-core/src/auth/useAuthenticated.tsx b/packages/ra-core/src/auth/useAuthenticated.tsx index 4f57ac6865d..dd6f781acb8 100644 --- a/packages/ra-core/src/auth/useAuthenticated.tsx +++ b/packages/ra-core/src/auth/useAuthenticated.tsx @@ -1,6 +1,8 @@ import { useEffect } from 'react'; import useCheckAuth from './useCheckAuth'; +const emptyParams = {}; + /** * Restrict access to authenticated users. * Redirect anonymous users to the login page. @@ -26,9 +28,9 @@ import useCheckAuth from './useCheckAuth'; * * ); */ -export default authParams => { - const checkAuth = useCheckAuth(authParams); +export default (params: any = emptyParams) => { + const checkAuth = useCheckAuth(); useEffect(() => { - checkAuth().catch(() => {}); - }, [checkAuth]); + checkAuth(params).catch(() => {}); + }, [checkAuth, params]); }; diff --git a/packages/ra-core/src/auth/useCheckAuth.ts b/packages/ra-core/src/auth/useCheckAuth.ts index 7263900a876..ea20f87b505 100644 --- a/packages/ra-core/src/auth/useCheckAuth.ts +++ b/packages/ra-core/src/auth/useCheckAuth.ts @@ -16,8 +16,6 @@ import useNotify from '../sideEffect/useNotify'; * @see useAuthenticated * @see useAuthState * - * @param {Object} authParams Any params you want to pass to the authProvider - * * @returns {Function} checkAuth callback * * @example @@ -33,24 +31,28 @@ import useNotify from '../sideEffect/useNotify'; * } // tip: use useAuthenticated() hook instead * * const MyPage = () => { - * const checkAuth = usecheckAuth(); + * const checkAuth = useCheckAuth(); * const [authenticated, setAuthenticated] = useState(true); // optimistic auth * useEffect(() => { - * checkAuth(false) + * checkAuth({}, false) * .then() => setAuthenticated(true)) * .catch(() => setAuthenticated(false)); * }, []); * return authenticated ? : ; * } // tip: use useAuthState() hook instead */ -const useCheckAuth = (authParams: any = defaultAuthParams): CheckAuth => { +const useCheckAuth = (): CheckAuth => { const authProvider = useAuthProvider(); const notify = useNotify(); - const logout = useLogout(authParams); + const logout = useLogout(); const checkAuth = useCallback( - (logoutOnFailure = true, redirectTo = authParams.loginUrl) => - authProvider(AUTH_CHECK, authParams).catch(error => { + ( + params: any = {}, + logoutOnFailure = true, + redirectTo = defaultAuthParams.loginUrl + ) => + authProvider(AUTH_CHECK, params).catch(error => { if (logoutOnFailure) { logout(redirectTo); notify( @@ -60,24 +62,26 @@ const useCheckAuth = (authParams: any = defaultAuthParams): CheckAuth => { } throw error; }), - [authParams, authProvider, logout, notify] + [authProvider, logout, notify] ); return authProvider ? checkAuth : checkAuthWithoutAuthProvider; }; -const checkAuthWithoutAuthProvider = (_, __) => Promise.resolve(); +const checkAuthWithoutAuthProvider = () => Promise.resolve(); /** * Check if the current user is authenticated by calling the authProvider AUTH_CHECK verb. * Logs the user out on failure. * + * @param {Object} params The parameters to pass to the authProvider * @param {boolean} logoutOnFailure Whether the user should be logged out if the authProvider fails to authenticatde them. True by default. * @param {string} redirectTo The login form url. Defaults to '/login' * * @return {Promise} Resolved to the authProvider response if the user passes the check, or rejected with an error otherwise */ type CheckAuth = ( + params?: any, logoutOnFailure?: boolean, redirectTo?: string ) => Promise; diff --git a/packages/ra-core/src/auth/useGetPermissions.ts b/packages/ra-core/src/auth/useGetPermissions.ts index 7512cd25c97..1070df66c22 100644 --- a/packages/ra-core/src/auth/useGetPermissions.ts +++ b/packages/ra-core/src/auth/useGetPermissions.ts @@ -1,15 +1,12 @@ import { useCallback } from 'react'; -import { useStore } from 'react-redux'; -import { Location } from 'history'; -import useAuthProvider, { defaultAuthParams } from './useAuthProvider'; +import useAuthProvider from './useAuthProvider'; import { AUTH_GET_PERMISSIONS } from './types'; /** * Get a callback for calling the authProvider with the AUTH_GET_PERMISSIONS verb. * * @see useAuthProvider - * @param {Object} authParams Any params you want to pass to the authProvider * * @returns {Function} getPermissions callback * @@ -37,31 +34,11 @@ import { AUTH_GET_PERMISSIONS } from './types'; * ); * } */ -const useGetPermissions = ( - authParams: any = defaultAuthParams -): GetPermissions => { +const useGetPermissions = (): GetPermissions => { const authProvider = useAuthProvider(); - /** - * We need the current location to pass to the authProvider for GET_PERMISSIONS. - * - * But if we used useSelector to read it from the store, the getPermissions function - * would be rebuilt each time the user changes location. Consequently, that - * would force a rerender of the enclosing component upon navigation. - * - * To avoid that, we don't subscribe to the store using useSelector; - * instead, we get a pointer to the store, and determine the location only - * after the getPermissions function was called. - */ - - const store = useStore(); - const getPermissions = useCallback( - (location?: Location) => - authProvider(AUTH_GET_PERMISSIONS, { - location: location || store.getState().router.location, - ...authParams, - }), - [authParams, authProvider, store] + (params: any = {}) => authProvider(AUTH_GET_PERMISSIONS, params), + [authProvider] ); return authProvider ? getPermissions : getPermissionsWithoutProvider; @@ -72,10 +49,10 @@ const getPermissionsWithoutProvider = () => Promise.resolve([]); /** * Ask the permissions to the authProvider using the AUTH_GET_PERMISSIONS verb * - * @param {Location} location the current location from history (optional) + * @param {Object} params The parameters to pass to the authProvider * * @return {Promise} The authProvider response */ -type GetPermissions = (location?: Location) => Promise; +type GetPermissions = (params?: any) => Promise; export default useGetPermissions; diff --git a/packages/ra-core/src/auth/useLogin.ts b/packages/ra-core/src/auth/useLogin.ts index b4296ae035f..d0d907eca6f 100644 --- a/packages/ra-core/src/auth/useLogin.ts +++ b/packages/ra-core/src/auth/useLogin.ts @@ -11,7 +11,6 @@ import { ReduxState } from '../types'; * and redirect to the previous authenticated page (or the home page) on success. * * @see useAuthProvider - * @param {Object} authParams Any params you want to pass to the authProvider * * @returns {Function} login callback * @@ -30,7 +29,7 @@ import { ReduxState } from '../types'; * return ; * } */ -const useLogin = (authParams: any = defaultAuthParams): Login => { +const useLogin = (): Login => { const authProvider = useAuthProvider(); const currentLocation = useSelector( (state: ReduxState) => state.router.location @@ -40,20 +39,20 @@ const useLogin = (authParams: any = defaultAuthParams): Login => { const dispatch = useDispatch(); const login = useCallback( - (params, pathName = authParams.afterLoginUrl) => - authProvider(AUTH_LOGIN, { ...params, ...authParams }).then(ret => { + (params: any = {}, pathName = defaultAuthParams.afterLoginUrl) => + authProvider(AUTH_LOGIN, params).then(ret => { dispatch(push(nextPathName || pathName)); return ret; }), - [authParams, authProvider, dispatch, nextPathName] + [authProvider, dispatch, nextPathName] ); const loginWithoutProvider = useCallback( (_, __) => { - dispatch(push(authParams.afterLoginUrl)); + dispatch(push(defaultAuthParams.afterLoginUrl)); return Promise.resolve(); }, - [authParams.afterLoginUrl, dispatch] + [dispatch] ); return authProvider ? login : loginWithoutProvider; diff --git a/packages/ra-core/src/auth/useLogout.ts b/packages/ra-core/src/auth/useLogout.ts index 1e32e9215ad..71f6cd34bf1 100644 --- a/packages/ra-core/src/auth/useLogout.ts +++ b/packages/ra-core/src/auth/useLogout.ts @@ -11,7 +11,6 @@ import { clearState } from '../actions/clearActions'; * redirect to the login page, and clear the Redux state. * * @see useAuthProvider - * @param {Object} authParams Any params you want to pass to the authProvider * * @returns {Function} logout callback * @@ -25,7 +24,7 @@ import { clearState } from '../actions/clearActions'; * return ; * } */ -const useLogout = (authParams: any = defaultAuthParams): Logout => { +const useLogout = (): Logout => { const authProvider = useAuthProvider(); /** * We need the current location to pass in the router state @@ -44,24 +43,22 @@ const useLogout = (authParams: any = defaultAuthParams): Logout => { const dispatch = useDispatch(); const logout = useCallback( - (redirectTo = authParams.loginUrl) => - authProvider(AUTH_LOGOUT, authParams).then( - redirectToFromProvider => { - dispatch(clearState()); - const currentLocation = store.getState().router.location; - dispatch( - push({ - pathname: redirectToFromProvider || redirectTo, - state: { - nextPathname: - currentLocation && currentLocation.pathname, - }, - }) - ); - return redirectToFromProvider; - } - ), - [authParams, authProvider, store, dispatch] + (params = {}, redirectTo = defaultAuthParams.loginUrl) => + authProvider(AUTH_LOGOUT, params).then(redirectToFromProvider => { + dispatch(clearState()); + const currentLocation = store.getState().router.location; + dispatch( + push({ + pathname: redirectToFromProvider || redirectTo, + state: { + nextPathname: + currentLocation && currentLocation.pathname, + }, + }) + ); + return redirectToFromProvider; + }), + [authProvider, store, dispatch] ); const logoutWithoutProvider = useCallback( @@ -69,7 +66,7 @@ const useLogout = (authParams: any = defaultAuthParams): Logout => { const currentLocation = store.getState().router.location; dispatch( push({ - pathname: authParams.loginUrl, + pathname: defaultAuthParams.loginUrl, state: { nextPathname: currentLocation && currentLocation.pathname, @@ -79,7 +76,7 @@ const useLogout = (authParams: any = defaultAuthParams): Logout => { dispatch(clearState()); return Promise.resolve(); }, - [authParams.loginUrl, store, dispatch] + [store, dispatch] ); return authProvider ? logout : logoutWithoutProvider; @@ -89,10 +86,11 @@ const useLogout = (authParams: any = defaultAuthParams): Logout => { * Log the current user out by calling the authProvider AUTH_LOGOUT verb, * and redirect them to the login screen. * + * @param {Object} params The parameters to pass to the authProvider * @param {string} redirectTo The path name to redirect the user to (optional, defaults to login) * * @return {Promise} The authProvider response */ -type Logout = (redirectTo?: string) => Promise; +type Logout = (params?: any, redirectTo?: string) => Promise; export default useLogout; diff --git a/packages/ra-core/src/auth/usePermissions.ts b/packages/ra-core/src/auth/usePermissions.ts index 14705c01f74..00253bad030 100644 --- a/packages/ra-core/src/auth/usePermissions.ts +++ b/packages/ra-core/src/auth/usePermissions.ts @@ -26,7 +26,7 @@ const emptyParams = {}; * * Useful to enable features based on user permissions * - * @param {Object} authParams Any params you want to pass to the authProvider + * @param {Object} params Any params you want to pass to the authProvider * * @returns The current auth check state. Destructure as { permissions, error, loading, loaded }. * @@ -42,14 +42,14 @@ const emptyParams = {}; * } * }; */ -const usePermissions = (authParams = emptyParams) => { +const usePermissions = (params = emptyParams) => { const [state, setState] = useSafeSetState({ loading: true, loaded: false, }); - const getPermissions = useGetPermissions(authParams); + const getPermissions = useGetPermissions(); useEffect(() => { - getPermissions() + getPermissions(params) .then(permissions => { setState({ loading: false, loaded: true, permissions }); }) @@ -60,7 +60,7 @@ const usePermissions = (authParams = emptyParams) => { error, }); }); - }, [authParams, getPermissions, setState]); + }, [getPermissions, params, setState]); return state; };