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

Remove permissions prop injection from main views #6921

Merged
merged 5 commits into from
Dec 6, 2021
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
11 changes: 11 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,17 @@ test('MyComponent', () => {
});
```

## `useAuthenticated` Signature hsa Changed

`useAuthenticated` uses to accept only the parameters passed to the `authProvider.checkAuth` function. It now accept an option object with two properties:
- `enabled`: whether it should check for an authenticated user
- `params`: the parameters to pass to `checkAuth`

```diff
- useAuthenticated('permissions.posts.can_create');
+ useAuthenticated({ params: 'permissions.posts.can_create' })
```

# Upgrade to 3.0

We took advantage of the major release to fix all the problems in react-admin that required a breaking change. As a consequence, you'll need to do many small changes in the code of existing react-admin v2 applications. Follow this step-by-step guide to upgrade to react-admin v3.
Expand Down
36 changes: 34 additions & 2 deletions docs/Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -701,11 +701,15 @@ const MyPage = () => {
export default MyPage;
```

If you call `useAuthenticated()` with a parameter, this parameter is passed to the `authProvider` call as second parameter. that allows you to add authentication logic depending on the context of the call:
`useAuthenticated` accepts an options object as its only argument, with the following properties:
- `enabled`: whether it should check for an authenticated user (`true` by default)
- `params`: the parameters to pass to `checkAuth`

If you call `useAuthenticated()` with a `params` option, those parameters are passed to the `authProvider.checkAuth` call. That allows you to add authentication logic depending on the context of the call:

```jsx
const MyPage = () => {
useAuthenticated({ foo: 'bar' }); // calls authProvider.checkAuth({ foo: 'bar' })
useAuthenticated({ params: foo: 'bar' } }); // calls authProvider.checkAuth({ foo: 'bar' })
return (
<div>
...
Expand Down Expand Up @@ -1165,3 +1169,31 @@ const Menu = ({ onMenuClick, logout }) => {
);
}
```

### Allowing Anonymous Access to Custom Views

You might have custom views that leverage react-admin components or hooks such as:

- `Create`, `CreateBase` `CreateController` and `useCreateController`
- `Edit`, `EditBase`, `EditController` and `useEditController`
- `List`, `ListBase`, `ListController` and `useListController`
- `Show`, `ShowBase`, `ShowController` and `useShowController`

By default, they all redirect anonymous users to the login page. You can disable this behavior by passing the `disableAuthentication` boolean prop:

```jsx
const MostRecentComments = () => {
const { data, loaded } = useListController({
disableAuthentication: true,
resource: 'comments',
sort: { field: 'created_at', order: 'desc' },
perPage: 10
});

if (!loaded) {
return null;
}

return <CommentsList comments={data} />
}
```
54 changes: 29 additions & 25 deletions examples/simple/src/users/UserCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TextInput,
Toolbar,
required,
usePermissions,
} from 'react-admin';

import Aside from './Aside';
Expand Down Expand Up @@ -38,32 +39,35 @@ const isValidName = async value =>
)
);

const UserCreate = ({ permissions, ...props }) => (
<Create {...props} aside={<Aside />}>
<TabbedForm toolbar={<UserEditToolbar permissions={permissions} />}>
<FormTab label="user.form.summary" path="">
<TextInput
source="name"
defaultValue="Slim Shady"
autoFocus
validate={[required(), isValidName]}
/>
</FormTab>
{permissions === 'admin' && (
<FormTab label="user.form.security" path="security">
<AutocompleteInput
source="role"
choices={[
{ id: '', name: 'None' },
{ id: 'admin', name: 'Admin' },
{ id: 'user', name: 'User' },
{ id: 'user_simple', name: 'UserSimple' },
]}
const UserCreate = () => {
const { permissions } = usePermissions();
return (
<Create aside={<Aside />}>
<TabbedForm toolbar={<UserEditToolbar permissions={permissions} />}>
<FormTab label="user.form.summary" path="">
<TextInput
source="name"
defaultValue="Slim Shady"
autoFocus
validate={[required(), isValidName]}
/>
</FormTab>
)}
</TabbedForm>
</Create>
);
{permissions === 'admin' && (
<FormTab label="user.form.security" path="security">
<AutocompleteInput
source="role"
choices={[
{ id: '', name: 'None' },
{ id: 'admin', name: 'Admin' },
{ id: 'user', name: 'User' },
{ id: 'user_simple', name: 'UserSimple' },
]}
/>
</FormTab>
)}
</TabbedForm>
</Create>
);
};

export default UserCreate;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.0",
"eslint-plugin-react-hooks": "^4.3.0",
"express": "~4.16.3",
"full-icu": "~1.3.1",
"husky": "^2.3.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/ra-core/src/auth/Authenticated.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cloneElement, ReactElement } from 'react';

import useAuthenticated from './useAuthenticated';
import { useAuthenticated } from './useAuthenticated';

export interface AuthenticatedProps {
children: ReactElement<any>;
Expand Down Expand Up @@ -43,7 +43,7 @@ const Authenticated = (props: AuthenticatedProps) => {
location, // kept for backwards compatibility, unused
...rest
} = props;
useAuthenticated(authParams);
useAuthenticated({ params: authParams });
// render the child even though the useAuthenticated() call isn't finished (optimistic rendering)
// the above hook will log out if the authProvider doesn't validate that the user is authenticated
return cloneElement(children, rest);
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/auth/WithPermissions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Children, ReactElement, ComponentType, createElement } from 'react';
import { Location } from 'history';

import warning from '../util/warning';
import useAuthenticated from './useAuthenticated';
import { useAuthenticated } from './useAuthenticated';
import usePermissionsOptimized from './usePermissionsOptimized';

export interface WithPermissionsChildrenParams {
Expand Down
7 changes: 3 additions & 4 deletions packages/ra-core/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import useAuthProvider from './useAuthProvider';
import useAuthState from './useAuthState';
import usePermissions from './usePermissions';
import usePermissionsOptimized from './usePermissionsOptimized';
import useAuthenticated from './useAuthenticated';
import WithPermissions, { WithPermissionsProps } from './WithPermissions';
import useLogin from './useLogin';
import useLogout from './useLogout';
import useCheckAuth from './useCheckAuth';
import useGetIdentity from './useGetIdentity';
import useGetPermissions from './useGetPermissions';
import useLogoutIfAccessDenied from './useLogoutIfAccessDenied';
import convertLegacyAuthProvider from './convertLegacyAuthProvider';

export * from './types';
export * from './useAuthenticated';
export * from './useCheckAuth';

export {
AuthContext,
Expand All @@ -22,15 +23,13 @@ export {
// low-level hooks for calling a particular verb on the authProvider
useLogin,
useLogout,
useCheckAuth,
useGetIdentity,
useGetPermissions,
// hooks with state management
usePermissions,
usePermissionsOptimized,
useAuthState,
// hook with immediate effect
useAuthenticated,
useLogoutIfAccessDenied,
// components
Authenticated,
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/auth/useAuthState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from 'react';

import useCheckAuth from './useCheckAuth';
import { useCheckAuth } from './useCheckAuth';
import { useSafeSetState } from '../util/hooks';

interface State {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useEffect } from 'react';
import useCheckAuth from './useCheckAuth';

const emptyParams = {};
import { useCheckAuth } from './useCheckAuth';

/**
* Restrict access to authenticated users.
Expand All @@ -28,9 +26,21 @@ const emptyParams = {};
* </Admin>
* );
*/
export default (params: any = emptyParams) => {
export const useAuthenticated = <ParamsType = any>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a breaking change for users who used to pass params, and should be mentioned in the upgrade guide

options: UseAuthenticatedOptions<ParamsType> = {}
) => {
const { enabled = true, params = emptyParams } = options;
const checkAuth = useCheckAuth();
useEffect(() => {
checkAuth(params).catch(() => {});
}, [checkAuth, params]);
if (enabled) {
checkAuth(params).catch(() => {});
}
}, [checkAuth, enabled, params]);
};

export type UseAuthenticatedOptions<ParamsType> = {
enabled?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to have documentation of the new enabled prop

params?: ParamsType;
};

const emptyParams = {};
2 changes: 1 addition & 1 deletion packages/ra-core/src/auth/useCheckAuth.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
import expect from 'expect';
import { render, waitFor } from '@testing-library/react';

import useCheckAuth from './useCheckAuth';
import { useCheckAuth } from './useCheckAuth';
import AuthContext from './AuthContext';
import useLogout from './useLogout';
import useNotify from '../sideEffect/useNotify';
Expand Down
6 changes: 2 additions & 4 deletions packages/ra-core/src/auth/useCheckAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import useNotify from '../sideEffect/useNotify';
* return authenticated ? <Bar /> : <BarNotAuthenticated />;
* } // tip: use useAuthState() hook instead
*/
const useCheckAuth = (): CheckAuth => {
export const useCheckAuth = (): CheckAuth => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing export for type CheckAuth

const authProvider = useAuthProvider();
const notify = useNotify();
const logout = useLogout();
Expand Down Expand Up @@ -90,7 +90,7 @@ const checkAuthWithoutAuthProvider = () => Promise.resolve();
*
* @return {Promise} Resolved to the authProvider response if the user passes the check, or rejected with an error otherwise
*/
type CheckAuth = (
export type CheckAuth = (
params?: any,
logoutOnFailure?: boolean,
redirectTo?: string,
Expand All @@ -104,5 +104,3 @@ const getErrorMessage = (error, defaultMessage) =>
: typeof error === 'undefined' || !error.message
? defaultMessage
: error.message;

export default useCheckAuth;
14 changes: 1 addition & 13 deletions packages/ra-core/src/controller/create/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
import {
CreateControllerResult,
useCreateController,
} from './useCreateController';

export * from './CreateBase';
export * from './CreateContext';
export * from './CreateContextProvider';
export * from './CreateController';

export * from './useCreateContext';
export * from '../show/useShowContext';

// We don't want to export CreateProps and EditProps as they should
// not be used outside ra-core, since it would conflict with ra-ui-materialui types,
// hence the named imports/exports
export type { CreateControllerResult as CreateControllerProps };
export { useCreateController };
export * from './useCreateController';
47 changes: 18 additions & 29 deletions packages/ra-core/src/controller/create/useCreateController.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,49 @@
import React from 'react';
import expect from 'expect';
import { act } from '@testing-library/react';
import { Location } from 'history';

import { getRecord } from './useCreateController';
import { getRecordFromLocation } from './useCreateController';
import { CreateController } from './CreateController';
import { renderWithRedux } from 'ra-test';
import { DataProviderContext } from '../../dataProvider';
import { DataProvider } from '../../types';

describe('useCreateController', () => {
describe('getRecord', () => {
const location = {
describe('getRecordFromLocation', () => {
const location: Location = {
key: 'a_key',
pathname: '/foo',
search: undefined,
state: undefined,
hash: undefined,
};

it('should return an empty record by default', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you remove this one ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that's not the responsability of this function anymore

expect(getRecord(location, undefined)).toEqual({});
});

it('should return location state record when set', () => {
expect(
getRecord(
{
...location,
state: { record: { foo: 'bar' } },
},
undefined
)
getRecordFromLocation({
...location,
state: { record: { foo: 'bar' } },
})
).toEqual({ foo: 'bar' });
});

it('should return location search when set', () => {
expect(
getRecord(
{
...location,
search: '?source={"foo":"baz","array":["1","2"]}',
},
undefined
)
getRecordFromLocation({
...location,
search: '?source={"foo":"baz","array":["1","2"]}',
})
).toEqual({ foo: 'baz', array: ['1', '2'] });
});

it('should return location state record when both state and search are set', () => {
expect(
getRecord(
{
...location,
state: { record: { foo: 'bar' } },
search: '?foo=baz',
},
undefined
)
getRecordFromLocation({
...location,
state: { record: { foo: 'bar' } },
search: '?foo=baz',
})
).toEqual({ foo: 'bar' });
});
});
Expand Down
Loading