Skip to content

Commit d3b109e

Browse files
th-24: Add protected routing [FE] (#60)
* th-24: + stylelint:fix script * th-24: + protected route component * th-24: + protected route example * th-24: + Outlet into ProtectedRoute return statement * th-24: + protected route example * th-24: + add spinner into the ProtectedRoute component * th-24: * changes routes declaration to JSX approach * th-24: * updates the ProtectedRoute component, adds a RouterOutlet * th-24: * updates the ProtectedRoute declaration in the router component * th-20: * removes wrapper over Route * th-24: * removes unused vars * th-24: + User type * th-24: + auth selectors * th-24: * updates protected route component to use the data from the store * th-24: * header and spinner styles * th-24: - remove UserGroup enum, now used enum from shared * th-24: * update User type imports * th-24: * changes imports * th-24: * now Navigate sends to 404 page * th-24: * update UserSignInResponseDto type * th-24: * now ProtectedRoute displays 404 page instead of redirecting * th-24: * update UserSignInResponseDto --------- Co-authored-by: Pavel Sukhinin <[email protected]>
1 parent d206308 commit d3b109e

File tree

12 files changed

+99
-51
lines changed

12 files changed

+99
-51
lines changed

frontend/src/libs/components/components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export { Image } from './image/image.js';
1313
export { Input } from './input/input.js';
1414
export { Link } from './link/link.js';
1515
export { Modal } from './modal/modal.js';
16+
export { ProtectedRoute } from './protected-route/protected-route.js';
1617
export { Radio } from './radio/radio.js';
1718
export { Router } from './router/router.jsx';
1819
export { RouterProvider } from './router-provider/router-provider.jsx';

frontend/src/libs/components/header/header.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AppRoute } from '~/libs/enums/enums.js';
22
import { useCallback, useNavigate } from '~/libs/hooks/hooks.js';
33

4-
import { AppLogo, Link } from '../components.js';
4+
import { AppLogo, Button, Link } from '../components.js';
55
import styles from './styles.module.scss';
66

77
const Header: React.FC = () => {
@@ -18,9 +18,12 @@ const Header: React.FC = () => {
1818
<AppLogo />
1919
</Link>
2020
<div className={styles.navMenu}>
21-
<button type="button" onClick={handleSignIn}>
22-
Sign In
23-
</button>
21+
<Button
22+
label="Sign In"
23+
className={styles.btn}
24+
type="button"
25+
onClick={handleSignIn}
26+
/>
2427
</div>
2528
</div>
2629
</header>

frontend/src/libs/components/header/styles.module.scss

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
@import "src/assets/css/vars.scss";
22

33
.container {
4-
position: absolute;
54
width: 100%;
65
}
76

@@ -13,9 +12,12 @@
1312
padding-right: 29px;
1413
padding-left: 48px;
1514
background-color: $header-background;
16-
border-radius: 10px;
1715
}
1816

1917
.logoContainer {
2018
width: 186px;
2119
}
20+
21+
.btn {
22+
padding-inline: 60px;
23+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { DataStatus } from '~/libs/enums/enums.js';
2+
import { useAppSelector } from '~/libs/hooks/hooks.js';
3+
import { type ValueOf } from '~/libs/types/types.js';
4+
import { type UserGroupKey } from '~/packages/users/libs/enums/enums.js';
5+
import { NotFound } from '~/pages/not-found/not-found.js';
6+
import { selectIsLoading, selectUser } from '~/slices/auth/selectors.js';
7+
8+
import { RouterOutlet } from '../router/router.js';
9+
import { Spinner } from '../spinner/spinner.js';
10+
11+
type Properties = {
12+
allowedUserGroup: ValueOf<typeof UserGroupKey>;
13+
};
14+
15+
const ProtectedRoute = ({
16+
allowedUserGroup,
17+
}: Properties): React.ReactElement | null => {
18+
const isLoading = useAppSelector(selectIsLoading);
19+
const user = useAppSelector(selectUser);
20+
21+
if (isLoading === DataStatus.PENDING) {
22+
return <Spinner size="sm" />;
23+
}
24+
25+
return user && allowedUserGroup === user.group.key ? (
26+
<RouterOutlet />
27+
) : (
28+
<NotFound />
29+
);
30+
};
31+
32+
export { ProtectedRoute };
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1+
import React from 'react';
12
import {
2-
type RouteObject,
33
createBrowserRouter,
4+
createRoutesFromElements,
45
RouterProvider as LibraryRouterProvider,
56
} from 'react-router-dom';
67

78
type Properties = {
8-
routes: RouteObject[];
9+
children: React.ReactNode;
910
};
1011

11-
const RouterProvider: React.FC<Properties> = ({ routes }: Properties) => (
12-
<LibraryRouterProvider router={createBrowserRouter(routes)} />
13-
);
12+
const RouterProvider: React.FC<Properties> = ({ children }: Properties) => {
13+
const routes = createRoutesFromElements(children);
14+
15+
return <LibraryRouterProvider router={createBrowserRouter(routes)} />;
16+
};
1417

1518
export { RouterProvider };
Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,40 @@
1+
import { Route } from 'react-router-dom';
2+
13
import { AppRoute } from '~/libs/enums/enums.js';
4+
import { UserGroupKey } from '~/packages/users/libs/enums/enums.js';
25
import { Auth } from '~/pages/auth/auth.js';
36
import { Dashboard } from '~/pages/dashboard/dashboard.js';
47
import { NotFound } from '~/pages/not-found/not-found.js';
58
import { WelcomePage } from '~/pages/welcome/welcome.js';
69

710
import { App } from '../app/app.js';
11+
import { ProtectedRoute } from '../components.js';
812
import { PageLayout } from '../page-layout/page-layout.js';
913
import { RouterProvider } from '../router-provider/router-provider.js';
1014

1115
const Router = (): JSX.Element => (
12-
<RouterProvider
13-
routes={[
14-
{
15-
path: AppRoute.ROOT,
16-
element: <App />,
17-
children: [
18-
{
19-
path: AppRoute.ROOT,
20-
element: 'Root',
21-
},
22-
{
23-
path: AppRoute.WELCOME,
24-
element: <WelcomePage />,
25-
},
26-
{
27-
path: AppRoute.SIGN_IN,
28-
element: <Auth />,
29-
},
30-
{
31-
path: AppRoute.SIGN_UP,
32-
element: <Auth />,
33-
},
34-
{
35-
path: AppRoute.ANY,
36-
element: <NotFound />,
37-
},
38-
],
39-
},
40-
{
41-
path: AppRoute.DASHBOARD,
42-
element: (
16+
<RouterProvider>
17+
<Route path={AppRoute.ROOT} element={<App />}>
18+
<Route path={AppRoute.WELCOME} element={<WelcomePage />} />
19+
<Route path={AppRoute.SIGN_IN} element={<Auth />} />
20+
<Route path={AppRoute.SIGN_UP} element={<Auth />} />
21+
</Route>
22+
<Route
23+
path={AppRoute.ROOT}
24+
element={<ProtectedRoute allowedUserGroup={UserGroupKey.BUSINESS} />}
25+
>
26+
<Route
27+
path={AppRoute.DASHBOARD}
28+
element={
4329
<PageLayout>
4430
<Dashboard />
4531
</PageLayout>
46-
),
47-
},
48-
]}
49-
/>
32+
}
33+
/>
34+
</Route>
35+
<Route path={AppRoute.ANY} element={<NotFound />} />
36+
</RouterProvider>
5037
);
5138

5239
export { Router };
40+
export { Outlet as RouterOutlet } from 'react-router-dom';

frontend/src/libs/components/spinner/styles.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
.container {
1111
display: flex;
12+
flex-grow: 1;
1213
width: 100%;
1314
height: 100%;
1415
background-color: white;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { type store } from '~/libs/packages/store/store.js';
2+
3+
type RootState = ReturnType<typeof store.instance.getState>;
4+
5+
export { type RootState };

frontend/src/libs/types/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { type AsyncThunkConfig } from './async-thunk-config.type.js';
22
export { type FormField } from './form.type.js';
33
export { type SelectOption } from './select-option.type.js';
44
export { type TabName, type TabsType } from './sidebar.type.js';
5+
export { type RootState } from './store.type.js';
56
export { type DeepPartial, type FieldValues } from 'react-hook-form';
67
export {
78
type BusinessSignUpRequestDto,

frontend/src/slices/auth/auth.slice.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { createSlice } from '@reduxjs/toolkit';
22

33
import { DataStatus } from '~/libs/enums/enums.js';
4-
import {
5-
type UserSignInResponseDto,
6-
type ValueOf,
7-
} from '~/libs/types/types.js';
4+
import { type ValueOf } from '~/libs/types/types.js';
5+
import { type UserSignInResponseDto } from '~/packages/users/users.js';
86

97
import { signIn, signUp } from './actions.js';
108

0 commit comments

Comments
 (0)