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

Use react-query for useAuthState and useAuthenticated #8496

Merged
merged 2 commits into from
Dec 13, 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
26 changes: 11 additions & 15 deletions packages/ra-core/src/auth/useAuthState.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,21 @@ import { CoreAdminContext } from '../core/CoreAdminContext';

import useAuthState from './useAuthState';

const UseAuth = ({ children, authParams }: any) => {
const res = useAuthState(authParams);
return children(res);
const UseAuth = (authParams: any) => {
const state = useAuthState(authParams);
return (
<div>
<span>{state.isLoading && 'LOADING'}</span>
<span>{state.authenticated && 'AUTHENTICATED'}</span>
</div>
);
};

const stateInpector = state => (
<div>
<span>{state.isLoading && 'LOADING'}</span>
<span>{state.authenticated && 'AUTHENTICATED'}</span>
</div>
);

describe('useAuthState', () => {
it('should return a loading state on mount', () => {
render(
<CoreAdminContext>
<UseAuth>{stateInpector}</UseAuth>
<UseAuth />
</CoreAdminContext>
);
expect(screen.queryByText('LOADING')).not.toBeNull();
Expand All @@ -31,7 +29,7 @@ describe('useAuthState', () => {
it('should return authenticated by default after a tick', async () => {
render(
<CoreAdminContext>
<UseAuth>{stateInpector}</UseAuth>
<UseAuth />
</CoreAdminContext>
);
await waitFor(() => {
Expand All @@ -50,9 +48,7 @@ describe('useAuthState', () => {
};
render(
<CoreAdminContext authProvider={authProvider}>
<UseAuth options={{ logoutOnFailure: false }}>
{stateInpector}
</UseAuth>
<UseAuth options={{ logoutOnFailure: false }} />
</CoreAdminContext>
);
await waitFor(() => {
Expand Down
77 changes: 61 additions & 16 deletions packages/ra-core/src/auth/useAuthState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useEffect } from 'react';

import { useCheckAuth } from './useCheckAuth';
import { useSafeSetState } from '../util/hooks';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions } from 'react-query';
import useAuthProvider, { defaultAuthParams } from './useAuthProvider';
import useLogout from './useLogout';
import { removeDoubleSlashes, useBasename } from '../routing';
import { useNotify } from '../notification';

interface State {
isLoading: boolean;
Expand Down Expand Up @@ -49,19 +51,62 @@ const emptyParams = {};
*/
const useAuthState = (
params: any = emptyParams,
logoutOnFailure: boolean = false
logoutOnFailure: boolean = false,
queryOptions?: UseQueryOptions<boolean, any>
): State => {
const [state, setState] = useSafeSetState({
isLoading: true,
authenticated: true, // optimistic
});
const checkAuth = useCheckAuth();
useEffect(() => {
checkAuth(params, logoutOnFailure)
.then(() => setState({ isLoading: false, authenticated: true }))
.catch(() => setState({ isLoading: false, authenticated: false }));
}, [checkAuth, params, logoutOnFailure, setState]);
return state;
const authProvider = useAuthProvider();
const logout = useLogout();
const basename = useBasename();
const notify = useNotify();

const result = useQuery<boolean, any>(
['auth', 'checkAuth', params],
() => {
// The authProvider is optional in react-admin
return authProvider?.checkAuth(params).then(() => true);
},
{
onError: error => {
const loginUrl = removeDoubleSlashes(
`${basename}/${defaultAuthParams.loginUrl}`
);
if (logoutOnFailure) {
logout(
{},
error && error.redirectTo != null
? error.redirectTo
: loginUrl
);
const shouldSkipNotify = error && error.message === false;
!shouldSkipNotify &&
notify(
getErrorMessage(error, 'ra.auth.auth_check_error'),
{ type: 'warning' }
);
}
},
retry: false,
...queryOptions,
}
);

return useMemo(() => {
return {
// If the data is undefined and the query isn't loading anymore, it means the query failed.
// In that case, we set authenticated to false unless there's no authProvider.
authenticated:
result.data ?? result.isLoading ? true : authProvider == null, // Optimisic
isLoading: result.isLoading,
error: result.error,
};
}, [authProvider, result]);
};

export default useAuthState;

const getErrorMessage = (error, defaultMessage) =>
typeof error === 'string'
? error
: typeof error === 'undefined' || !error.message
? defaultMessage
: error.message;
43 changes: 28 additions & 15 deletions packages/ra-core/src/auth/useAuthenticated.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import { createMemoryHistory } from 'history';
import { Routes, Route, useLocation } from 'react-router-dom';

import { memoryStore } from '../store';
import { Authenticated } from './Authenticated';
import { useNotificationContext } from '../notification';
import { CoreAdminContext } from '../core';
import { useAuthenticated } from '.';

const Authenticated = ({ children, ...params }) => {
useAuthenticated({ params });
return children;
};

describe('useAuthenticated', () => {
const Foo = () => <div>Foo</div>;
Expand All @@ -27,6 +32,12 @@ describe('useAuthenticated', () => {
<Authenticated>
<Foo />
</Authenticated>
<Authenticated>
<Foo />
</Authenticated>
<Authenticated>
<Foo />
</Authenticated>
</CoreAdminContext>
);
expect(authProvider.checkAuth).toBeCalledTimes(1);
Expand All @@ -53,7 +64,7 @@ describe('useAuthenticated', () => {
</CoreAdminContext>
);
const { rerender } = render(<FooWrapper />);
rerender(<FooWrapper authParams={{ foo: 'bar' }} />);
rerender(<FooWrapper foo="bar" />);
expect(authProvider.checkAuth).toBeCalledTimes(2);
expect(authProvider.checkAuth.mock.calls[1][0]).toEqual({ foo: 'bar' });
expect(reset).toHaveBeenCalledTimes(0);
Expand Down Expand Up @@ -132,19 +143,21 @@ describe('useAuthenticated', () => {
</CoreAdminContext>
);
await waitFor(() => {
expect(authProvider.checkAuth.mock.calls[0][0]).toEqual({});
expect(authProvider.logout.mock.calls[0][0]).toEqual({});
expect(reset).toHaveBeenCalledTimes(1);
expect(notificationsSpy).toEqual([
{
message: 'ra.auth.auth_check_error',
type: 'warning',
notificationOptions: {},
},
]);
expect(screen.getByLabelText('nextPathname').innerHTML).toEqual(
'/'
);
expect(authProvider.checkAuth).toHaveBeenCalledTimes(1);
});
expect(authProvider.checkAuth.mock.calls[0][0]).toEqual({});
await waitFor(() => {
expect(authProvider.logout).toHaveBeenCalledTimes(1);
});
expect(authProvider.logout.mock.calls[0][0]).toEqual({});
expect(reset).toHaveBeenCalledTimes(1);
expect(notificationsSpy).toEqual([
{
message: 'ra.auth.auth_check_error',
type: 'warning',
notificationOptions: {},
},
]);
expect(screen.getByLabelText('nextPathname').innerHTML).toEqual('/');
});
});
25 changes: 11 additions & 14 deletions packages/ra-core/src/auth/useAuthenticated.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { useCheckAuth } from './useCheckAuth';
import { UseQueryOptions } from 'react-query';
import useAuthState from './useAuthState';

/**
* Restrict access to authenticated users.
Expand All @@ -26,20 +26,17 @@ import { useCheckAuth } from './useCheckAuth';
* </Admin>
* );
*/
export const useAuthenticated = <ParamsType = any>(
options: UseAuthenticatedOptions<ParamsType> = {}
) => {
const { enabled = true, params = emptyParams } = options;
const checkAuth = useCheckAuth();
useEffect(() => {
if (enabled) {
checkAuth(params).catch(() => {});
}
}, [checkAuth, enabled, params]);
export const useAuthenticated = <ParamsType = any>({
params,
...options
}: UseAuthenticatedOptions<ParamsType> = {}) => {
useAuthState(params ?? emptyParams, true, options);
};

export type UseAuthenticatedOptions<ParamsType> = {
enabled?: boolean;
export type UseAuthenticatedOptions<ParamsType> = UseQueryOptions<
boolean,
any
> & {
params?: ParamsType;
};

Expand Down