Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom login redirection #7999

Merged
merged 1 commit into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion docs/AuthProviderWriting.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,32 @@ Once the promise resolves, the login form redirects to the previous page, or to

**Tip**: It's a good idea to store credentials in `localStorage`, as in this example, to avoid reconnection when opening a new browser tab. But this makes your application [open to XSS attacks](https://www.redotheweb.com/2015/11/09/api-security.html), so you'd better double down on security, and add an `httpOnly` cookie on the server side, too.

After login, react-admin redirects the user to the location returned by `authProvider.login()` - or to the previous page if the method returns nothing. You can customize the redirection url by returning an object with a `redirectTo` key containing a string or false to disable redirection after login.

```js
// in src/authProvider.js
const authProvider = {
login: ({ username, password }) => {
const request = new Request('https://mydomain.com/authenticate', {
method: 'POST',
body: JSON.stringify({ username, password }),
headers: new Headers({ 'Content-Type': 'application/json' }),
});
return fetch(request)
.then(response => {
// ...
return { redirectTo: false };
})
.catch(() => {
throw new Error('Network error')
});
},
checkAuth: () => { /* ... */ },
getPermissions: () => { /* ... */ },
// ...
};

```
If the login fails, `authProvider.login()` should return a rejected Promise with an Error object. React-admin displays the Error message to the user in a notification.

### `checkError`
Expand Down Expand Up @@ -388,7 +414,7 @@ React-admin calls the `authProvider` methods with the following params:

| Method | Resolve if | Response format |
| ---------------- | --------------------------------- | --------------- |
| `login` | Login credentials were accepted | `void` |
| `login` | Login credentials were accepted | `void | { redirectTo?: string | boolean }` route to redirect to after login |
| `checkError` | Error is not an auth error | `void` |
| `checkAuth` | User is authenticated | `void` |
| `logout` | Auth backend acknowledged logout | `string | false | void` route to redirect to after logout, defaults to `/login` |
Expand Down
100 changes: 100 additions & 0 deletions packages/ra-core/src/auth/useLogin.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import expect from 'expect';

import { CoreAdminContext } from '../core/CoreAdminContext';
import useLogin from './useLogin';

describe('useLogin', () => {
describe('redirect after login', () => {
it('should redirect to home page by default', async () => {
const Login = () => {
const login = useLogin();
return <button onClick={login}>Login</button>;
};
const authProvider = {
login: () => Promise.resolve(),
checkError: error => Promise.resolve(),
checkAuth: params => Promise.resolve(),
logout: () => Promise.resolve(),
getIdentity: () => Promise.resolve({ id: 'joe' }),
getPermissions: () => Promise.resolve(),
};
render(
<MemoryRouter initialEntries={['/login']}>
<CoreAdminContext authProvider={authProvider}>
<Routes>
<Route path="/" element={<div>Home</div>} />
<Route path="/login" element={<Login />} />
</Routes>
</CoreAdminContext>
</MemoryRouter>
);
await screen.findByText('Login');
fireEvent.click(screen.getByText('Login'));
await screen.findByText('Home');
});
it('should redirect to the redirectTo returned by login', async () => {
const Login = () => {
const login = useLogin();
return <button onClick={login}>Login</button>;
};
const authProvider = {
login: () => Promise.resolve({ redirectTo: '/foo' }),
checkError: error => Promise.resolve(),
checkAuth: params => Promise.resolve(),
logout: () => Promise.resolve(),
getIdentity: () => Promise.resolve({ id: 'joe' }),
getPermissions: () => Promise.resolve(),
};
render(
<MemoryRouter initialEntries={['/login']}>
<CoreAdminContext authProvider={authProvider}>
<Routes>
<Route path="/" element={<div>Home</div>} />
<Route path="/login" element={<Login />} />
<Route path="/foo" element={<div>Foo</div>} />
</Routes>
</CoreAdminContext>
</MemoryRouter>
);
await screen.findByText('Login');
fireEvent.click(screen.getByText('Login'));
await screen.findByText('Foo');
});

it('should not redirect if login returns redirectTo false', async () => {
const Login = () => {
const login = useLogin();
return <button onClick={login}>Login</button>;
};
const authProvider = {
login: () => Promise.resolve({ redirectTo: false }),
checkError: error => Promise.resolve(),
checkAuth: params => Promise.resolve(),
logout: () => Promise.resolve(),
getIdentity: () => Promise.resolve({ id: 'joe' }),
getPermissions: () => Promise.resolve(),
};
render(
<MemoryRouter initialEntries={['/login']}>
<CoreAdminContext authProvider={authProvider}>
<Routes>
<Route path="/" element={<div>Home</div>} />
<Route path="/login" element={<Login />} />
</Routes>
</CoreAdminContext>
</MemoryRouter>
);
await screen.findByText('Login');
fireEvent.click(screen.getByText('Login'));
await waitFor(
// there is no other way to know if the login has been done
() => new Promise(resolve => setTimeout(resolve, 50))
);
expect(screen.queryByText('Home')).toBeNull();
expect(screen.queryByText('Login')).not.toBeNull();
});
});
});
14 changes: 10 additions & 4 deletions packages/ra-core/src/auth/useLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,16 @@ const useLogin = (): Login => {
(params: any = {}, pathName) =>
authProvider.login(params).then(ret => {
resetNotifications();
const redirectUrl = pathName
? pathName
: nextPathName + nextSearch || afterLoginUrl;
navigate(redirectUrl);
if (ret && ret.hasOwnProperty('redirectTo')) {
if (ret) {
navigate(ret.redirectTo);
}
} else {
const redirectUrl = pathName
? pathName
: nextPathName + nextSearch || afterLoginUrl;
navigate(redirectUrl);
}
return ret;
}),
[
Expand Down
4 changes: 3 additions & 1 deletion packages/ra-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export interface UserIdentity {
* authProvider types
*/
export type AuthProvider = {
login: (params: any) => Promise<any>;
login: (
params: any
) => Promise<{ redirectTo?: string | boolean } | void | any>;
logout: (params: any) => Promise<void | false | string>;
checkAuth: (params: any) => Promise<void>;
checkError: (error: any) => Promise<void>;
Expand Down