Skip to content

Commit

Permalink
improvement: Add support for react-router Outlet
Browse files Browse the repository at this point in the history
Added support for Outlet in route guards to be able to use them in
nested routes without specifying children.
  • Loading branch information
Gido Manders committed Dec 6, 2024
1 parent d26a9c1 commit 56891b0
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 60 deletions.
12 changes: 4 additions & 8 deletions src/routeguards/IsAuthenticated.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { ReactElement, ReactNode } from 'react';
import { Navigate, useLocation } from 'react-router';
import { PropsWithChildren } from 'react';
import { Navigate, Outlet, useLocation } from 'react-router';

import { Config, getConfig } from '../config';
import { useAuthentication } from '../hooks';

type Props = {
children: ReactNode;
};

/**
* Works just like a regular Route except for when the user is
* not logged in. If the user is not logged in it will Redirect
Expand All @@ -27,7 +23,7 @@ type Props = {
*
* @returns Either the Component or a Redirect
*/
export function IsAuthenticated({ children }: Props): ReactElement {
export function IsAuthenticated({ children }: PropsWithChildren) {
const config: Config = getConfig();
const location = useLocation();
const authentication = useAuthentication();
Expand All @@ -41,5 +37,5 @@ export function IsAuthenticated({ children }: Props): ReactElement {
);
}

return <>{children}</>;
return children ?? <Outlet />;
}
9 changes: 4 additions & 5 deletions src/routeguards/IsAuthorized.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactElement, ReactNode } from 'react';
import { Navigate, useLocation } from 'react-router';
import { PropsWithChildren } from 'react';
import { Navigate, Outlet, useLocation } from 'react-router';

import { Config, getConfig } from '../config';
import { useAuthentication } from '../hooks';
Expand All @@ -8,7 +8,6 @@ export type Authorizer<User> = (user?: User) => boolean;

export type Props<User> = {
authorizer: Authorizer<User>;
children: ReactNode;
};

/**
Expand Down Expand Up @@ -36,7 +35,7 @@ export type Props<User> = {
export function IsAuthorized<User>({
authorizer,
children
}: Props<User>): ReactElement {
}: PropsWithChildren<Props<User>>) {
const config: Config = getConfig();
const { currentUser, isLoggedIn } = useAuthentication<User>();
const location = useLocation();
Expand Down Expand Up @@ -65,5 +64,5 @@ export function IsAuthorized<User>({
return <Navigate to={{ pathname: config.dashboardRoute }} />;
}

return <>{children}</>;
return children ?? <Outlet />;
}
43 changes: 32 additions & 11 deletions tests/routeguards/IsAuthenticated.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,36 @@ function Login() {
afterEach(cleanup);

describe('IsAuthenticated', () => {
function setup({ isLoggedIn }: { isLoggedIn: boolean }) {
function setup({
isLoggedIn,
path = '/dashboard'
}: {
isLoggedIn: boolean;
path?: string;
}) {
configureAuthentication();

if (isLoggedIn) {
getService().login({});
}

return render(
<MemoryRouter initialEntries={['/dashboard']}>
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route
path="/dashboard"
element={
<IsAuthenticated>
<Dashboard />
</IsAuthenticated>
}
/>
<Route path="/login" element={<Login />} />
<Route path="/">
<Route
path="dashboard"
element={
<IsAuthenticated>
<Dashboard />
</IsAuthenticated>
}
/>
<Route path="users" element={<IsAuthenticated />}>
<Route index element={<Dashboard />} />
</Route>
<Route path="login" element={<Login />} />
</Route>
</Routes>
</MemoryRouter>
);
Expand All @@ -51,6 +62,16 @@ describe('IsAuthenticated', () => {
});
});

test('loggedIn - subroutes', async () => {
expect.assertions(1);

const { getByTestId } = setup({ isLoggedIn: true, path: '/users' });

await waitFor(() => {
expect(getByTestId('header')).toHaveTextContent('Hello World');
});
});

test('not logged in', () => {
const { getByTestId } = setup({ isLoggedIn: false });

Expand Down
87 changes: 51 additions & 36 deletions tests/routeguards/IsAuthorized.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { cleanup, render, waitFor } from '@testing-library/react';

import { configureAuthentication, getService } from '../../src/config';
import { IsAuthorized } from '../../src/routeguards/IsAuthorized';
import { IsAuthenticated } from '../../src/routeguards/IsAuthenticated';

function Dashboard(): JSX.Element {
return <h1 data-testid="header">Hello logged in user</h1>;
Expand Down Expand Up @@ -37,7 +36,7 @@ describe('IsAuthorized', () => {
authenticationUrl: '/api/authentication',
currentUserUrl: '/api/authentication/current',
loginRoute: '/login',
dashboardRoute: '/'
dashboardRoute: '/dashboard'
});

if (isLoggedIn) {
Expand All @@ -47,40 +46,48 @@ describe('IsAuthorized', () => {
return render(
<MemoryRouter initialEntries={[route]}>
<Routes>
<Route
path="/"
element={
<IsAuthenticated>
<Dashboard />
</IsAuthenticated>
}
/>
<Route
path="/admin"
element={
<IsAuthorized<User> authorizer={(user) => !!user?.isAdmin}>
<AdminArea />
</IsAuthorized>
}
/>
<Route path="/login" element={<Login />} />
<Route path="/">
<Route
path="dashboard"
element={
<IsAuthorized<User> authorizer={(user) => !user?.isAdmin}>
<Dashboard />
</IsAuthorized>
}
/>
<Route
path="users"
element={
<IsAuthorized<User> authorizer={(user) => !!user?.isAdmin} />
}
>
<Route index element={<AdminArea />} />
</Route>
<Route path="login" element={<Login />} />
</Route>
</Routes>
</MemoryRouter>
);
}

test('loggedIn as admin', async () => {
expect.assertions(1);
test('not logged in', () => {
const { getByTestId } = setup({
isLoggedIn: false,
isAdmin: false,
route: '/dashboard'
});

expect(getByTestId('header')).toHaveTextContent('Please log in');
});

test('not logged in but somehow admin', () => {
const { getByTestId } = setup({
isLoggedIn: true,
isLoggedIn: false,
isAdmin: true,
route: '/admin'
route: '/dashboard'
});

await waitFor(() => {
expect(getByTestId('header')).toHaveTextContent('Hello logged in admin');
});
expect(getByTestId('header')).toHaveTextContent('Please log in');
});

test('loggedIn as non admin', async () => {
Expand All @@ -89,7 +96,7 @@ describe('IsAuthorized', () => {
const { getByTestId } = setup({
isLoggedIn: true,
isAdmin: false,
route: '/admin'
route: '/users'
});
const route = getByTestId('header');

Expand All @@ -98,23 +105,31 @@ describe('IsAuthorized', () => {
});
});

test('not logged in but somehow admin', () => {
test('loggedIn as admin', async () => {
expect.assertions(1);

const { getByTestId } = setup({
isLoggedIn: false,
isLoggedIn: true,
isAdmin: true,
route: '/admin'
route: '/users'
});

expect(getByTestId('header')).toHaveTextContent('Please log in');
await waitFor(() => {
expect(getByTestId('header')).toHaveTextContent('Hello logged in admin');
});
});

test('not logged in', () => {
test('loggedIn as admin - subroutes', async () => {
expect.assertions(1);

const { getByTestId } = setup({
isLoggedIn: false,
isAdmin: false,
route: '/admin'
isLoggedIn: true,
isAdmin: true,
route: '/users'
});

expect(getByTestId('header')).toHaveTextContent('Please log in');
await waitFor(() => {
expect(getByTestId('header')).toHaveTextContent('Hello logged in admin');
});
});
});

0 comments on commit 56891b0

Please sign in to comment.