Skip to content

Commit

Permalink
Merge pull request #10257 from marmelab/optional-authprovider-getperm…
Browse files Browse the repository at this point in the history
…issions

Make authProvider.getPermissions optional
  • Loading branch information
fzaninotto authored Oct 4, 2024
2 parents ec56f9f + c53bc86 commit 86f1474
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 84 deletions.
16 changes: 8 additions & 8 deletions packages/ra-core/src/auth/useGetPermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { useCallback } from 'react';

import useAuthProvider from './useAuthProvider';

const getPermissionsWithoutProvider = () => Promise.resolve([]);

/**
* Get a callback for calling the authProvider.getPermissions() method.
*
Expand Down Expand Up @@ -38,13 +36,15 @@ const getPermissionsWithoutProvider = () => Promise.resolve([]);
const useGetPermissions = (): GetPermissions => {
const authProvider = useAuthProvider();
const getPermissions = useCallback(
(params: any = {}) =>
(params: any = {}) => {
// react-query requires the query to return something
authProvider
? authProvider
.getPermissions(params)
.then(result => result ?? null)
: getPermissionsWithoutProvider(),
if (authProvider && authProvider.getPermissions) {
return authProvider
.getPermissions(params)
.then(result => result ?? null);
}
return Promise.resolve([]);
},
[authProvider]
);

Expand Down
106 changes: 49 additions & 57 deletions packages/ra-core/src/auth/usePermissions.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
import * as React from 'react';
import expect from 'expect';
import { waitFor, render, screen } from '@testing-library/react';
import { CoreAdminContext } from '../core/CoreAdminContext';

import usePermissions from './usePermissions';
import { QueryClient } from '@tanstack/react-query';

const UsePermissions = ({ children }: any) => {
const permissionQueryParams = {
retry: false,
};
const res = usePermissions({}, permissionQueryParams);
return children(res);
};

const stateInpector = state => (
<div>
<span>{state.isPending && 'LOADING'}</span>
{state.permissions && <span>PERMISSIONS: {state.permissions}</span>}
<span>{state.error && 'ERROR'}</span>
</div>
);
import {
NoAuthProvider,
NoAuthProviderGetPermissions,
WithAuthProviderAndGetPermissions,
} from './usePermissions.stories';
import { AuthProvider } from '..';

describe('usePermissions', () => {
it('should return a loading state on mount', () => {
it('should return a loading state on mount with an authProvider that supports permissions', async () => {
let resolveGetPermissions;
const authProvider = {
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () => Promise.reject('bad method'),
checkError: () => Promise.reject('bad method'),
getPermissions: () => {
return new Promise(resolve => {
resolveGetPermissions = resolve;
});
},
};
render(
<CoreAdminContext>
<UsePermissions>{stateInpector}</UsePermissions>
</CoreAdminContext>
<WithAuthProviderAndGetPermissions authProvider={authProvider} />
);
expect(screen.queryByText('LOADING')).not.toBeNull();
expect(screen.queryByText('AUTHENTICATED')).toBeNull();
expect(screen.queryByText('PERMISSIONS: ')).toBeNull();
resolveGetPermissions('admin');
await screen.findByText('PERMISSIONS: admin');
});

it('should return nothing by default after a tick', async () => {
render(
<CoreAdminContext>
<UsePermissions>{stateInpector}</UsePermissions>
</CoreAdminContext>
);
await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
});
it('should return immediately without an authProvider', async () => {
render(<NoAuthProvider />);
expect(screen.queryByText('LOADING')).toBeNull();
expect(screen.queryByText('PERMISSIONS: ')).toBeNull();
});

it('should return immediately without an authProvider that supports permissions', async () => {
render(<NoAuthProviderGetPermissions />);
expect(screen.queryByText('LOADING')).toBeNull();
expect(screen.queryByText('PERMISSIONS: ')).toBeNull();
});

it('should return the permissions after a tick', async () => {
Expand All @@ -53,14 +53,10 @@ describe('usePermissions', () => {
getPermissions: () => Promise.resolve('admin'),
};
render(
<CoreAdminContext authProvider={authProvider}>
<UsePermissions>{stateInpector}</UsePermissions>
</CoreAdminContext>
<WithAuthProviderAndGetPermissions authProvider={authProvider} />
);
await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
expect(screen.queryByText('PERMISSIONS: admin')).not.toBeNull();
});
await screen.findByText('PERMISSIONS: admin');
expect(screen.queryByText('LOADING')).toBeNull();
});

it('should return an error after a tick if the auth.getPermissions call fails and checkError resolves', async () => {
Expand All @@ -72,14 +68,10 @@ describe('usePermissions', () => {
getPermissions: () => Promise.reject('not good'),
};
render(
<CoreAdminContext authProvider={authProvider}>
<UsePermissions>{stateInpector}</UsePermissions>
</CoreAdminContext>
<WithAuthProviderAndGetPermissions authProvider={authProvider} />
);
await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
expect(screen.queryByText('ERROR')).not.toBeNull();
});
await screen.findByText('ERROR');
expect(screen.queryByText('LOADING')).toBeNull();
});

it('should call logout when the auth.getPermissions call fails and checkError rejects', async () => {
Expand All @@ -91,9 +83,7 @@ describe('usePermissions', () => {
getPermissions: () => Promise.reject('not good'),
};
render(
<CoreAdminContext authProvider={authProvider}>
<UsePermissions>{stateInpector}</UsePermissions>
</CoreAdminContext>
<WithAuthProviderAndGetPermissions authProvider={authProvider} />
);
await waitFor(() => {
expect(screen.queryByText('LOADING')).toBeNull();
Expand All @@ -103,24 +93,26 @@ describe('usePermissions', () => {

it('should abort the request if the query is canceled', async () => {
const abort = jest.fn();
const authProvider = {
const authProvider: AuthProvider = {
login: () => Promise.reject('bad method'),
logout: jest.fn(() => Promise.resolve()),
checkAuth: () => Promise.reject('bad method'),
checkError: () => Promise.reject(),
getPermissions: jest.fn(
({ signal }) =>
new Promise(() => {
signal.addEventListener('abort', () => {
abort(signal.reason);
});
})
) as any,
} as any;
),
};
const queryClient = new QueryClient();
render(
<CoreAdminContext
<WithAuthProviderAndGetPermissions
authProvider={authProvider}
queryClient={queryClient}
>
<UsePermissions>{stateInpector}</UsePermissions>
</CoreAdminContext>
/>
);
await waitFor(() => {
expect(authProvider.getPermissions).toHaveBeenCalled();
Expand Down
74 changes: 74 additions & 0 deletions packages/ra-core/src/auth/usePermissions.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as React from 'react';
import usePermissions, { UsePermissionsResult } from './usePermissions';
import { QueryClient } from '@tanstack/react-query';
import { AuthProvider, CoreAdminContext, TestMemoryRouter } from '..';

export default {
title: 'ra-core/auth/usePermissions',
};

export const NoAuthProvider = () => (
<TestMemoryRouter>
<CoreAdminContext>
<UsePermissions>{state => inspectState(state)}</UsePermissions>
</CoreAdminContext>
</TestMemoryRouter>
);

export const NoAuthProviderGetPermissions = () => (
<TestMemoryRouter>
<CoreAdminContext
authProvider={{
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () => Promise.reject('bad method'),
checkError: () => Promise.reject('bad method'),
}}
>
<UsePermissions>{state => inspectState(state)}</UsePermissions>
</CoreAdminContext>
</TestMemoryRouter>
);

export const WithAuthProviderAndGetPermissions = ({
authProvider = {
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () => Promise.reject('bad method'),
checkError: () => Promise.reject('bad method'),
getPermissions: () =>
new Promise(resolve => setTimeout(resolve, 300, 'admin')),
},
queryClient,
}: {
authProvider?: AuthProvider;
queryClient?: QueryClient;
}) => (
<TestMemoryRouter>
<CoreAdminContext authProvider={authProvider} queryClient={queryClient}>
<UsePermissions>{state => inspectState(state)}</UsePermissions>
</CoreAdminContext>
</TestMemoryRouter>
);

const UsePermissions = ({
children,
}: {
children: (state: UsePermissionsResult<any, Error>) => React.ReactNode;
}) => {
const permissionQueryParams = {
retry: false,
};
const res = usePermissions({}, permissionQueryParams);
return children(res);
};

const inspectState = (state: UsePermissionsResult<any, Error>) => (
<div>
{state.isPending ? <span>LOADING</span> : null}
{state.permissions ? (
<span>PERMISSIONS: {state.permissions}</span>
) : null}
{state.error ? <span>ERROR</span> : null}
</div>
);
70 changes: 52 additions & 18 deletions packages/ra-core/src/auth/usePermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ const usePermissions = <PermissionsType = any, ErrorType = Error>(
const { onSuccess, onError, onSettled, ...queryOptions } =
queryParams ?? {};

const result = useQuery<PermissionsType, ErrorType>({
const queryResult = useQuery<PermissionsType, ErrorType>({
queryKey: ['auth', 'getPermissions', params],
queryFn: async ({ signal }) => {
if (!authProvider) return Promise.resolve([]);
if (!authProvider || !authProvider.getPermissions) {
return [];
}
const permissions = await authProvider.getPermissions({
...params,
signal,
Expand All @@ -77,33 +79,37 @@ const usePermissions = <PermissionsType = any, ErrorType = Error>(
);

useEffect(() => {
if (result.data === undefined || result.isFetching) return;
onSuccessEvent(result.data);
}, [onSuccessEvent, result.data, result.isFetching]);
if (queryResult.data === undefined || queryResult.isFetching) return;
onSuccessEvent(queryResult.data);
}, [onSuccessEvent, queryResult.data, queryResult.isFetching]);

useEffect(() => {
if (result.error == null || result.isFetching) return;
onErrorEvent(result.error);
}, [onErrorEvent, result.error, result.isFetching]);
if (queryResult.error == null || queryResult.isFetching) return;
onErrorEvent(queryResult.error);
}, [onErrorEvent, queryResult.error, queryResult.isFetching]);

useEffect(() => {
if (result.status === 'pending' || result.isFetching) return;
onSettledEvent(result.data, result.error);
if (queryResult.status === 'pending' || queryResult.isFetching) return;
onSettledEvent(queryResult.data, queryResult.error);
}, [
onSettledEvent,
result.data,
result.error,
result.status,
result.isFetching,
queryResult.data,
queryResult.error,
queryResult.status,
queryResult.isFetching,
]);

return useMemo(
const result = useMemo(
() => ({
...result,
permissions: result.data,
...queryResult,
permissions: queryResult.data,
}),
[result]
[queryResult]
);

return !authProvider || !authProvider.getPermissions
? (fakeQueryResult as UsePermissionsResult<PermissionsType, ErrorType>)
: result;
};

export default usePermissions;
Expand All @@ -126,3 +132,31 @@ export type UsePermissionsResult<
};

const noop = () => {};

const fakeQueryResult = {
permissions: undefined,
data: undefined,
dataUpdatedAt: 0,
error: null,
errorUpdatedAt: 0,
errorUpdateCount: 0,
failureCount: 0,
failureReason: null,
fetchStatus: 'idle',
isError: false,
isInitialLoading: false,
isLoading: false,
isLoadingError: false,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isPaused: false,
isPlaceholderData: false,
isPending: false,
isRefetchError: false,
isRefetching: false,
isStale: false,
isSuccess: true,
status: 'success',
refetch: () => Promise.resolve(fakeQueryResult),
};
2 changes: 1 addition & 1 deletion packages/ra-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export type AuthProvider = {
checkAuth: (params: any & QueryFunctionContext) => Promise<void>;
checkError: (error: any) => Promise<void>;
getIdentity?: (params?: QueryFunctionContext) => Promise<UserIdentity>;
getPermissions: (params: any & QueryFunctionContext) => Promise<any>;
getPermissions?: (params: any & QueryFunctionContext) => Promise<any>;
handleCallback?: (
params?: QueryFunctionContext
) => Promise<AuthRedirectResult | void | any>;
Expand Down

0 comments on commit 86f1474

Please sign in to comment.