From 5d069baa25907918045dd721552437cbbe8eeeb2 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 6 Nov 2020 09:26:39 +0100 Subject: [PATCH 1/4] [Doc] Improve Auth Provider chapter Closes #5491 --- docs/Admin.md | 4 +- docs/Authentication.md | 889 +++++++++++++++++++++++++++++++++-------- docs/Authorization.md | 257 ------------ docs/Ecosystem.md | 6 +- docs/Reference.md | 11 +- docs/UnitTesting.md | 2 +- docs/navigation.html | 7 +- 7 files changed, 741 insertions(+), 435 deletions(-) delete mode 100644 docs/Authorization.md diff --git a/docs/Admin.md b/docs/Admin.md index 66efb5f0af6..71ebcf57dac 100644 --- a/docs/Admin.md +++ b/docs/Admin.md @@ -85,7 +85,7 @@ const App = () => ( ); ``` -The [Authentication documentation](./Authentication.md) explains how to implement these functions in detail. +The [Auth Provider documentation](./Authentication.md) explains how to implement these functions in detail. ## `i18nProvider` @@ -585,7 +585,7 @@ You might want to dynamically define the resources when the app starts. To do so ### Using a Function As `` Child -The `` component accepts a function as its child and this function can return a Promise. If you also defined an `authProvider`, the child function will receive the result of a call to `authProvider.getPermissions()` (you can read more about this in the [Authorization](./Authorization.md) chapter). +The `` component accepts a function as its child and this function can return a Promise. If you also defined an `authProvider`, the child function will receive the result of a call to `authProvider.getPermissions()` (you can read more about this in the [Auth Provider](./Authentication.md#authorization) chapter). For instance, getting the resource from an API might look like: diff --git a/docs/Authentication.md b/docs/Authentication.md index 007562b0db3..fd70014be5d 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -1,15 +1,15 @@ --- layout: default -title: "Authentication" +title: "Auth Providers" --- -# Authentication +# Auth Providers ![Login](./img/login.gif) -React-admin lets you secure your admin app with the authentication strategy of your choice. Since there are many possible strategies (Basic Auth, JWT, OAuth, etc.), react-admin delegates authentication logic to your `authProvider`, and provides hooks to execute your authentication code. +React-admin lets you secure your admin app with the authentication strategy of your choice. Since there are many possible strategies (Basic Auth, JWT, OAuth, etc.), react-admin delegates authentication logic to an `authProvider`. -## The `authProvider` +## Enabling Auth Features By default, react-admin apps don't require authentication. To restrict access to the admin, pass an `authProvider` to the `` component. @@ -24,30 +24,53 @@ const App = () => ( ); ``` -What's an `authProvider`? Just like a `dataProvider`, an `authProvider` is an object that handles authentication logic. It exposes methods that react-admin calls when needed, and that return a Promise. The simplest `authProvider` is: +React-admin delegates the [Authentication](#authentication) and [Authorization](#authorization) logic to an object that you must write, the `authProvider`. + +- "Authentication" logic allows to restrict an app to identified users only, and reject anonymous users +- "Authorization" logic allows to tweak the features based on user permissions + +## Anatomy Of An `authProvider` + +What's an `authProvider`? Just like a `dataProvider`, an `authProvider` is an object that handles authentication and authorization logic. It exposes methods that react-admin calls when needed, and that you can call manually through specialized [hooks](#hooks). The `authProvider` methods must return a Promise. The simplest `authProvider` is: ```js const authProvider = { + // authentication login: params => Promise.resolve(), - logout: params => Promise.resolve(), - checkAuth: params => Promise.resolve(), checkError: error => Promise.resolve(), - getPermissions: params => Promise.resolve(), + checkAuth: params => Promise.resolve(), + logout: () => Promise.resolve(), getIdentity: () => Promise.resolve(), + // authorization + getPermissions: params => Promise.resolve(), }; ``` +You can get more details about input params, response and error formats in the [Building Your Own Auth Provider section](#building-your-own-auth-provider) below. + **Tip**: In react-admin version 2.0, the `authProvider` used to be a function instead of an object. React-admin 3.0 accepts both object and (legacy) function authProviders. -Let's see when react-admin calls the `authProvider`, and how to write one for your own authentication provider. +## Available Providers -## Login Configuration +It's very common that your auth logic is so specific that so you'll need to write your own `authProvider`. However, the community has built a few open-source Auth Providers that may fit your need: + +- **[AWS Amplify](https://docs.amplify.aws)**: [MrHertal/react-admin-amplify](https://github.com/MrHertal/react-admin-amplify) +- **[AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/setting-up-the-javascript-sdk.html)**: [thedistance/ra-cognito](https://github.com/thedistance/ra-cognito) +- **[Firebase Auth (Google, Facebook, Github etc)](https://firebase.google.com/docs/auth/web/firebaseui)**: [benwinding/react-admin-firebase](https://github.com/benwinding/react-admin-firebase#auth-provider) + +If you have released a reusable `authProvider` for a standard auth backend, please open a PR to add it to this list! + +## Authentication + +Let's see when react-admin calls the `authProvider`, and how customize it depending on your authentication strategy and backend. + +### Login Configuration Once an admin has an `authProvider`, react-admin enables a new page on the `/login` route, which displays a login form asking for a username and password. ![Default Login Form](./img/login-form.png) -Upon submission, this form calls the `authProvider.login({ login, password })` method. It's the ideal place to authenticate the user, and store their credentials. +Upon submission, this form calls the `authProvider.login({ login, password })` method. React-admin expects this method to return a resolved Promise if the credentials are correct, and to a rejected Promise if they're not. For instance, to query an authentication route via HTTPS and store the credentials (a token) in local storage, configure `authProvider` as follows: @@ -69,6 +92,9 @@ const authProvider = { }) .then(auth => { localStorage.setItem('auth', JSON.stringify(auth)); + }) + .catch(() => { + throw new Error('Network error') }); }, // ... @@ -81,9 +107,11 @@ 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. -## Sending Credentials to the API +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. -Now the user has logged in, you can use their credentials to communicate with the `dataProvider`. For that, you have to tweak, this time, the `dataProvider` function. As explained in the [Data providers documentation](DataProviders.md#adding-custom-headers), `simpleRestProvider` and `jsonServerProvider` take an `httpClient` as second parameter. That's the place where you can change request headers, cookies, etc. +### Sending Credentials To The API + +Now the user has logged in, you can use their credentials in the `dataProvider` to communicate with the data API. As explained in the [Data providers documentation](DataProviders.md#adding-custom-headers), `simpleRestProvider` and `jsonServerProvider` take an `httpClient` as second parameter. That's the place where you can change request headers, cookies, etc. For instance, to pass the token obtained during login as an `Authorization` header, configure the Data Provider as follows: @@ -112,77 +140,11 @@ Now the admin is secured: The user can be authenticated and use their credential If you have a custom REST client, don't forget to add credentials yourself. -## User Identity - -React-admin displays the current user name and avatar on the top right side of the screen. To enable this feature, implement the `getIdentity` method in the `authProvider`: - -```js -// in src/authProvider.js -const authProvider = { - login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { - const { id, fullName, avatar } = JSON.parse(localStorage.getItem('auth')); - return { id, fullName, avatar }; - } - // ... -}; - -export default authProvider; -``` - -React-admin uses the `fullName` and the `avatar` (an image source, or a data-uri) in the App Bar: - -![User identity](./img/identity.png) - -**Tip**: You can use the `id` field to identify the current user in your code, by calling the `useGetIdentity` hook: - -```jsx -import { useGetIdentity, useGetOne } from 'react-admin'; - -const PostDetail = ({ id }) => { - const { data: post, loading: postLoading } = useGetOne('posts', id); - const { identity, loading: identityLoading } = useGetIdentity(); - if (postLoading || identityLoading) return <>Loading...; - if (!post.lockedBy || post.lockedBy === identity.id) { - // post isn't locked, or is locked by me - return - } else { - // post is locked by someone else and cannot be edited - return - } -} -``` - -## Logout Configuration - -As soon as you provide an `authProvider` prop to ``, react-admin displays a logout button in the top bar (or in the menu on mobile). When the user clicks on the logout button, this calls the `authProvider.logout()` method, and removes potentially sensitive data from the Redux store. Then the user gets redirected to the login page. - -So it's the responsibility of the `authProvider` to clean up the current authentication data. For instance, if the authentication was a token stored in local storage, here the code to remove it: - -```js -// in src/authProvider.js -export default { - login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { /* ... */ }, - logout: () => { - localStorage.removeItem('auth'); - return Promise.resolve(); - }, - // ... -}; -``` - -![Logout button](./img/logout.gif) - -The `authProvider` is also a good place to notify the authentication API that the user credentials are no longer valid after logout. - -Note that after logout, react-admin redirects the user to the string returned by `authProvider.logout()` - or to the `/login` url if the method returns nothing. You can customize the redirection url by returning a route string, or `false` to disable redirection after logout. - -## Catching Authentication Errors On The API +### Catching Authentication Errors On The API -If the API requires authentication, and the user credentials are missing in the request or invalid, the API usually answers with an HTTP error code 401 or 403. +When the user credentials are missing or become invalid, a secure API usually answers to the `dataProvider` with an HTTP error code 401 or 403. -Fortunately, each time the API returns an error, react-admin calls the `authProvider.checkError()` method. When `checkError()` returns a rejected promise, react-admin calls the `authProvider.logout()` method. +Fortunately, each time the `dataProvider` returns an error, react-admin calls the `authProvider.checkError()` method. If it returns a rejected promise, react-admin calls the `authProvider.logout()` method immediately, and asks the user to log in again. So it's up to you to decide which HTTP status codes should let the user continue (by returning a resolved promise) or log them out (by returning a rejected promise). @@ -192,8 +154,6 @@ For instance, to log the user out for both 401 and 403 codes: // in src/authProvider.js export default { login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { /* ... */ }, - logout: () => { /* ... */ }, checkError: (error) => { const status = error.status; if (status === 401 || status === 403) { @@ -213,8 +173,6 @@ When `authProvider.checkError()` returns a rejected Promise, react-admin redirec // in src/authProvider.js export default { login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { /* ... */ }, - logout: () => { /* ... */ }, checkError: (error) => { const status = error.status; if (status === 401 || status === 403) { @@ -234,8 +192,6 @@ When `authProvider.checkError()` returns a rejected Promise, react-admin display // in src/authProvider.js export default { login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { /* ... */ }, - logout: () => { /* ... */ }, checkError: (error) => { const status = error.status; if (status === 401 || status === 403) { @@ -249,11 +205,11 @@ export default { }; ``` -## Checking Credentials During Navigation +### Checking Credentials During Navigation -Redirecting to the login page whenever a REST response uses a 401 status code is usually not enough, because react-admin keeps data on the client side, and could display stale data while contacting the server - even after the credentials are no longer valid. +Redirecting to the login page whenever a REST response uses a 401 status code is usually not enough. React-admin keeps data on the client side, and could briefly display stale data while contacting the server - even after the credentials are no longer valid. -Fortunately, each time the user navigates, react-admin calls the `authProvider.checkAuth()` method, so it's the ideal place to validate the credentials. +Fortunately, each time the user navigates to a list, edit, create or show page, react-admin calls the `authProvider.checkAuth()` method. If this method returns a rejected Promise, react-admin calls `authProvider.logout()` and redirects the user to the login page. So it's the ideal place to make sure the credentials are still valid. For instance, to check for the existence of the authentication data in local storage: @@ -261,8 +217,6 @@ For instance, to check for the existence of the authentication data in local sto // in src/authProvider.js export default { login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { /* ... */ }, - logout: () => { /* ... */ }, checkError: (error) => { /* ... */ }, checkAuth: () => localStorage.getItem('auth') ? Promise.resolve() @@ -277,8 +231,6 @@ If the promise is rejected, react-admin redirects by default to the `/login` pag // in src/authProvider.js export default { login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { /* ... */ }, - logout: () => { /* ... */ }, checkError: (error) => { /* ... */ }, checkAuth: () => localStorage.getItem('auth') ? Promise.resolve() @@ -287,7 +239,7 @@ export default { } ``` -Note that react-admin will call the `authProvider.logout()` method before redirecting. If you specify the `redirectTo` here, it will override the url which may have been returned by the call to `logout()`. +**Tip**: If both `authProvider.checkAuth()` and `authProvider.logout()` return a redirect URL, the one from `authProvider.checkAuth()` takes precedence. If the promise is rejected, react-admin displays a notification to the end user. You can customize this message by rejecting an error with a `message` property: @@ -295,8 +247,6 @@ If the promise is rejected, react-admin displays a notification to the end user. // in src/authProvider.js export default { login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { /* ... */ }, - logout: () => { /* ... */ }, checkError: (error) => { /* ... */ }, checkAuth: () => localStorage.getItem('auth') ? Promise.resolve() @@ -311,8 +261,6 @@ You can also disable this notification completely by rejecting an error with a ` // in src/authProvider.js export default { login: ({ username, password }) => { /* ... */ }, - getIdentity: () => { /* ... */ }, - logout: () => { /* ... */ }, checkError: (error) => { /* ... */ }, checkAuth: () => localStorage.getItem('auth') ? Promise.resolve() @@ -321,32 +269,270 @@ export default { } ``` -**Tip**: In addition to `login()`, `logout()`, `checkError()`, and `checkAuth()`, react-admin calls the `authProvider.getPermissions()` method to check user permissions. It's useful to enable or disable features on a per user basis. Read the [Authorization Documentation](./Authorization.md) to learn how to implement that type. +### Logout Configuration -## Customizing The Login and Logout Components +If you enable authentication, react-admin adds a logout button in the user menu in the top bar (or in the sliding menu on mobile. When the user clicks on the logout button, this calls the `authProvider.logout()` method, and removes potentially sensitive data from the Redux store. Then the user gets redirected to the login page. The two previous sections also illustrated that react-admin can call `authProvider.logout()` itself, when the API returns a 403 error or when the local credentials expire. -Using `authProvider` is enough to implement a full-featured authorization system if the authentication relies on a username and password. +It's the responsibility of the `authProvider.logout()` method to clean up the current authentication data. For instance, if the authentication was a token stored in local storage, here is the code to remove it: -But what if you want to use an email instead of a username? What if you want to use a Single-Sign-On (SSO) with a third-party authentication service? What if you want to use two-factor authentication? +```js +// in src/authProvider.js +export default { + login: ({ username, password }) => { /* ... */ }, + checkError: (error) => { /* ... */ }, + checkAuth: () => { /* ... */ }, + logout: () => { + localStorage.removeItem('auth'); + return Promise.resolve(); + }, + // ... +}; +``` -For all these cases, it's up to you to implement your own `LoginPage` component, which will be displayed under the `/login` route instead of the default username/password form, and your own `LogoutButton` component, which will be displayed in the sidebar. Pass both these components to the `` component: +![Logout button](./img/logout.gif) + +The `authProvider.logout()` method is also a good place to notify the authentication backend that the user credentials are no longer valid after logout. + +After logout, react-admin redirects the user to the string returned by `authProvider.logout()` - or to the `/login` url if the method returns nothing. You can customize the redirection url by returning a route string, or `false` to disable redirection after logout. + +```js +// in src/authProvider.js +export default { + login: ({ username, password }) => { /* ... */ }, + checkError: (error) => { /* ... */ }, + checkAuth: () => { /* ... */ }, + logout: () => { + localStorage.removeItem('auth'); + return Promise.resolve('/my-custom-login'); + }, + // ... +}; +``` + +### User Identity + +React-admin can display the current user name and avatar on the top right side of the screen. To enable this feature, implement the `authProvider.getIdentity()` method: + +```js +// in src/authProvider.js +const authProvider = { + login: ({ username, password }) => { /* ... */ }, + checkError: (error) => { /* ... */ }, + checkAuth: () => { /* ... */ }, + logout: () => { /* ... */ }, + getIdentity: () => { + try { + const { id, fullName, avatar } = JSON.parse(localStorage.getItem('auth')); + return Promise.resolve({ id, fullName, avatar }); + } catch (error) { + return Promise.reject(error); + } + } + // ... +}; + +export default authProvider; +``` + +React-admin uses the `fullName` and the `avatar` (an image source, or a data-uri) in the App Bar: + +![User identity](./img/identity.png) + +**Tip**: You can use the `id` field to identify the current user in your code, by calling the `useGetIdentity` hook: ```jsx -// in src/App.js +import { useGetIdentity, useGetOne } from 'react-admin'; + +const PostDetail = ({ id }) => { + const { data: post, loading: postLoading } = useGetOne('posts', id); + const { identity, loading: identityLoading } = useGetIdentity(); + if (postLoading || identityLoading) return <>Loading...; + if (!post.lockedBy || post.lockedBy === identity.id) { + // post isn't locked, or is locked by me + return + } else { + // post is locked by someone else and cannot be edited + return + } +} +``` + +## Authorization + +Some applications may require fine-grained permissions to enable or disable access to certain features depending on user permissions. Since there are many possible strategies (single role, multiple roles or rights, ACLs, etc.), react-admin delegates the permission logic to `autheProvider.getPermissions()`. + +By default, a react-admin app doesn't require any special permission on list, create, edit, and show pages. However, react-admin calls the `authProvider.getPermissions()` method before navigating to these pages, and passes the result to the main page component (``, ``, etc.). You can then tweak the content of these pages based on permissions. + +Additionally, in custom pages, you can call the `usePermissions()` hook to grab the user permissions. + +### User Permissions + +React-admin calls the `authProvider.getPermissions()` whenever it needs the user permissions. These permissions can take the shape you want: + +- a string (e.g. `'admin'`), +- an array of roles (e.g. `['post_editor', 'comment_moderator', 'super_admin']`) +- an object with fine-grained permissions (e.g. `{ postList: { read: true, write: false, delete: false } }`) +- or even a function + +The format of permissions is free because react-admin never actually uses the permissions itself. It's up to you to use them in your code to hide or display content, redirect the user to another page, or display warnings. + +Following is an example where the `authProvider` stores the user's permissions in `localStorage` upon authentication, and returns these permissions when called with `getPermissions`: + +{% raw %} +```jsx +// in src/authProvider.js +import decodeJwt from 'jwt-decode'; + +export default { + 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 => { + if (response.status < 200 || response.status >= 300) { + throw new Error(response.statusText); + } + return response.json(); + }) + .then(({ token }) => { + const decodedToken = decodeJwt(token); + localStorage.setItem('token', token); + localStorage.setItem('permissions', decodedToken.permissions); + }); + }, + checkError: (error) => { /* ... */ }, + checkAuth: () => { + return localStorage.getItem('token') ? Promise.resolve() : Promise.reject(); + }, + logout: () => { + localStorage.removeItem('token'); + localStorage.removeItem('permissions'); + return Promise.resolve(); + }, + getIdentity: (error) => { /* ... */ }, + getPermissions: () => { + const role = localStorage.getItem('permissions'); + return role ? Promise.resolve(role) : Promise.reject(); + } +}; +``` +{% endraw %} + +### Getting User Permissions In CRUD Pages + +By default, react-admin calls `authProvider.getPermissions()` for each resource route, and passes the permissions to the `list`, `edit`, `create`, and `show` view components. So the ``, ``, `` and `` components all receive a `permissions` prop containing what `authProvider.getPermissions()` returned. + +Here is an example of a `Create` view with a conditional Input based on permissions: + +{% raw %} +```jsx +export const UserCreate = ({ permissions, ...props }) => + + + + {permissions === 'admin' && + } + + ; +``` +{% endraw %} + +### Getting User Permissions In Custom Pages + +In custom pages, react-admin doesn't call `authProvider.getPermissions()`. It's up to you to call it yourself, using [the `usePermissions()` hook](#usepermissions-hook): + +```jsx +// in src/MyPage.js import * as React from "react"; -import { Admin } from 'react-admin'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { usePermissions } from 'react-admin'; -import MyLoginPage from './MyLoginPage'; -import MyLogoutButton from './MyLogoutButton'; +const MyPage = () => { + const { permissions } = usePermissions(); + return ( + + Lorem ipsum sic dolor amet... + {permissions === 'admin' && + Sensitive data + } + + ); +} +``` -const App = () => ( - - ... - -); +## Building Your Own Auth Provider + +Here is the interface react-admin expect `authProvider` objects to implement. + +**Tip**: If you're a TypeScript user, you can check that your `authProvider` is correct at compile-tiome using the `AuthProvider` type: + +```jsx +import { AuthProvider } from 'react-admin'; + +const authProvider: AuthProvider = { + // authentication + login: ({ username, password }) => { /* ... */ }, + checkError: (error) => { /* ... */ }, + checkAuth: () => { /* ... */ }, + logout: () => { /* ... */ }, + getIdentity: () => { /* ... */ }, + // authorization + getPermissions: (params) => { /* ... */ }, +} ``` -Use the `useLogin` and `useLogout` hooks in your custom `LoginPage` and `LogoutButton` components. +### Request Format + +React-admin calls the `authProvider` methods with the following params: + +| Method | Usage | Parameters format | +| ---------------- | ----------------------------------------------- | ------------------ | +| `login` | Log a user in | `Object` whatever fields the login form contains | +| `checkError` | Check if a dataProvider error is an authentication error | `{ message: string, status: number, body: Object }` the error returned by the `dataProvider` | +| `checkAuth` | Check credentials before moving to a new route | `Object` whatever params passed to `useCheckAuth()` - void for react-admin default routes | +| `logout` | Log a user out | `void` | +| `getIdentity` | Get the current user identity | `void` | +| `getPermissions` | Get the current user credentials | `Object` whatever params passed to `usePermissions()` - void for react-admin default routes | + +### Response Format + +`authProvider` methods must return a Promise. In case of success, the Promise should resolve to the following value: + +| Method | Resolve if | Response format | +| ---------------- | --------------------------------- | --------------- | +| `login` | Login credentials were accepted | `void` | +| `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` | +| `getIdentity` | Auth backend returned identity | `{ id: string | number, fullName?: string, avatar?: string }` | +| `getPermissions` | Auth backend returned permissions | `Object | Array` free format - the response will be returned when `usePermissions()` is called | + +### Error Format + +When the auth backend returns an error, the Auth Provider should return a rejected Promise, with the following value: + +| Method | Reject if | Error format | +| ---------------- | ----------------------------------------- | --------------- | +| `login` | Login credentials weren't accepted | `string | { message?: string }` error message to display | +| `checkError` | Error is an auth error | `void | { redirectTo?: string, message?: boolean }` route to redirect to after logout, and whether to disable error notification | +| `checkAuth` | User is not authenticated | `void | { redirectTo?: string, message?: string }` route to redirect to after logout, message to notify the user | +| `logout` | Auth backend failed to log the user out | `void` | +| `getIdentity` | Auth backend failed to return identity | `Object` free format - returned as `error` when `useGetIdentity()` is called | +| `getPermissions` | Auth backend failed to return permissions | `Object` free format - returned as `error` when `usePermissions()` is called | + +## Hooks + +### `useLogin()` Hook + +This hook returns a callback allowing to call `authProvider.login()`, so it's used in Login forms. + +For instance, here is how to build a custom Login page based on email rather than login for authentication: ```jsx // in src/MyLoginPage.js @@ -363,6 +549,7 @@ const MyLoginPage = ({ theme }) => { const notify = useNotify(); const submit = e => { e.preventDefault(); + // will call authProvider.login({ email, password }) login({ email, password }).catch(() => notify('Invalid email or password') ); @@ -390,40 +577,25 @@ const MyLoginPage = ({ theme }) => { }; export default MyLoginPage; +``` -// in src/MyLogoutButton.js -import * as React from 'react'; -import { forwardRef } from 'react'; -import { useLogout } from 'react-admin'; -import MenuItem from '@material-ui/core/MenuItem'; -import ExitIcon from '@material-ui/icons/PowerSettingsNew'; +Then pass the custom Login form to ``, as follows: -const MyLogoutButton = forwardRef((props, ref) => { - const logout = useLogout(); - const handleClick = () => logout(); - return ( - - Logout - - ); -}); - -export default MyLogoutButton; -``` +```jsx +// in src/App.js +import * as React from "react"; +import { Admin } from 'react-admin'; -**Tip**: By default, react-admin redirects the user to '/login' after they log out. This can be changed by passing the url to redirect to as parameter to the `logout()` function: +import MyLoginPage from './MyLoginPage'; -```diff -// in src/MyLogoutButton.js -// ... -- const handleClick = () => logout(); -+ const handleClick = () => logout('/custom-login'); +const App = () => ( + + ... + +); ``` -## `useAuthenticated()` Hook +### `useAuthenticated()` Hook If you add [custom pages](./Actions.md), or if you [create an admin app from scratch](./CustomApp.md), you may need to secure access to pages manually. That's the purpose of the `useAuthenticated()` hook, which calls the `authProvider.checkAuth()` method on mount, and redirects to login if it returns a rejected Promise. @@ -458,9 +630,181 @@ const MyPage = () => { The `useAuthenticated` hook is optimistic: it doesn't block rendering during the `authProvider` call. In the above example, the `MyPage` component renders even before getting the response from the `authProvider`. If the call returns a rejected promise, the hook redirects to the login page, but the user may have seen the content of the `MyPage` component for a brief moment. -## `` Component +### `useAuthState()` Hook + +To avoid rendering a component, and to force waiting for the `authProvider` response, use `useAuthState()` instead of `useAuthenticated()`. It calls `authProvider.checkAuth()` on mount and returns an object with 3 properties: + +- `loading`: `true` just after mount, while the `authProvider` is being called. `false` once the `authProvider` has answered. +- `loaded`: the opposite of `loading`. +- `authenticated`: `true` while loading. then `true` or `false` depending on the `authProvider` response. + +You can render different content depending on the authenticated status. + +```jsx +import { useAuthState, Loading } from 'react-admin'; + +const MyPage = () => { + const { loading, authenticated } = useAuthState(); + if (loading) { + return ; + } + if (authenticated) { + return ; + } + return ; +}; +``` + +### `useLogout()` Hook -The `` component uses the `useAuthenticated()` hook, and renders its child component - unless the authentication check fails. Use it as an alternative to the `useAuthenticated()` hook when you can't use a hook, e.g. inside a `Route` `render` function: +Just like `useLogin()`, `useLogout()` returns a callback that you can use to call `authProvider.logout()``. Use it to build a custom Logout button, like the following: + +```jsx +// in src/MyLogoutButton.js +import * as React from 'react'; +import { forwardRef } from 'react'; +import { useLogout } from 'react-admin'; +import MenuItem from '@material-ui/core/MenuItem'; +import ExitIcon from '@material-ui/icons/PowerSettingsNew'; + +const MyLogoutButton = forwardRef((props, ref) => { + const logout = useLogout(); + const handleClick = () => logout(); + return ( + + Logout + + ); +}); + +export default MyLogoutButton; +``` + +Then pass the Logout button to the `` component, as follows: + +```jsx +// in src/App.js +import * as React from "react"; +import { Admin } from 'react-admin'; + +import MyLogoutButton from './MyLogoutButton'; + +const App = () => ( + + ... + +); +``` + +### `useGetIdentity()` Hook + +You may want to use the current user name, avatar, or id in your code. for that purpose, call the `useGetIdentity()` hook, which calls `authProvider.getIdentity()` on mount. + +Here is an example Edit component, which falls back to a Show component is the record is locked for edition by another user: + +```jsx +import { useGetIdentity, useGetOne } from 'react-admin'; + +const PostDetail = ({ id }) => { + const { data: post, loading: postLoading } = useGetOne('posts', id); + const { identity, loading: identityLoading } = useGetIdentity(); + if (postLoading || identityLoading) return <>Loading...; + if (!post.lockedBy || post.lockedBy === identity.id) { + // post isn't locked, or is locked by me + return + } else { + // post is locked by someone else and cannot be edited + return + } +} +``` + +### `usePermissions()` Hook + +You might want to check user permissions inside a [custom page](./Admin.md#customroutes). That's the purpose of the `usePermissions()` hook, which calls the `authProvider.getPermissions()` method on mount, and returns the result when available: + +```jsx +// in src/MyPage.js +import * as React from "react"; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { usePermissions } from 'react-admin'; + +const MyPage = () => { + const { loading, permissions } = usePermissions(); + return loading + ? (
Waiting for permissions...
) + : ( + + Lorem ipsum sic dolor amet... + {permissions === 'admin' && + Sensitive data + } + + ); +} + +export default MyPage; + +// in src/customRoutes.js +import * as React from "react"; +import { Route } from 'react-router-dom'; +import MyPage from './MyPage'; + +export default [ + , +]; +``` + +The `usePermissions` hook is optimistic: it doesn't block rendering during the `authProvider` call. In the above example, the `MyPage` component renders even before getting the response from the `authProvider`. To avoid a blink in the interface while the `authProvider` is answering, use the `loaded` return value of `usePermissions()`: + +```jsx +const MyPage = () => { + const { loaded, permissions } = usePermissions(); + return loaded ? ( + + Lorem ipsum sic dolor amet... + {permissions === 'admin' && + Sensitive data + } + + ) : null; +} +``` + +### `useGetPermissions()` Hook + +React-admin also exposes a `useGetPermissions()` hook, returning a callback to call `authProvider.getPermissions()` on demand. In practice, you seldom need this hook - `usePermissions` covers most authorizatoin needs, and manages the loading state for you. + +Here is an example usage: + +```jsx + import { useGetPermissions } from 'react-admin'; + + const Roles = () => { + const [permissions, setPermissions] = useState([]); + const getPermissions = useGetPermissions(); + useEffect(() => { + getPermissions().then(permissions => setPermissions(permissions)) + }, []) + return ( +
    + {permissions.map((permission, key) => ( +
  • {permission}
  • + ))} +
+ ); + } + ``` + +## Components + +### `` Component + +The `` component calls [the `useAuthenticated()` hook](#useauthenticated-hook), and renders its child component - unless the authentication check fails. Use it as an alternative to the `useAuthenticated()` hook when you can't use a hook, e.g. inside a `Route` `render` function: ```jsx import { Authenticated } from 'react-admin'; @@ -479,27 +823,250 @@ const App = () => ( ); ``` -## `useAuthState()` Hook +## Recipes -To avoid rendering a component and force waiting for the `authProvider` response, use the `useAuthState()` hook instead of the `useAuthenticated()` hook. It returns an object with 3 properties: +### Customizing The Login and Logout Components -- `loading`: `true` just after mount, while the `authProvider` is being called. `false` once the `authProvider` has answered. -- `loaded`: the opposite of `loading`. -- `authenticated`: `true` while loading. then `true` or `false` depending on the `authProvider` response. +Using `authProvider` is enough to implement a full-featured authorization system if the authentication relies on a username and password. -You can render different content depending on the authenticated status. +But what if you want to use an email instead of a username? What if you want to use a Single-Sign-On (SSO) with a third-party authentication service? What if you want to use two-factor authentication? + +For all these cases, it's up to you to implement your own `LoginPage` component, which will be displayed under the `/login` route instead of the default username/password form, and your own `LogoutButton` component, which will be displayed in the sidebar. Pass both these components to the `` component: ```jsx -import { useAuthState, Loading } from 'react-admin'; +// in src/App.js +import * as React from "react"; +import { Admin } from 'react-admin'; -const MyPage = () => { - const { loading, authenticated } = useAuthState(); - if (loading) { - return ; - } - if (authenticated) { - return ; - } - return ; +import MyLoginPage from './MyLoginPage'; +import MyLogoutButton from './MyLogoutButton'; + +const App = () => ( + + ... + +); +``` + +Use the `useLogin` and `useLogout` hooks in your custom `LoginPage` and `LogoutButton` components. + +```jsx +// in src/MyLoginPage.js +import * as React from 'react'; +import { useState } from 'react'; +import { useLogin, useNotify, Notification, defaultTheme } from 'react-admin'; +import { ThemeProvider } from '@material-ui/styles'; +import { createMuiTheme } from '@material-ui/core/styles'; + +const MyLoginPage = ({ theme }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const login = useLogin(); + const notify = useNotify(); + const submit = e => { + e.preventDefault(); + login({ email, password }).catch(() => + notify('Invalid email or password') + ); + }; + + return ( + +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> +
+ +
+ ); }; + +export default MyLoginPage; + +// in src/MyLogoutButton.js +import * as React from 'react'; +import { forwardRef } from 'react'; +import { useLogout } from 'react-admin'; +import MenuItem from '@material-ui/core/MenuItem'; +import ExitIcon from '@material-ui/icons/PowerSettingsNew'; + +const MyLogoutButton = forwardRef((props, ref) => { + const logout = useLogout(); + const handleClick = () => logout(); + return ( + + Logout + + ); +}); + +export default MyLogoutButton; +``` + +**Tip**: By default, react-admin redirects the user to '/login' after they log out. This can be changed by passing the url to redirect to as parameter to the `logout()` function: + +```diff +// in src/MyLogoutButton.js +// ... +- const handleClick = () => logout(); ++ const handleClick = () => logout('/custom-login'); +``` + +### Restricting Access to Resources or Views + +Permissions can be useful to restrict access to resources or their views. To do so, you must use a function as the `` only child. React-admin will call this function with the permissions returned by the `authProvider`. + +```jsx + + {permissions => [ + // Restrict access to the edit and remove views to admin only + , + // Only include the categories resource for admin users + permissions === 'admin' + ? + : null, + ]} + +``` + +Note that the function returns an array of React elements. This is required to avoid having to wrap them in a container element which would prevent the `Admin` from working. + +**Tip**: Even if that's possible, be careful when completely excluding a resource (like with the `categories` resource in this example) as it will prevent you to reference this resource in the other resource views, too. + +### Restricting Access to Fields and Inputs + +You might want to display some fields or inputs only to users with specific permissions. By default, react-admin calls the `authProvider` for permissions for each resource routes, and passes them to the `list`, `edit`, `create`, and `show` components. + +Here is an example of a `Create` view with a conditional Input based on permissions: + +{% raw %} +```jsx +export const UserCreate = ({ permissions, ...props }) => + + + + {permissions === 'admin' && + } + + ; +``` +{% endraw %} + +This also works inside an `Edition` view with a `TabbedForm`, and you can even hide a `FormTab` completely: + +{% raw %} +```jsx +export const UserEdit = ({ permissions, ...props }) => + } {...props}> + + + {permissions === 'admin' && } + + + {permissions === 'admin' && + + + } + + ; +``` +{% endraw %} + +What about the `List` view, the `Datagrid`, `SimpleList` and `Filter` components? It works there, too. And in the next example, the `permissions` prop is passed down to a custom `filters` component. + +```jsx +const UserFilter = ({ permissions, ...props }) => + + + + {permissions === 'admin' && } + ; + +export const UserList = ({ permissions, ...props }) => + } + > + + + + {permissions === 'admin' && } + {permissions === 'admin' && } + + + ; +``` + +### Restricting Access to the Dashboard + +React-admin injects the permissions into the component provided as a [`dashboard`](./Admin.md#dashboard), too: + +```jsx +// in src/Dashboard.js +import * as React from "react"; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { Title } from 'react-admin'; + +export default ({ permissions }) => ( + + + <CardContent>Lorem ipsum sic dolor amet...</CardContent> + {permissions === 'admin' + ? <CardContent>Sensitive data</CardContent> + : null + } + </Card> +); +``` + +### Restricting Access to a Menu + +What if you want to check the permissions inside a [custom menu](./Admin.md#menu)? Much like getting permissions inside a custom page, you'll have to use the `usePermissions` hook: + +```jsx +// in src/myMenu.js +import * as React from "react"; +import { MenuItemLink, usePermissions } from 'react-admin'; + +const Menu = ({ onMenuClick, logout }) => { + const { permissions } = usePermissions(); + return ( + <div> + <MenuItemLink to="/posts" primaryText="Posts" onClick={onMenuClick} /> + <MenuItemLink to="/comments" primaryText="Comments" onClick={onMenuClick} /> + {permissions === 'admin' && + <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} /> + } + {logout} + </div> + ); +} ``` diff --git a/docs/Authorization.md b/docs/Authorization.md deleted file mode 100644 index 313e7961bd1..00000000000 --- a/docs/Authorization.md +++ /dev/null @@ -1,257 +0,0 @@ ---- -layout: default -title: "Authorization" ---- - -# Authorization - -Some applications may require fine-grained permissions to enable or disable access to certain features. Since there are many possible strategies (single role, multiple roles or rights, ACLs, etc.), react-admin simply provides hooks to execute your own authorization code. - -By default, a react-admin app doesn't check authorization. However, if needed, it will rely on the `authProvider` introduced in the [Authentication documentation](./Authentication.md) to do so. You should read that chapter first. - -## Configuring the Auth Provider - -Each time react-admin needs to determine the user permissions, it calls the `authProvider.getPermissions()` method. It's up to you to return the user permissions, be it a string (e.g. `'admin'`) or an array of roles (e.g. `['post_editor', 'comment_moderator', 'super_admin']`). - -Following is an example where the `authProvider` stores the user's permissions in `localStorage` upon authentication, and returns these permissions when called with `getPermissions`: - -{% raw %} -```jsx -// in src/authProvider.js -import decodeJwt from 'jwt-decode'; - -export default { - 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 => { - if (response.status < 200 || response.status >= 300) { - throw new Error(response.statusText); - } - return response.json(); - }) - .then(({ token }) => { - const decodedToken = decodeJwt(token); - localStorage.setItem('token', token); - localStorage.setItem('permissions', decodedToken.permissions); - }); - }, - logout: () => { - localStorage.removeItem('token'); - localStorage.removeItem('permissions'); - return Promise.resolve(); - }, - checkError: error => { - // ... - }, - checkAuth: () => { - return localStorage.getItem('token') ? Promise.resolve() : Promise.reject(); - }, - getPermissions: () => { - const role = localStorage.getItem('permissions'); - return role ? Promise.resolve(role) : Promise.reject(); - } -}; -``` -{% endraw %} - -## Restricting Access to Resources or Views - -Permissions can be useful to restrict access to resources or their views. To do so, you must use a function as the `<Admin>` only child. React-admin will call this function with the permissions returned by the `authProvider`. - -```jsx -<Admin - dataProvider={dataProvider} - authProvider={authProvider} -> - {permissions => [ - // Restrict access to the edit and remove views to admin only - <Resource - name="customers" - list={VisitorList} - edit={permissions === 'admin' ? VisitorEdit : null} - icon={VisitorIcon} - />, - // Only include the categories resource for admin users - permissions === 'admin' - ? <Resource name="categories" list={CategoryList} edit={CategoryEdit} icon={CategoryIcon} /> - : null, - ]} -</Admin> -``` - -Note that the function returns an array of React elements. This is required to avoid having to wrap them in a container element which would prevent the `Admin` from working. - -**Tip**: Even if that's possible, be careful when completely excluding a resource (like with the `categories` resource in this example) as it will prevent you to reference this resource in the other resource views, too. - -## Restricting Access to Fields and Inputs - -You might want to display some fields or inputs only to users with specific permissions. By default, react-admin calls the `authProvider` for permissions for each resource routes, and passes them to the `list`, `edit`, `create`, and `show` components. - -Here is an example of a `Create` view with a conditional Input based on permissions: - -{% raw %} -```jsx -export const UserCreate = ({ permissions, ...props }) => - <Create {...props}> - <SimpleForm - defaultValue={{ role: 'user' }} - > - <TextInput source="name" validate={[required()]} /> - {permissions === 'admin' && - <TextInput source="role" validate={[required()]} />} - </SimpleForm> - </Create>; -``` -{% endraw %} - -This also works inside an `Edition` view with a `TabbedForm`, and you can even hide a `FormTab` completely: - -{% raw %} -```jsx -export const UserEdit = ({ permissions, ...props }) => - <Edit title={<UserTitle />} {...props}> - <TabbedForm defaultValue={{ role: 'user' }}> - <FormTab label="user.form.summary"> - {permissions === 'admin' && <TextInput disabled source="id" />} - <TextInput source="name" validate={required()} /> - </FormTab> - {permissions === 'admin' && - <FormTab label="user.form.security"> - <TextInput source="role" validate={required()} /> - </FormTab>} - </TabbedForm> - </Edit>; -``` -{% endraw %} - -What about the `List` view, the `Datagrid`, `SimpleList` and `Filter` components? It works there, too. And in the next example, the `permissions` prop is passed down to a custom `filters` component. - -```jsx -const UserFilter = ({ permissions, ...props }) => - <Filter {...props}> - <TextInput - label="user.list.search" - source="q" - alwaysOn - /> - <TextInput source="name" /> - {permissions === 'admin' && <TextInput source="role" />} - </Filter>; - -export const UserList = ({ permissions, ...props }) => - <List - {...props} - filters={props => <UserFilter permissions={permissions} {...props} />} - > - <Datagrid> - <TextField source="id" /> - <TextField source="name" /> - {permissions === 'admin' && <TextField source="role" />} - {permissions === 'admin' && <EditButton />} - <ShowButton /> - </Datagrid> - </List>; -``` - -## Restricting Access to the Dashboard - -React-admin injects the permissions into the component provided as a [`dashboard`](./Admin.md#dashboard), too: - -```jsx -// in src/Dashboard.js -import * as React from "react"; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import { Title } from 'react-admin'; - -export default ({ permissions }) => ( - <Card> - <Title title="Dashboard" /> - <CardContent>Lorem ipsum sic dolor amet...</CardContent> - {permissions === 'admin' - ? <CardContent>Sensitive data</CardContent> - : null - } - </Card> -); -``` - -## `usePermissions()` Hook - -You might want to check user permissions inside a [custom page](./Admin.md#customroutes). That's the purpose of the `usePermissions()` hook, which calls the `authProvider.getPermissions()` method on mount, and returns the result when available: - -```jsx -// in src/MyPage.js -import * as React from "react"; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import { usePermissions } from 'react-admin'; - -const MyPage = () => { - const { permissions } = usePermissions(); - return ( - <Card> - <CardContent>Lorem ipsum sic dolor amet...</CardContent> - {permissions === 'admin' && - <CardContent>Sensitive data</CardContent> - } - </Card> - ); -} - -export default MyPage; - -// in src/customRoutes.js -import * as React from "react"; -import { Route } from 'react-router-dom'; -import MyPage from './MyPage'; - -export default [ - <Route exact path="/baz" component={MyPage} />, -]; -``` - -The `usePermissions` hook is optimistic: it doesn't block rendering during the `authProvider` call. In the above example, the `MyPage` component renders even before getting the response from the `authProvider`. To avoid a blink in the interface while the `authProvider` is answering, use the `loaded` return value of `usePermissions()`: - -```jsx -const MyPage = () => { - const { loaded, permissions } = usePermissions(); - return loaded ? ( - <Card> - <CardContent>Lorem ipsum sic dolor amet...</CardContent> - {permissions === 'admin' && - <CardContent>Sensitive data</CardContent> - } - </Card> - ) : null; -} -``` - -## Restricting Access to a Menu - -What if you want to check the permissions inside a [custom menu](./Admin.md#menu)? Much like getting permissions inside a custom page, you'll have to use the `usePermissions` hook: - -```jsx -// in src/myMenu.js -import * as React from "react"; -import { MenuItemLink, usePermissions } from 'react-admin'; - -const Menu = ({ onMenuClick, logout }) => { - const { permissions } = usePermissions(); - return ( - <div> - <MenuItemLink to="/posts" primaryText="Posts" onClick={onMenuClick} /> - <MenuItemLink to="/comments" primaryText="Comments" onClick={onMenuClick} /> - {permissions === 'admin' && - <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} /> - } - {logout} - </div> - ); -} -``` diff --git a/docs/Ecosystem.md b/docs/Ecosystem.md index ab1ad63b927..f2666e13c9d 100644 --- a/docs/Ecosystem.md +++ b/docs/Ecosystem.md @@ -33,11 +33,7 @@ See the [Translation](./Translation.md#available-locales) page. ## Authentication Providers -- **[AWS Amplify](https://docs.amplify.aws)**: [MrHertal/react-admin-amplify](https://github.com/MrHertal/react-admin-amplify) -- **[AWS Cognito](https://docs.aws.amazon.com/cognito/latest/developerguide/setting-up-the-javascript-sdk.html)**: [thedistance/ra-cognito](https://github.com/thedistance/ra-cognito) -- **[Firebase Auth (Google, Facebook, Github etc)](https://firebase.google.com/docs/auth/web/firebaseui)**: [benwinding/react-admin-firebase](https://github.com/benwinding/react-admin-firebase#auth-provider) - -## Authorization Management +See the [Auth Provider](./Authentication.md#available-providers) page. Here is a list of additional packages: - **[Access Control List (ACL) for Resources](https://github.com/marmelab/ra-auth-acl)**: [marmelab/ra-auth-acl](https://github.com/marmelab/ra-auth-acl) diff --git a/docs/Reference.md b/docs/Reference.md index ab3178927a4..f83bbdc4c10 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -148,13 +148,14 @@ title: "Reference" * [`useDeleteMany`](./Actions.md#usedeletemany) * [`useEditController`](./CreateEdit.md#useeditcontroller) * `useFilterState` +* [`useGetIdentity`](./Authentication.md#usegetidentity-hook) * [`useGetList`](./Actions.md#usegetlist) * [`useGetMany`](./Actions.md#usegetmany) * [`useGetManyReference`](./Actions.md#usegetmanyreference) * `useGetMatching` * `useGetMatchingReferences` * [`useGetOne`](./Actions.md#usegetone) -* `useGetPermissions` +* [`useGetPermissions`](./Authentication.md#usegetpermissions-hook) * [`useHasLock`](https://marmelab.com/ra-enterprise/modules/ra-realtime#locks-on-content)<img class="icon" src="./img/premium.svg" /> * [`useHasLocks`](https://marmelab.com/ra-enterprise/modules/ra-realtime#locks-on-content)<img class="icon" src="./img/premium.svg" /> * `useInput` @@ -163,14 +164,14 @@ title: "Reference" * `useLoading` * [`useLocale`](./Translation.md#uselocale-getting-the-current-locale) * [`useLock`](https://marmelab.com/ra-enterprise/modules/ra-realtime#locks-on-content)<img class="icon" src="./img/premium.svg" /> -* [`useLogin`](./Authentication.md#customizing-the-login-and-logout-components) -* [`useLogout`](./Authentication.md#customizing-the-login-and-logout-components) +* [`useLogin`](./Authentication.md#uselogin-hook) +* [`useLogout`](./Authentication.md#uselogout-hook) * `useLogoutIfAccessDenied` * [`useMediaQuery`](./Theming.md#usemediaquery-hook) * [`useMutation`](./Actions.md#usemutation-hook) * [`useNotify`](./Actions.md#handling-side-effects-in-usedataprovider) * `usePaginationState` -* [`usePermissions`](./Authorization.md#usepermissions-hook) +* [`usePermissions`](./Authentication.md#usepermissions-hook) * [`usePreferences`](https://marmelab.com/ra-enterprise/modules/ra-preferences#usepreferences-reading-and-writing-user-preferences)<img class="icon" src="./img/premium.svg" /> * [`useQuery`](./Actions.md#usequery-hook) * [`useQueryWithStore`](./Actions.md#usequerywithstore-hook) @@ -197,6 +198,6 @@ title: "Reference" * `useVersion` * [`withDataProvider`](./Actions.md#legacy-components-query-mutation-and-withdataprovider) * [`withTranslate`](./Translation.md#withtranslate-hoc) -* [`<WithPermissions>`](./Authorization.md#usepermissions-hook) +* [`<WithPermissions>`](./Authentication.md#usepermissions-hook) </div> diff --git a/docs/UnitTesting.md b/docs/UnitTesting.md index 11774ef0596..32084cb5966 100644 --- a/docs/UnitTesting.md +++ b/docs/UnitTesting.md @@ -89,7 +89,7 @@ it('should send the user to another url', () => { ## Testing Permissions -As explained on the [Authorization page](./Authorization.md), it's possible to manage permissions via the authentication provider in order to filter page and fields the users can see. +As explained on the [Auth Provider chapter](./Authentication.md#authorization), it's possible to manage permissions via the `authProvider` in order to filter page and fields the users can see. In order to avoid regressions and make the design explicit to your co-workers, it's better to unit test which fields are supposed to be displayed or hidden for each permission. diff --git a/docs/navigation.html b/docs/navigation.html index 871ebaf2c7b..9c81923f1d3 100644 --- a/docs/navigation.html +++ b/docs/navigation.html @@ -2,12 +2,11 @@ <li {% if page.path contains 'Tutorial.md' %} class="active" {% endif %}><a href="./Tutorial.html">Tutorial</a></li> <ul><div>App Configuration</div> - <li {% if page.path contains 'DataProviders.md' %} class="active" {% endif %}><a href="./DataProviders.html">Data Providers</a></li> <li {% if page.path contains 'Admin.md' %} class="active" {% endif %}><a href="./Admin.html"><code><Admin></code></a></li> - <li {% if page.path contains 'Resource.md' %} class="active" {% endif %}><a href="./Resource.html"><code><Resource></code></a></li> - <li {% if page.path contains 'Authentication.md' %} class="active" {% endif %}><a href="./Authentication.html">Authentication</a></li> - <li {% if page.path contains 'Authorization.md' %} class="active" {% endif %}><a href="./Authorization.html">Authorization</a></li> + <li {% if page.path contains 'DataProviders.md' %} class="active" {% endif %}><a href="./DataProviders.html">Data Providers</a></li> + <li {% if page.path contains 'Authentication.md' %} class="active" {% endif %}><a href="./Authentication.html">Auth Providers</a></li> <li {% if page.path contains 'Translation.md' %} class="active" {% endif %}><a href="./Translation.html">Translation & i18n</a></li> + <li {% if page.path contains 'Resource.md' %} class="active" {% endif %}><a href="./Resource.html"><code><Resource></code></a></li> </ul> <ul><div>View Configuration</div> From 9f2dc9a6058297f4223c5dfe6e0bade9f91849ce Mon Sep 17 00:00:00 2001 From: Francois Zaninotto <francois@marmelab.com> Date: Fri, 6 Nov 2020 11:43:13 +0100 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com> --- docs/Authentication.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Authentication.md b/docs/Authentication.md index fd70014be5d..8d632df3273 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -70,9 +70,9 @@ Once an admin has an `authProvider`, react-admin enables a new page on the `/log ![Default Login Form](./img/login-form.png) -Upon submission, this form calls the `authProvider.login({ login, password })` method. React-admin expects this method to return a resolved Promise if the credentials are correct, and to a rejected Promise if they're not. +Upon submission, this form calls the `authProvider.login({ login, password })` method. React-admin expects this method to return a resolved Promise if the credentials are correct, and a rejected Promise if they're not. -For instance, to query an authentication route via HTTPS and store the credentials (a token) in local storage, configure `authProvider` as follows: +For instance, to query an authentication route via HTTPS and store the credentials (a token) in local storage, configure the `authProvider` as follows: ```js // in src/authProvider.js @@ -470,7 +470,7 @@ const MyPage = () => { Here is the interface react-admin expect `authProvider` objects to implement. -**Tip**: If you're a TypeScript user, you can check that your `authProvider` is correct at compile-tiome using the `AuthProvider` type: +**Tip**: If you're a TypeScript user, you can check that your `authProvider` is correct at compile-time using the `AuthProvider` type: ```jsx import { AuthProvider } from 'react-admin'; @@ -495,7 +495,7 @@ React-admin calls the `authProvider` methods with the following params: | ---------------- | ----------------------------------------------- | ------------------ | | `login` | Log a user in | `Object` whatever fields the login form contains | | `checkError` | Check if a dataProvider error is an authentication error | `{ message: string, status: number, body: Object }` the error returned by the `dataProvider` | -| `checkAuth` | Check credentials before moving to a new route | `Object` whatever params passed to `useCheckAuth()` - void for react-admin default routes | +| `checkAuth` | Check credentials before moving to a new route | `Object` whatever params passed to `useCheckAuth()` - nothing for react-admin default routes | | `logout` | Log a user out | `void` | | `getIdentity` | Get the current user identity | `void` | | `getPermissions` | Get the current user credentials | `Object` whatever params passed to `usePermissions()` - void for react-admin default routes | From 1c60b16492d35791256f26e2ce123d849ee2e141 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Fri, 6 Nov 2020 11:47:35 +0100 Subject: [PATCH 3/4] Review --- docs/Authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Authentication.md b/docs/Authentication.md index 8d632df3273..336f4c98517 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -495,10 +495,10 @@ React-admin calls the `authProvider` methods with the following params: | ---------------- | ----------------------------------------------- | ------------------ | | `login` | Log a user in | `Object` whatever fields the login form contains | | `checkError` | Check if a dataProvider error is an authentication error | `{ message: string, status: number, body: Object }` the error returned by the `dataProvider` | -| `checkAuth` | Check credentials before moving to a new route | `Object` whatever params passed to `useCheckAuth()` - nothing for react-admin default routes | +| `checkAuth` | Check credentials before moving to a new route | `Object` whatever params passed to `useCheckAuth()` - empty for react-admin default routes | | `logout` | Log a user out | `void` | | `getIdentity` | Get the current user identity | `void` | -| `getPermissions` | Get the current user credentials | `Object` whatever params passed to `usePermissions()` - void for react-admin default routes | +| `getPermissions` | Get the current user credentials | `Object` whatever params passed to `usePermissions()` - empty for react-admin default routes | ### Response Format From fcc8c2a6f8b37feb33379e351454c93176d8f5d4 Mon Sep 17 00:00:00 2001 From: fzaninotto <fzaninotto@gmail.com> Date: Fri, 6 Nov 2020 12:08:18 +0100 Subject: [PATCH 4/4] Review --- docs/Authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Authentication.md b/docs/Authentication.md index 336f4c98517..9908104a4f4 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -496,8 +496,8 @@ React-admin calls the `authProvider` methods with the following params: | `login` | Log a user in | `Object` whatever fields the login form contains | | `checkError` | Check if a dataProvider error is an authentication error | `{ message: string, status: number, body: Object }` the error returned by the `dataProvider` | | `checkAuth` | Check credentials before moving to a new route | `Object` whatever params passed to `useCheckAuth()` - empty for react-admin default routes | -| `logout` | Log a user out | `void` | -| `getIdentity` | Get the current user identity | `void` | +| `logout` | Log a user out | | +| `getIdentity` | Get the current user identity | | | `getPermissions` | Get the current user credentials | `Object` whatever params passed to `usePermissions()` - empty for react-admin default routes | ### Response Format