ra-core
Version:
Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React
151 lines (137 loc) • 5.27 kB
text/typescript
import { useCallback } from 'react';
import useAuthProvider from './useAuthProvider';
import useLogout from './useLogout';
import { useNotify } from '../notification';
import { useNavigate } from 'react-router';
let timer;
/**
* Returns a callback used to call the authProvider.checkError() method
* and an error from the dataProvider. If the authProvider rejects the call,
* the hook logs the user out and shows a logged out notification.
*
* Used in the useDataProvider hook to check for access denied responses
* (e.g. 401 or 403 responses) and trigger a logout.
*
* @see useLogout
* @see useDataProvider
*
* @returns {Function} logoutIfAccessDenied callback
*
* @example
*
* import { useLogoutIfAccessDenied, useNotify, DataProviderContext } from 'react-admin';
*
* const FetchRestrictedResource = () => {
* const dataProvider = useContext(DataProviderContext);
* const logoutIfAccessDenied = useLogoutIfAccessDenied();
* const notify = useNotify()
* useEffect(() => {
* dataProvider.getOne('secret', { id: 123 })
* .catch(error => {
* logoutIfAccessDenied(error);
* notify('server error', { type: 'error' });
* })
* }, []);
* // ...
* }
*/
const useLogoutIfAccessDenied = (): LogoutIfAccessDenied => {
const authProvider = useAuthProvider();
const logout = useLogout();
const notify = useNotify();
const navigate = useNavigate();
const handleRedirect = useCallback(
(url: string) => {
if (url.startsWith('http')) {
window.location.href = url;
} else {
navigate(url);
}
},
[navigate]
);
const logoutIfAccessDenied = useCallback<LogoutIfAccessDenied>(
async (errorFromCheckAuth?: any) => {
if (!authProvider) {
return logoutIfAccessDeniedWithoutProvider();
}
try {
await authProvider.checkError(errorFromCheckAuth);
return false;
} catch (errorFromCheckError: any) {
const logoutUser = errorFromCheckError?.logoutUser ?? true;
// manual debounce
if (timer) {
return true; // side effects already triggered in this tick, exit
}
timer = setTimeout(() => {
timer = undefined;
}, 0);
const redirectTo =
errorFromCheckError &&
errorFromCheckError.redirectTo != null
? errorFromCheckError.redirectTo
: errorFromCheckAuth && errorFromCheckAuth.redirectTo
? errorFromCheckAuth.redirectTo
: undefined;
const shouldNotify = !(
(errorFromCheckError &&
errorFromCheckError.message === false) ||
(errorFromCheckAuth &&
errorFromCheckAuth.message === false) ||
redirectTo?.startsWith('http')
);
if (shouldNotify) {
try {
// notify only if not yet logged out
await authProvider.checkAuth({});
if (logoutUser) {
notify(
getErrorMessage(
errorFromCheckError,
'ra.notification.logged_out'
),
{ type: 'error' }
);
} else {
notify(
getErrorMessage(
errorFromCheckError,
'ra.notification.not_authorized'
),
{ type: 'error' }
);
}
} catch {
// ignore
}
}
if (logoutUser) {
logout({}, redirectTo);
} else if (redirectTo) {
handleRedirect(redirectTo);
}
return true;
}
},
[authProvider, logout, notify, handleRedirect]
);
return logoutIfAccessDenied;
};
const logoutIfAccessDeniedWithoutProvider = async () => false;
/**
* Call the authProvider.authError() method, using the error passed as argument.
* If the authProvider rejects the call, logs the user out and shows a logged out notification.
*
* @param {Error} error An Error object (usually returned by the dataProvider)
*
* @return {Promise} Resolved to true if there was a logout, false otherwise
*/
type LogoutIfAccessDenied = (error?: any) => Promise<boolean>;
const getErrorMessage = (error, defaultMessage) =>
typeof error === 'string'
? error
: typeof error === 'undefined' || !error.message
? defaultMessage
: error.message;
export default useLogoutIfAccessDenied;