From 5dae2f59751b644090ceb9a1c07fe3e9035f4ca3 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 12 Sep 2021 19:31:31 +0300 Subject: [PATCH] refactor: rendering pages and links by roles --- src/App.tsx | 15 +++++- src/components/Layout/Layout.tsx | 35 +++++++++++-- src/components/Nav/Mobile/index.tsx | 55 +++++++++++--------- src/components/Nav/NavLink.tsx | 12 +++-- src/components/Nav/NavList.tsx | 19 ++----- src/components/PrivateRoute/PrivateRoute.tsx | 4 +- src/config.ts | 11 +++- src/helpers/index.ts | 11 ++++ src/interfaces/index.ts | 4 +- src/pages/404/index.tsx | 6 ++- src/pages/control/index.tsx | 11 +--- src/types/index.d.ts | 6 +++ 12 files changed, 124 insertions(+), 65 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0171cc4..7590a33 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,14 @@ import React from 'react'; import { Router, Switch } from 'react-router-dom'; + +/** + * Types + */ import { UserRoleEnum } from 'enums'; + +/** + * Hooks + */ import { useRootStore } from 'hooks/useRootStore'; /** @@ -32,7 +40,12 @@ const App = () => { - + diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index b59a9bc..2f4a90b 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,21 +1,25 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { observer } from 'mobx-react'; import { useErrorHandler } from 'react-error-boundary'; import { Flex, Box } from '@chakra-ui/react'; import { useRootStore } from 'hooks/useRootStore'; import Header from 'components/Header'; import NavDesktop from 'components/Nav/Desktop'; +import { UserRoleEnum } from 'enums'; +import { userContainRoles } from 'helpers'; +import { Redirect } from 'react-router'; + +type LayoutPropsType = { roles: UserRoleEnum[] | undefined }; /** - * * Layout component * Сomponent for rendering the current page. */ -const Layout: React.FC = ({ children }) => { +const Layout: React.FC = ({ children, roles }) => { const { userStore } = useRootStore(); const handleError = useErrorHandler(); - React.useEffect(() => { + useEffect(() => { userStore.loadCurrentUserData().catch(error => handleError(error)); }, [handleError, userStore]); @@ -42,7 +46,28 @@ const Layout: React.FC = ({ children }) => { }, }} > - {children} + {(() => { + /** + * Check user roles if defined "roles" prop in + * PrivateRoute component and return page + */ + if (roles) { + const { me } = userStore; + if (me === null) return null; + + /** + * The user does not have the required roles + */ + if (!userContainRoles(me.roles, roles)) return ; + + return children; + } else { + /** + * Return children if "roles" prop is not defined + */ + return children; + } + })()} diff --git a/src/components/Nav/Mobile/index.tsx b/src/components/Nav/Mobile/index.tsx index 8842e7e..74c657d 100644 --- a/src/components/Nav/Mobile/index.tsx +++ b/src/components/Nav/Mobile/index.tsx @@ -3,7 +3,6 @@ import { observer } from 'mobx-react'; import { MdExitToApp } from 'react-icons/md'; import { Link } from 'react-router-dom'; import { useRootStore } from 'hooks/useRootStore'; - import { Avatar } from '@chakra-ui/avatar'; import { Flex, Box, Text } from '@chakra-ui/layout'; import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal'; @@ -31,31 +30,37 @@ const NavMobile: React.FC = () => { {userStore.me ? ( - - - - - - {`${userStore.me.first_name} ${userStore.me.last_name}`} - - - View profile - + + + + + + + {`${userStore.me.first_name} ${userStore.me.last_name}`} + + + View profile + + + } + /> - } - /> - + ) : ( = ({ link, onClickLink }) => { - const { uiStore } = useRootStore(); + const { uiStore, userStore } = useRootStore(); const { colorMode } = useColorMode(); - const onClick = () => onClickLink(link); + if (!link.public && link.roles !== undefined) { + const { me } = userStore; + if (me === null) return null; + if (!userContainRoles(me.roles, link.roles)) return null; + } + return ( = ({ links, onCloseDrawer }) => { - const { userStore } = useRootStore(); const history = useHistory(); const onClickLink = useCallback( (link: LinkType) => { @@ -30,16 +26,9 @@ const NavList: React.FC = ({ links, onCloseDrawer }) => { return ( <> - {links.map(link => { - if (link.public) { - return ; - } else { - if (userStore.me && userHasRoles(userStore.me.roles, [UserRoleEnum.ADMIN])) { - return ; - } - } - return null; - })} + {links.map(link => ( + + ))} ); }; diff --git a/src/components/PrivateRoute/PrivateRoute.tsx b/src/components/PrivateRoute/PrivateRoute.tsx index fc69a52..644f604 100644 --- a/src/components/PrivateRoute/PrivateRoute.tsx +++ b/src/components/PrivateRoute/PrivateRoute.tsx @@ -21,13 +21,13 @@ const PrivateRoute: React.FC = ({ component: Component, titl render={props => authStore.isLoggedIn ? ( - + {title} • {config.metaData.appName}{' '} - + ) : ( diff --git a/src/config.ts b/src/config.ts index 5793c58..412d1c1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,4 @@ +import { UserRoleEnum } from 'enums'; import { MdAccountCircle, MdBuild, MdCollectionsBookmark, MdHome, MdMessage } from 'react-icons/md'; import { IAppConfig } from './interfaces'; @@ -7,11 +8,17 @@ const config: IAppConfig = { appDescription: 'Learn management system', }, links: [ - { location: '/', title: 'Home', public: true, icon: MdHome }, + { location: '/', title: 'Main', public: true, icon: MdHome }, { location: '/courses', title: 'Courses', public: true, icon: MdCollectionsBookmark }, { location: '/messages', title: 'Messages', public: true, icon: MdMessage }, { location: '/profile', title: 'Profile', public: true, icon: MdAccountCircle }, - { location: '/control', title: 'Control', public: false, icon: MdBuild }, + { + location: '/control', + title: 'Control', + public: false, + roles: [UserRoleEnum.ADMIN, UserRoleEnum.TEACHER], + icon: MdBuild, + }, ], }; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 80415cc..4bf4419 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -13,6 +13,17 @@ export function userHasRoles(userRoles: IUserRole[], roles: UserRoleEnum[]): boo return true; } +/** + * Return true if user roles contain any one + * of roles in second param + * + * @param userRoles Current user roles + * @param roles Roles to check + */ +export function userContainRoles(userRoles: IUserRole[], roles: UserRoleEnum[]): boolean { + return userRoles.map(r => r.slug).some(r => roles.includes(r)); +} + /** * Clean object values * diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 2824c18..b7cb630 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -119,12 +119,14 @@ export interface IUserResult extends IDataResult { export interface IPageProps extends RouteComponentProps { title?: string; - roles?: UserRoleEnum[]; } export interface IPrivateRouteProps extends RouteProps { component: React.FC; title?: string; + /** + * This field means who can access the page + */ roles?: UserRoleEnum[]; } diff --git a/src/pages/404/index.tsx b/src/pages/404/index.tsx index 0f7f037..18d5174 100644 --- a/src/pages/404/index.tsx +++ b/src/pages/404/index.tsx @@ -1,7 +1,8 @@ import React from 'react'; import Helmet from 'react-helmet'; import { MdError } from 'react-icons/md'; -import { Flex, Box, Heading, Text, useToast } from '@chakra-ui/react'; +import { Flex, Box, Heading, Text, useToast, Link } from '@chakra-ui/react'; +import { Link as ReactRouterLink } from 'react-router-dom'; const NotFoundPage: React.FC = () => { const toast = useToast(); @@ -23,6 +24,9 @@ const NotFoundPage: React.FC = () => { Page not found Sorry the page you are looking for could not be found. + + Return to main page + ); diff --git a/src/pages/control/index.tsx b/src/pages/control/index.tsx index 1a4177b..a83446a 100644 --- a/src/pages/control/index.tsx +++ b/src/pages/control/index.tsx @@ -3,20 +3,11 @@ import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from ' import { IPageProps } from 'interfaces'; import { observer } from 'mobx-react'; -import { useRootStore } from 'hooks/useRootStore'; -import { userHasRoles } from 'helpers'; -import { Redirect } from 'react-router-dom'; /** * Control page */ -const ControlPage: React.FC = ({ title, roles }) => { - const { userStore } = useRootStore(); - - if (userStore.me && roles && !userHasRoles(userStore.me.roles, roles)) { - return ; - } - +const ControlPage: React.FC = ({ title }) => { return ( <> diff --git a/src/types/index.d.ts b/src/types/index.d.ts index a326da0..996fd27 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,9 +1,15 @@ +import { UserRoleEnum } from 'enums'; import { IconType } from 'react-icons/lib'; export type LinkType = { location: string; title: string; public: boolean; + /** + * Render link if user roles includes roles + * in config + */ + roles?: UserRoleEnum[]; icon: IconType; };