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

Added internal useConfig hook [SDK-2264] #262

Merged
merged 1 commit into from
Jan 29, 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
1 change: 1 addition & 0 deletions src/frontend/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as ConfigProvider, ConfigProviderProps, useConfig } from './use-config';
export { default as UserProvider, UserProviderProps, UserProfile, UserContext, useUser } from './use-user';
export { default as withPageAuthRequired, WithPageAuthRequired } from './with-page-auth-required';
18 changes: 18 additions & 0 deletions src/frontend/use-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { ReactElement, useContext, createContext } from 'react';

export type ConfigContext = {
loginUrl?: string;
};

const Config = createContext<ConfigContext>({});

export type ConfigProviderProps = React.PropsWithChildren<ConfigContext>;
export type UseConfig = () => ConfigContext;
export const useConfig: UseConfig = () => useContext<ConfigContext>(Config);

export default ({
children,
loginUrl = process.env.NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login'
}: ConfigProviderProps): ReactElement<ConfigContext> => {
return <Config.Provider value={{ loginUrl }}>{children}</Config.Provider>;
};
46 changes: 31 additions & 15 deletions src/frontend/use-user.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { ReactElement, useState, useEffect, useCallback, useContext, createContext } from 'react';

import ConfigProvider, { ConfigContext } from './use-config';

/**
* The user claims returned from the {@link useUser} hook.
*
Expand All @@ -21,12 +23,12 @@ export interface UserProfile {
*
* @category Client
*/
export interface UserContext {
export type UserContext = {
user?: UserProfile;
error?: Error;
isLoading: boolean;
checkSession: () => Promise<void>;
}
};

/**
* Configure the {@link UserProvider} component.
Expand Down Expand Up @@ -59,7 +61,7 @@ export interface UserContext {
*
* @category Client
*/
export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string }>;
export type UserProviderProps = React.PropsWithChildren<{ user?: UserProfile; profileUrl?: string } & ConfigContext>;

/**
* @ignore
Expand Down Expand Up @@ -119,33 +121,47 @@ export const useUser: UseUser = () => useContext<UserContext>(User);
*/
export type UserProvider = (props: UserProviderProps) => ReactElement<UserContext>;

/**
* @ignore
*/
type UserProviderState = {
user?: UserProfile;
error?: Error;
isLoading: boolean;
};

export default ({
children,
user: initialUser,
profileUrl = '/api/auth/me'
profileUrl = process.env.NEXT_PUBLIC_AUTH0_PROFILE || '/api/auth/me',
loginUrl
}: UserProviderProps): ReactElement<UserContext> => {
const [user, setUser] = useState<UserProfile | undefined>(() => initialUser);
const [error, setError] = useState<Error | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(() => !initialUser);
const [state, setState] = useState<UserProviderState>({ user: initialUser, isLoading: !initialUser });

const checkSession = useCallback(async (): Promise<void> => {
try {
const response = await fetch(profileUrl);
setUser(response.ok ? await response.json() : undefined);
setError(undefined);
const user = response.ok ? await response.json() : undefined;
setState((previous) => ({ ...previous, user, error: undefined }));
} catch (_e) {
setUser(undefined);
setError(new Error(`The request to ${profileUrl} failed`));
const error = new Error(`The request to ${profileUrl} failed`);
setState((previous) => ({ ...previous, user: undefined, error }));
}
}, [profileUrl]);

useEffect((): void => {
if (user) return;
if (state.user) return;
(async (): Promise<void> => {
await checkSession();
setIsLoading(false);
setState((previous) => ({ ...previous, isLoading: false }));
})();
}, [user]);
}, [state.user]);

const { user, error, isLoading } = state;

return <User.Provider value={{ user, error, isLoading, checkSession }}>{children}</User.Provider>;
return (
<ConfigProvider loginUrl={loginUrl}>
<User.Provider value={{ user, error, isLoading, checkSession }}>{children}</User.Provider>
</ConfigProvider>
);
};
18 changes: 3 additions & 15 deletions src/frontend/with-page-auth-required.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { ComponentType, useEffect } from 'react';
import { useRouter } from 'next/router';

import { useConfig } from './use-config';
import { useUser } from './use-user';

/**
Expand Down Expand Up @@ -29,15 +30,6 @@ export interface WithPageAuthRequiredOptions {
* Add a path to return the user to after login.
*/
returnTo?: string;
/**
* ```js
* withPageAuthRequired(Profile, {
* loginUrl: '/api/login'
* });
* ```
* The path of your custom login API route.
*/
loginUrl?: string;
/**
* ```js
* withPageAuthRequired(Profile, {
Expand Down Expand Up @@ -81,12 +73,8 @@ export type WithPageAuthRequired = <P extends object>(
const withPageAuthRequired: WithPageAuthRequired = (Component, options = {}) => {
return function withPageAuthRequired(props): JSX.Element {
const router = useRouter();
const {
returnTo = router.asPath,
onRedirecting = defaultOnRedirecting,
onError = defaultOnError,
loginUrl = '/api/auth/login'
} = options;
const { returnTo = router.asPath, onRedirecting = defaultOnRedirecting, onError = defaultOnError } = options;
const { loginUrl } = useConfig();
const { user, error, isLoading } = useUser();

useEffect(() => {
Expand Down
12 changes: 10 additions & 2 deletions tests/fixtures/frontend.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';

import { UserProvider, UserProviderProps, UserProfile } from '../../src';
import { ConfigProvider, ConfigProviderProps } from '../../src/frontend';

type FetchUserMock = {
ok: boolean;
Expand All @@ -16,8 +18,10 @@ export const user: UserProfile = {
updated_at: null
};

export const withUserProvider = ({ user, profileUrl }: UserProviderProps = {}): React.ComponentType => {
return (props: any): React.ReactElement => <UserProvider {...props} user={user} profileUrl={profileUrl} />;
export const withUserProvider = ({ user, profileUrl, loginUrl }: UserProviderProps = {}): React.ComponentType => {
return (props: any): React.ReactElement => (
<UserProvider {...props} user={user} profileUrl={profileUrl} loginUrl={loginUrl} />
);
};

export const fetchUserMock = (): Promise<FetchUserMock> => {
Expand All @@ -34,3 +38,7 @@ export const fetchUserUnsuccessfulMock = (): Promise<FetchUserMock> => {
};

export const fetchUserErrorMock = (): Promise<FetchUserMock> => Promise.reject(new Error('Error'));

export const withConfigProvider = ({ loginUrl }: ConfigProviderProps = {}): React.ComponentType => {
return (props: any): React.ReactElement => <ConfigProvider {...props} loginUrl={loginUrl} />;
};
36 changes: 36 additions & 0 deletions tests/frontend/use-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { renderHook } from '@testing-library/react-hooks';

import { withConfigProvider } from '../fixtures/frontend';
import { useConfig } from '../../src/frontend/use-config';

jest.mock('next/router', () => ({
useRouter: (): any => ({ asPath: '/' })
}));

describe('context wrapper', () => {
test('should provide the default login url', async () => {
const { result } = renderHook(() => useConfig(), {
wrapper: withConfigProvider({ loginUrl: '/api/auth/login' })
});

expect(result.current.loginUrl).toEqual('/api/auth/login');
});

test('should provide a custom login url', async () => {
const { result } = renderHook(() => useConfig(), {
wrapper: withConfigProvider({ loginUrl: '/api/custom-url' })
});

expect(result.current.loginUrl).toEqual('/api/custom-url');
});

test('should provide a custom login url from an environment variable', async () => {
process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/custom-url';
const { result } = renderHook(() => useConfig(), {
wrapper: withConfigProvider()
});

expect(result.current.loginUrl).toEqual('/api/custom-url');
delete process.env.NEXT_PUBLIC_AUTH0_LOGIN;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import {
withUserProvider,
user
} from '../fixtures/frontend';
import { useConfig } from '../../src/frontend';
import { useUser } from '../../src';

jest.mock('next/router', () => ({
useRouter: (): any => ({ asPath: '/' })
}));

describe('context wrapper', () => {
afterEach(() => delete (global as any).fetch);

Expand Down Expand Up @@ -57,15 +62,26 @@ describe('context wrapper', () => {
expect(result.current.isLoading).toEqual(false);
});

test('should use the existing user', async () => {
test('should provide the existing user', async () => {
const { result } = renderHook(() => useUser(), { wrapper: withUserProvider({ user }) });

expect(result.current.user).toEqual(user);
expect(result.current.error).toBeUndefined();
expect(result.current.isLoading).toEqual(false);
});

test('should use a custom profileUrl', async () => {
test('should use the default profile url', async () => {
const fetchSpy = jest.fn().mockReturnValue(Promise.resolve());
(global as any).fetch = fetchSpy;
const { result, waitForValueToChange } = renderHook(() => useUser(), {
wrapper: withUserProvider()
});

await waitForValueToChange(() => result.current.isLoading);
expect(fetchSpy).toHaveBeenCalledWith('/api/auth/me');
});

test('should accept a custom profile url', async () => {
const fetchSpy = jest.fn().mockReturnValue(Promise.resolve());
(global as any).fetch = fetchSpy;
const { result, waitForValueToChange } = renderHook(() => useUser(), {
Expand All @@ -76,12 +92,33 @@ describe('context wrapper', () => {
expect(fetchSpy).toHaveBeenCalledWith('/api/custom-url');
});

test('should use a custom profile url from an environment variable', async () => {
process.env.NEXT_PUBLIC_AUTH0_PROFILE = '/api/custom-url';
const fetchSpy = jest.fn().mockReturnValue(Promise.resolve());
(global as any).fetch = fetchSpy;
const { result, waitForValueToChange } = renderHook(() => useUser(), {
wrapper: withUserProvider()
});

await waitForValueToChange(() => result.current.isLoading);
expect(fetchSpy).toHaveBeenCalledWith('/api/custom-url');
delete process.env.NEXT_PUBLIC_AUTH0_PROFILE;
});

test('should accept a custom login url', async () => {
const { result } = renderHook(() => useConfig(), {
wrapper: withUserProvider({ user, loginUrl: '/api/custom-url' })
});

expect(result.current.loginUrl).toEqual('/api/custom-url');
});

test('should check the session when logged in', async () => {
(global as any).fetch = fetchUserUnsuccessfulMock;
const { result, waitForValueToChange } = renderHook(() => useUser(), { wrapper: withUserProvider() });

await waitForValueToChange(() => result.current.isLoading);
expect(result.current.user).toBeUndefined;
expect(result.current.user).toBeUndefined();

(global as any).fetch = fetchUserMock;

Expand Down
13 changes: 12 additions & 1 deletion tests/frontend/with-page-auth-required.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jest.mock('next/router', () => ({ useRouter: (): any => routerMock }));
describe('with-page-auth-required csr', () => {
afterEach(() => delete (global as any).fetch);

it('should block access to a CSR page when not authenticated', async () => {
it('should deny access to a CSR page when not authenticated', async () => {
(global as any).fetch = fetchUserUnsuccessfulMock;
const MyPage = (): JSX.Element => <>Private</>;
const ProtectedPage = withPageAuthRequired(MyPage);
Expand Down Expand Up @@ -83,4 +83,15 @@ describe('with-page-auth-required csr', () => {
render(<ProtectedPage />, { wrapper: withUserProvider() });
await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('?returnTo=/foo')));
});

it('should use a custom login url', async () => {
process.env.NEXT_PUBLIC_AUTH0_LOGIN = '/api/foo';
(global as any).fetch = fetchUserUnsuccessfulMock;
const MyPage = (): JSX.Element => <>Private</>;
const ProtectedPage = withPageAuthRequired(MyPage);

render(<ProtectedPage />, { wrapper: withUserProvider() });
await waitFor(() => expect(routerMock.push).toHaveBeenCalledWith(expect.stringContaining('/api/foo')));
delete process.env.NEXT_PUBLIC_AUTH0_LOGIN;
});
});
1 change: 1 addition & 0 deletions typedoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = {
exclude: [
'./src/auth0-session/**',
'./src/session/cache.ts',
'./src/frontend/use-config.tsx',
'./src/utils/!(errors.ts)',
'./src/index.ts',
'./src/index.browser.ts'
Expand Down