diff --git a/docs/Authentication.md b/docs/Authentication.md
index af92d28116e..377bf15f692 100644
--- a/docs/Authentication.md
+++ b/docs/Authentication.md
@@ -186,6 +186,25 @@ export default {
};
```
+It's possible to not log the user out, and to instead redirect them. You can do this by passing `error.logoutUser = false` to the `Promise.reject` along with an `error.redirectTo` url.
+
+
+```js
+// in src/authProvider.js
+export default {
+ login: ({ username, password }) => { /* ... */ },
+ checkError: (error) => {
+ const status = error.status;
+ if (status === 401 || status === 403) {
+ return Promise.reject({ redirectTo: '/unauthorized', logoutUser: false });
+ }
+ // other error code (404, 500, etc): no need to log out
+ return Promise.resolve();
+ },
+ // ...
+};
+```
+
When `authProvider.checkError()` returns a rejected Promise, react-admin displays a notification to the end user, unless the `error.message` is `false`. That means you can disable the notification on error as follows:
```js
diff --git a/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx b/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx
index 474ca9853e6..9a04b224db0 100644
--- a/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx
+++ b/packages/ra-core/src/auth/useLogoutIfAccessDenied.spec.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
import { useState, useEffect } from 'react';
import expect from 'expect';
import { render, waitFor } from '@testing-library/react';
+import { Router } from 'react-router-dom';
+import { createMemoryHistory } from 'history';
import useLogoutIfAccessDenied from './useLogoutIfAccessDenied';
import AuthContext from './AuthContext';
@@ -58,6 +60,16 @@ const notify = jest.fn();
//@ts-expect-error
useNotify.mockImplementation(() => notify);
+function renderInRouter(children) {
+ const history = createMemoryHistory();
+ const api = render({children});
+
+ return {
+ ...api,
+ history,
+ };
+}
+
describe('useLogoutIfAccessDenied', () => {
afterEach(() => {
//@ts-expect-error
@@ -67,11 +79,12 @@ describe('useLogoutIfAccessDenied', () => {
});
it('should not logout if passed no error', async () => {
- const { queryByText } = render(
+ const { queryByText } = renderInRouter(
);
+
await waitFor(() => {
expect(authProvider.logout).toHaveBeenCalledTimes(0);
expect(notify).toHaveBeenCalledTimes(0);
@@ -80,7 +93,7 @@ describe('useLogoutIfAccessDenied', () => {
});
it('should not log out if passed an error that does not make the authProvider throw', async () => {
- const { queryByText } = render(
+ const { queryByText } = renderInRouter(
@@ -93,7 +106,7 @@ describe('useLogoutIfAccessDenied', () => {
});
it('should logout if passed an error that makes the authProvider throw', async () => {
- const { queryByText } = render(
+ const { queryByText } = renderInRouter(
@@ -106,7 +119,7 @@ describe('useLogoutIfAccessDenied', () => {
});
it('should not send multiple notifications if already logged out', async () => {
- const { queryByText } = render(
+ const { queryByText } = renderInRouter(
@@ -129,7 +142,7 @@ describe('useLogoutIfAccessDenied', () => {
index++; // answers immediately first, then after 100ms the second time
}),
};
- const { queryByText } = render(
+ const { queryByText } = renderInRouter(
@@ -143,7 +156,7 @@ describe('useLogoutIfAccessDenied', () => {
});
it('should logout without showing a notification if disableAuthentication is true', async () => {
- const { queryByText } = render(
+ const { queryByText } = renderInRouter(
{
});
it('should logout without showing a notification if authProvider returns error with message false', async () => {
- const { queryByText } = render(
+ const { queryByText } = renderInRouter(
{
expect(queryByText('logged in')).toBeNull();
});
});
+
+ it('should not logout the user if logoutUser is set to false', async () => {
+ const { queryByText, history } = renderInRouter(
+ {
+ return Promise.reject({
+ logoutUser: false,
+ redirectTo: '/unauthorized',
+ });
+ },
+ }}
+ >
+
+
+ );
+ await waitFor(() => {
+ expect(authProvider.logout).toHaveBeenCalledTimes(0);
+ expect(notify).toHaveBeenCalledTimes(1);
+ expect(queryByText('logged in')).toBeNull();
+ expect(history.location.pathname).toBe('/unauthorized');
+ });
+ });
});
diff --git a/packages/ra-core/src/auth/useLogoutIfAccessDenied.ts b/packages/ra-core/src/auth/useLogoutIfAccessDenied.ts
index c1652812e97..22b84ae11ea 100644
--- a/packages/ra-core/src/auth/useLogoutIfAccessDenied.ts
+++ b/packages/ra-core/src/auth/useLogoutIfAccessDenied.ts
@@ -3,6 +3,7 @@ import { useCallback } from 'react';
import useAuthProvider from './useAuthProvider';
import useLogout from './useLogout';
import { useNotify } from '../sideEffect';
+import { useHistory } from 'react-router';
let timer;
@@ -41,12 +42,15 @@ const useLogoutIfAccessDenied = (): LogoutIfAccessDenied => {
const authProvider = useAuthProvider();
const logout = useLogout();
const notify = useNotify();
+ const history = useHistory();
const logoutIfAccessDenied = useCallback(
(error?: any, disableNotification?: boolean) =>
authProvider
.checkError(error)
.then(() => false)
.catch(async e => {
+ const logoutUser = e?.logoutUser ?? true;
+
//manual debounce
if (timer) {
// side effects already triggered in this tick, exit
@@ -66,7 +70,17 @@ const useLogoutIfAccessDenied = (): LogoutIfAccessDenied => {
authProvider
.checkAuth({})
.then(() => {
- notify('ra.notification.logged_out', 'warning');
+ if (logoutUser) {
+ notify(
+ 'ra.notification.logged_out',
+ 'warning'
+ );
+ } else {
+ notify(
+ 'ra.notification.not_authorized',
+ 'warning'
+ );
+ }
})
.catch(() => {});
}
@@ -76,11 +90,16 @@ const useLogoutIfAccessDenied = (): LogoutIfAccessDenied => {
: error && error.redirectTo
? error.redirectTo
: undefined;
- logout({}, redirectTo);
+
+ if (logoutUser) {
+ logout({}, redirectTo);
+ } else {
+ history.push(redirectTo);
+ }
return true;
}),
- [authProvider, logout, notify]
+ [authProvider, logout, notify, history]
);
return authProvider
? logoutIfAccessDenied
diff --git a/packages/ra-language-english/src/index.ts b/packages/ra-language-english/src/index.ts
index ed4d50a3dc1..62b3003e4c1 100644
--- a/packages/ra-language-english/src/index.ts
+++ b/packages/ra-language-english/src/index.ts
@@ -138,6 +138,7 @@ const englishMessages: TranslationMessages = {
'Cannot load the translations for the specified language',
canceled: 'Action cancelled',
logged_out: 'Your session has ended, please reconnect.',
+ not_authorized: "You're not authorized to access this resource.",
},
validation: {
required: 'Required',
diff --git a/packages/ra-language-french/src/index.ts b/packages/ra-language-french/src/index.ts
index b01ed2cd9f5..ec1c8a15c87 100644
--- a/packages/ra-language-french/src/index.ts
+++ b/packages/ra-language-french/src/index.ts
@@ -143,6 +143,8 @@ const frenchMessages: TranslationMessages = {
'Erreur de chargement des traductions pour la langue sélectionnée',
canceled: 'Action annulée',
logged_out: 'Votre session a pris fin, veuillez vous reconnecter.',
+ not_authorized:
+ "Vous n'êtes pas autorisé(e) à accéder à cette ressource.",
},
validation: {
required: 'Ce champ est requis',