Skip to content

Commit

Permalink
Authentication layouts using react-router-dom (#617)
Browse files Browse the repository at this point in the history
* refactor(backoffice-v2): no longer using useEffect to redirect based on auth state

now using <Navigate/> and navigating to the last page of the user

* refactor(shouldredirect): wrapped in useMemo (PR comment)

* feat(authprovider): added a full screen loader

* fix(useselectentityfilteronmount): updated dependency array

* feat(backoffice-v2): now returning full screen loader instead of null
  • Loading branch information
Omri-Levy authored Jul 16, 2023
1 parent 1b824b7 commit 0cd2be2
Show file tree
Hide file tree
Showing 30 changed files with 310 additions and 250 deletions.
67 changes: 46 additions & 21 deletions apps/backoffice-v2/src/Router/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import { RouteError } from '../common/components/atoms/RouteError/RouteError';
import { CaseManagement } from '../pages/CaseManagement/CaseManagement.page';
import { rootLoader } from '../pages/Root/Root.loader';
import { entitiesLoader } from '../pages/Entities/Entities.loader';
import { localeLoader } from '../pages/Locale/Locale.loader';
import { Locale } from '../pages/Locale/Locale.page';
import { authenticatedLayoutLoader } from '../domains/auth/components/AuthenticatedLayout/AuthenticatedLayout.loader';
import { entityLoader } from '../pages/Entity/Entity.loader';
import { AuthenticatedLayout } from '../domains/auth/components/AuthenticatedLayout';
import { UnauthenticatedLayout } from '../domains/auth/components/UnauthenticatedLayout';
import { Locale } from '../pages/Locale/Locale.page';
import { unauthenticatedLayoutLoader } from '../domains/auth/components/UnauthenticatedLayout/UnauthenticatedLayout.loader';

const router = createBrowserRouter([
{
Expand All @@ -21,35 +24,57 @@ const router = createBrowserRouter([
loader: rootLoader,
errorElement: <RootError />,
children: [
...(env.VITE_AUTH_ENABLED
? [
{
path: '/:locale/auth/sign-in',
element: <SignIn />,
},
]
: []),
{
path: '/:locale',
element: <Locale />,
loader: localeLoader,
element: <UnauthenticatedLayout />,
loader: unauthenticatedLayoutLoader,
errorElement: <RouteError />,
children: [
{
path: '/:locale',
element: <Locale />,
errorElement: <RouteError />,
children: [
...(env.VITE_AUTH_ENABLED
? [
{
path: '/:locale/auth/sign-in',
element: <SignIn />,
errorElement: <RouteError />,
},
]
: []),
],
},
],
},
{
element: <AuthenticatedLayout />,
loader: authenticatedLayoutLoader,
errorElement: <RouteError />,
children: [
{
path: '/:locale/case-management',
element: <CaseManagement />,
path: '/:locale',
element: <Locale />,
errorElement: <RouteError />,
children: [
{
path: '/:locale/case-management/entities',
element: <Entities />,
loader: entitiesLoader,
path: '/:locale/case-management',
element: <CaseManagement />,
errorElement: <RouteError />,
children: [
{
path: '/:locale/case-management/entities/:entityId',
element: <Entity />,
loader: entityLoader,
path: '/:locale/case-management/entities',
element: <Entities />,
loader: entitiesLoader,
errorElement: <RouteError />,
children: [
{
path: '/:locale/case-management/entities/:entityId',
element: <Entity />,
loader: entityLoader,
errorElement: <RouteError />,
},
],
},
],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ComponentProps, FunctionComponent } from 'react';
import { ctw } from '../../../utils/ctw/ctw';

export const Skeleton: FunctionComponent<ComponentProps<'div'>> = ({ className, ...props }) => (
<div className={ctw('animate-pulse rounded-md bg-muted', className)} {...props} />
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { FunctionComponent } from 'react';

export const FullScreenLoader: FunctionComponent = () => {
return (
<div className={`d-full mt-32 flex justify-center`}>
<svg
width="414"
height="608"
viewBox="-2 -2 32 38"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
className={`animated-path`}
fill={`transparent`}
strokeWidth={1}
stroke={`black`}
d="M15.0495 34.0001C11.7759 34.0001 9.24159 32.4364 7.99481 31.4764C7.16779 30.8416 7.02056 29.6619 7.66275 28.8444C8.30493 28.0269 9.49847 27.8813 10.3223 28.5161C11.2151 29.2036 13.0571 30.3431 15.3283 30.244C19.1689 30.0768 22.7996 26.5282 22.7996 22.9393C22.7996 21.1495 22.4644 19.9635 21.5622 18.5701L21.5278 18.5143C19.786 15.6005 16.0112 13.6404 12.1361 13.6404C11.1776 13.6404 10.2879 13.7642 9.48907 14.012C8.48976 14.3216 7.4278 13.7704 7.11454 12.7826C6.80127 11.7948 7.35888 10.742 8.35819 10.4354C9.52353 10.0763 10.7922 9.89355 12.133 9.89355C17.3708 9.89355 22.3266 12.5163 24.77 16.579C26.0575 18.5824 26.5838 20.428 26.5838 22.9393C26.5838 28.4728 21.2928 33.74 15.4849 33.9908C15.3346 33.997 15.1873 34.0001 15.0401 34.0001H15.0495Z"
/>
<path
className={`animated-path`}
fill={`transparent`}
strokeWidth={1}
stroke={`black`}
d="M6.81973 26.2184C2.93214 26.2184 0 23.3727 0 19.6011C0 18.1736 0.523149 16.7244 1.55692 15.2938L1.59451 15.2442C1.71668 15.0708 1.85138 14.9005 1.99548 14.7302C2.09573 14.6095 2.42152 14.2224 2.94467 13.7207C3.6965 12.9993 4.8963 13.0178 5.6262 13.761C6.3561 14.5042 6.33731 15.6901 5.58548 16.4116C5.17197 16.808 4.94015 17.0929 4.93702 17.096L4.89316 17.1486C4.82111 17.2291 4.75846 17.3096 4.70521 17.3871L4.64255 17.4769C4.43267 17.7679 3.79048 18.6535 3.79048 19.6042C3.79048 21.2918 5.03413 22.4716 6.81973 22.4716C8.80582 22.4716 12.6339 21.9328 15.9451 18.3191L15.967 18.2974L16.5183 17.7153C17.2326 16.9597 18.4324 16.9195 19.1999 17.6286C19.9642 18.3346 20.005 19.5206 19.2876 20.2792L18.7488 20.849C14.4602 25.5217 9.43548 26.2215 6.82286 26.2215L6.81973 26.2184Z"
/>
<path
stroke={`black`}
strokeWidth={1}
className={`animated-path`}
fill={`transparent`}
d="M9.12705 20.0439C8.19979 20.0439 7.38844 19.372 7.25373 18.4368C6.76818 15.1018 6.60841 12.0703 6.76504 9.16576C6.79324 4.10601 10.9659 0 16.0877 0C21.2096 0 25.4136 4.13388 25.4136 9.2184C25.4136 12.1849 24.5678 13.9375 23.2019 15.8047C22.5879 16.6439 21.4007 16.8297 20.5549 16.2228C19.7059 15.6158 19.518 14.4454 20.132 13.6062C21.1438 12.2251 21.6231 11.2157 21.6231 9.2184C21.6231 6.20237 19.1421 3.74991 16.0909 3.74991C13.0397 3.74991 10.5587 6.20237 10.5587 9.2184V9.32058C10.4083 11.9898 10.5555 14.7953 11.0066 17.9011C11.157 18.9261 10.4365 19.8767 9.39958 20.0253C9.30874 20.0377 9.21789 20.0439 9.12705 20.0439Z"
/>
</svg>
</div>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@ import { FunctionComponent, PropsWithChildren } from 'react';
import { queryClient } from '../../../../lib/react-query/query-client';
import { AuthProvider } from '../../../../domains/auth/context/AuthProvider/AuthProvider';
import { env } from '../../../env/env';
// import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useLocation } from 'react-router-dom';

export const Providers: FunctionComponent<PropsWithChildren> = ({ children }) => {
const { state } = useLocation();

return (
<QueryClientProvider client={queryClient}>
<AuthProvider
protectedRoutes={
[
// Uses String.prototype.startsWith until authenticated layout is implemented.
'/en/case-management/',
] as const
}
redirectAuthenticatedTo={'/en/case-management/entities'}
redirectUnauthenticatedTo={'/en/auth/sign-in'}
signInOptions={{
redirect: env.VITE_AUTH_ENABLED,
callbackUrl: '/en/case-management/entities',
callbackUrl: state?.from
? `${state?.from?.pathname}${state?.from?.search}`
: '/en/case-management/entities',
}}
signOutOptions={{
redirect: env.VITE_AUTH_ENABLED,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ export const useZodSearchParams = <TSchema extends AnyZodObject>(
[schema, searchParamsAsObject],
);
const navigate = useNavigate();
const { state } = useLocation();
const onSetSearchParams = useCallback(
(searchParams: Record<string, unknown>) => {
navigate(
`${pathname}${serializer({
...parsedSearchParams,
...searchParams,
})}`,
{
state: {
from: state?.from,
},
},
);
},
[pathname, parsedSearchParams, setSearchParams],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import { Header } from '../../../../common/components/organisms/Header';
import { FunctionComponentWithChildren } from '../../../../common/types';
import { useSelectEntityFilterOnMount } from '../../../entities/hooks/useSelectEntityFilterOnMount/useSelectEntityFilterOnMount';
import { useAuthenticatedLayoutLogic } from './hooks/useAuthenticatedLayoutLogic/useAuthenticatedLayoutLogic';
import { Navigate, Outlet } from 'react-router-dom';
import { FunctionComponent } from 'react';
import { FullScreenLoader } from '../../../../common/components/molecules/FullScreenLoader/FullScreenLoader';

export const AuthenticatedLayout: FunctionComponentWithChildren = ({ children }) => {
// Should only be uncommented once `useAuthRedirects` is no longer in use in `AuthProvider`
// useAuthenticatedLayout();
useSelectEntityFilterOnMount();
export const AuthenticatedLayout: FunctionComponent = () => {
const { shouldRedirect, isLoading, redirectUnauthenticatedTo, location } =
useAuthenticatedLayoutLogic();

if (isLoading) return <FullScreenLoader />;

if (shouldRedirect) {
return (
<Navigate
to={redirectUnauthenticatedTo}
replace
state={{
from: location,
}}
/>
);
}

return (
<div className="drawer-mobile drawer">
<input id="app-drawer" type="checkbox" className="drawer-toggle" />
<div className={`drawer-content`}>
<main className={`grid h-full grid-cols-[285px_1fr]`}>
{/*<Outlet />*/}
{children}
<Outlet />
</main>
</div>
<div className={`drawer-side w-56`}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { env } from '../../common/env/env';
import { authQueryKeys } from '../../domains/auth/query-keys';
import { queryClient } from '../../lib/react-query/query-client';
import { filtersQueryKeys } from '../../domains/filters/query-keys';
import { env } from '../../../../common/env/env';
import { authQueryKeys } from '../../query-keys';
import { queryClient } from '../../../../lib/react-query/query-client';
import { filtersQueryKeys } from '../../../filters/query-keys';
import { LoaderFunction } from 'react-router-dom';

export const localeLoader: LoaderFunction = async () => {
export const authenticatedLayoutLoader: LoaderFunction = async () => {
if (!env.VITE_AUTH_ENABLED) return null;

const authenticatedUser = authQueryKeys.authenticatedUser();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useAuthContext } from '../../../../context/AuthProvider/hooks/useAuthContext/useAuthContext';
import { useAuthenticatedUserQuery } from '../../../../hooks/queries/useAuthenticatedUserQuery/useAuthenticatedUserQuery';
import { useIsAuthenticated } from '../../../../context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated';
import { env } from '../../../../../../common/env/env';
import { useLocation } from 'react-router-dom';
import { useMemo } from 'react';

export const useAuthenticatedLayoutLogic = () => {
const { redirectUnauthenticatedTo } = useAuthContext();
const { isLoading } = useAuthenticatedUserQuery();
const isAuthenticated = useIsAuthenticated();
const location = useLocation();
const shouldRedirect = useMemo(
() =>
[!isLoading, !isAuthenticated, !!redirectUnauthenticatedTo, env.VITE_AUTH_ENABLED].every(
Boolean,
),
[isLoading, isAuthenticated, redirectUnauthenticatedTo],
);

return {
shouldRedirect,
isLoading,
redirectUnauthenticatedTo,
location,
};
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import { FunctionComponentWithChildren } from '../../../../common/types';
import { useUnauthenticatedLayoutLogic } from './hooks/useUnauthenticatedLayoutLogic/useUnauthenticatedLayoutLogic';
import { Navigate, Outlet } from 'react-router-dom';
import { FunctionComponent } from 'react';
import { FullScreenLoader } from '../../../../common/components/molecules/FullScreenLoader/FullScreenLoader';

export const UnauthenticatedLayout: FunctionComponentWithChildren = ({ children }) => {
// Should only be uncommented once `useAuthRedirects` is no longer in use in `AuthProvider`
// useUnauthenticatedLayout();
export const UnauthenticatedLayout: FunctionComponent = () => {
const { isLoading, shouldRedirect, redirectAuthenticatedTo, state } =
useUnauthenticatedLayoutLogic();

if (isLoading) return <FullScreenLoader />;

if (shouldRedirect) {
return (
<Navigate
to={redirectAuthenticatedTo}
replace
state={{
from: state?.from,
}}
/>
);
}

return (
<main className={`h-full`}>
{/*<Outlet />*/}
{children}
<Outlet />
</main>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LoaderFunction, redirect } from 'react-router-dom';

export const unauthenticatedLayoutLoader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);

if (url.pathname === `/en/auth/sign-in`) return null;

return redirect(`/en/auth/sign-in`);
};
Loading

0 comments on commit 0cd2be2

Please sign in to comment.