mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
refactor: rendering pages and links by roles
This commit is contained in:
parent
5a510d77d2
commit
5dae2f5975
15
src/App.tsx
15
src/App.tsx
@ -1,6 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Router, Switch } from 'react-router-dom';
|
import { Router, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types
|
||||||
|
*/
|
||||||
import { UserRoleEnum } from 'enums';
|
import { UserRoleEnum } from 'enums';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks
|
||||||
|
*/
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,7 +40,12 @@ const App = () => {
|
|||||||
<PrivateRoute path='/courses' component={CoursesPage} title='Courses' />
|
<PrivateRoute path='/courses' component={CoursesPage} title='Courses' />
|
||||||
<PrivateRoute path='/messages' component={MessagesPage} title='Messages' />
|
<PrivateRoute path='/messages' component={MessagesPage} title='Messages' />
|
||||||
<PrivateRoute path='/profile' component={ProfilePage} title='Profile' />
|
<PrivateRoute path='/profile' component={ProfilePage} title='Profile' />
|
||||||
<PrivateRoute path='/control' component={ControlPage} title='Control' roles={[UserRoleEnum.ADMIN]} />
|
<PrivateRoute
|
||||||
|
path='/control'
|
||||||
|
component={ControlPage}
|
||||||
|
title='Control'
|
||||||
|
roles={[UserRoleEnum.ADMIN, UserRoleEnum.TEACHER]}
|
||||||
|
/>
|
||||||
<PublicRoute path='/auth' component={AuthPage} />
|
<PublicRoute path='/auth' component={AuthPage} />
|
||||||
<PublicRoute component={NotFoundPage} />
|
<PublicRoute component={NotFoundPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@ -1,21 +1,25 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { useErrorHandler } from 'react-error-boundary';
|
import { useErrorHandler } from 'react-error-boundary';
|
||||||
import { Flex, Box } from '@chakra-ui/react';
|
import { Flex, Box } from '@chakra-ui/react';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
import Header from 'components/Header';
|
import Header from 'components/Header';
|
||||||
import NavDesktop from 'components/Nav/Desktop';
|
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
|
* Layout component
|
||||||
* Сomponent for rendering the current page.
|
* Сomponent for rendering the current page.
|
||||||
*/
|
*/
|
||||||
const Layout: React.FC = ({ children }) => {
|
const Layout: React.FC<LayoutPropsType> = ({ children, roles }) => {
|
||||||
const { userStore } = useRootStore();
|
const { userStore } = useRootStore();
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
userStore.loadCurrentUserData().catch(error => handleError(error));
|
userStore.loadCurrentUserData().catch(error => handleError(error));
|
||||||
}, [handleError, userStore]);
|
}, [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 <Redirect to='/404' />;
|
||||||
|
|
||||||
|
return children;
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* Return children if "roles" prop is not defined
|
||||||
|
*/
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { observer } from 'mobx-react';
|
|||||||
import { MdExitToApp } from 'react-icons/md';
|
import { MdExitToApp } from 'react-icons/md';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
|
||||||
import { Avatar } from '@chakra-ui/avatar';
|
import { Avatar } from '@chakra-ui/avatar';
|
||||||
import { Flex, Box, Text } from '@chakra-ui/layout';
|
import { Flex, Box, Text } from '@chakra-ui/layout';
|
||||||
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal';
|
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody } from '@chakra-ui/modal';
|
||||||
@ -31,6 +30,7 @@ const NavMobile: React.FC = () => {
|
|||||||
<NavList links={config.links} onCloseDrawer={onClose} />
|
<NavList links={config.links} onCloseDrawer={onClose} />
|
||||||
</Flex>
|
</Flex>
|
||||||
{userStore.me ? (
|
{userStore.me ? (
|
||||||
|
<Box pb='10px'>
|
||||||
<Box
|
<Box
|
||||||
minHeight='60px'
|
minHeight='60px'
|
||||||
padding='10px'
|
padding='10px'
|
||||||
@ -44,7 +44,11 @@ const NavMobile: React.FC = () => {
|
|||||||
<Text as='span' mr={2} lineHeight='1.2'>
|
<Text as='span' mr={2} lineHeight='1.2'>
|
||||||
{`${userStore.me.first_name} ${userStore.me.last_name}`}
|
{`${userStore.me.first_name} ${userStore.me.last_name}`}
|
||||||
</Text>
|
</Text>
|
||||||
<Link to={`/profile`} style={{ fontSize: '13px', textDecoration: 'underline' }} onClick={onClose}>
|
<Link
|
||||||
|
to={`/profile`}
|
||||||
|
style={{ fontSize: '13px', textDecoration: 'underline' }}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
View profile
|
View profile
|
||||||
</Link>
|
</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -57,6 +61,7 @@ const NavMobile: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
h='60px'
|
h='60px'
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Box, useColorMode } from '@chakra-ui/react';
|
import { Box, useColorMode } from '@chakra-ui/react';
|
||||||
import type { LinkType } from 'interfaces';
|
import type { LinkType } from 'types';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { useRootStore } from 'hooks/useRootStore';
|
||||||
|
import { userContainRoles } from 'helpers';
|
||||||
|
|
||||||
type NavLinkPropsType = {
|
type NavLinkPropsType = {
|
||||||
link: LinkType;
|
link: LinkType;
|
||||||
@ -13,11 +14,16 @@ type NavLinkPropsType = {
|
|||||||
* Returns the link component for navigation.
|
* Returns the link component for navigation.
|
||||||
*/
|
*/
|
||||||
const NavLink: React.FC<NavLinkPropsType> = ({ link, onClickLink }) => {
|
const NavLink: React.FC<NavLinkPropsType> = ({ link, onClickLink }) => {
|
||||||
const { uiStore } = useRootStore();
|
const { uiStore, userStore } = useRootStore();
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
const onClick = () => onClickLink(link);
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as='a'
|
as='a'
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
import { LinkType } from 'types';
|
||||||
import { userHasRoles } from 'helpers';
|
|
||||||
import { UserRoleEnum } from 'enums';
|
|
||||||
import { LinkType } from 'interfaces';
|
|
||||||
import NavLink from './NavLink';
|
import NavLink from './NavLink';
|
||||||
|
|
||||||
type NavListPropsType = {
|
type NavListPropsType = {
|
||||||
@ -16,7 +13,6 @@ type NavListPropsType = {
|
|||||||
* Render a navigation list of links
|
* Render a navigation list of links
|
||||||
*/
|
*/
|
||||||
const NavList: React.FC<NavListPropsType> = ({ links, onCloseDrawer }) => {
|
const NavList: React.FC<NavListPropsType> = ({ links, onCloseDrawer }) => {
|
||||||
const { userStore } = useRootStore();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const onClickLink = useCallback(
|
const onClickLink = useCallback(
|
||||||
(link: LinkType) => {
|
(link: LinkType) => {
|
||||||
@ -30,16 +26,9 @@ const NavList: React.FC<NavListPropsType> = ({ links, onCloseDrawer }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{links.map(link => {
|
{links.map(link => (
|
||||||
if (link.public) {
|
<NavLink link={link} key={link.location} onClickLink={onClickLink} />
|
||||||
return <NavLink link={link} key={link.location} onClickLink={onClickLink} />;
|
))}
|
||||||
} else {
|
|
||||||
if (userStore.me && userHasRoles(userStore.me.roles, [UserRoleEnum.ADMIN])) {
|
|
||||||
return <NavLink link={link} key={link.location} onClickLink={onClickLink} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,13 +21,13 @@ const PrivateRoute: React.FC<IPrivateRouteProps> = ({ component: Component, titl
|
|||||||
render={props =>
|
render={props =>
|
||||||
authStore.isLoggedIn ? (
|
authStore.isLoggedIn ? (
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<Layout>
|
<Layout roles={roles}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>
|
<title>
|
||||||
{title} • {config.metaData.appName}{' '}
|
{title} • {config.metaData.appName}{' '}
|
||||||
</title>
|
</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Component {...props} title={title} roles={roles} />
|
<Component {...props} title={title} />
|
||||||
</Layout>
|
</Layout>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { UserRoleEnum } from 'enums';
|
||||||
import { MdAccountCircle, MdBuild, MdCollectionsBookmark, MdHome, MdMessage } from 'react-icons/md';
|
import { MdAccountCircle, MdBuild, MdCollectionsBookmark, MdHome, MdMessage } from 'react-icons/md';
|
||||||
import { IAppConfig } from './interfaces';
|
import { IAppConfig } from './interfaces';
|
||||||
|
|
||||||
@ -7,11 +8,17 @@ const config: IAppConfig = {
|
|||||||
appDescription: 'Learn management system',
|
appDescription: 'Learn management system',
|
||||||
},
|
},
|
||||||
links: [
|
links: [
|
||||||
{ location: '/', title: 'Home', public: true, icon: MdHome },
|
{ location: '/', title: 'Main', public: true, icon: MdHome },
|
||||||
{ location: '/courses', title: 'Courses', public: true, icon: MdCollectionsBookmark },
|
{ location: '/courses', title: 'Courses', public: true, icon: MdCollectionsBookmark },
|
||||||
{ location: '/messages', title: 'Messages', public: true, icon: MdMessage },
|
{ location: '/messages', title: 'Messages', public: true, icon: MdMessage },
|
||||||
{ location: '/profile', title: 'Profile', public: true, icon: MdAccountCircle },
|
{ 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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,17 @@ export function userHasRoles(userRoles: IUserRole[], roles: UserRoleEnum[]): boo
|
|||||||
return true;
|
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
|
* Clean object values
|
||||||
*
|
*
|
||||||
|
|||||||
@ -119,12 +119,14 @@ export interface IUserResult extends IDataResult {
|
|||||||
|
|
||||||
export interface IPageProps extends RouteComponentProps {
|
export interface IPageProps extends RouteComponentProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
roles?: UserRoleEnum[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPrivateRouteProps extends RouteProps {
|
export interface IPrivateRouteProps extends RouteProps {
|
||||||
component: React.FC<IPageProps>;
|
component: React.FC<IPageProps>;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
/**
|
||||||
|
* This field means who can access the page
|
||||||
|
*/
|
||||||
roles?: UserRoleEnum[];
|
roles?: UserRoleEnum[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { MdError } from 'react-icons/md';
|
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 NotFoundPage: React.FC = () => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@ -23,6 +24,9 @@ const NotFoundPage: React.FC = () => {
|
|||||||
Page not found
|
Page not found
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text mt='5'>Sorry the page you are looking for could not be found.</Text>
|
<Text mt='5'>Sorry the page you are looking for could not be found.</Text>
|
||||||
|
<Link as={ReactRouterLink} to='/' color='blue.500'>
|
||||||
|
Return to main page
|
||||||
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,20 +3,11 @@ import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '
|
|||||||
|
|
||||||
import { IPageProps } from 'interfaces';
|
import { IPageProps } from 'interfaces';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { useRootStore } from 'hooks/useRootStore';
|
|
||||||
import { userHasRoles } from 'helpers';
|
|
||||||
import { Redirect } from 'react-router-dom';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Control page
|
* Control page
|
||||||
*/
|
*/
|
||||||
const ControlPage: React.FC<IPageProps> = ({ title, roles }) => {
|
const ControlPage: React.FC<IPageProps> = ({ title }) => {
|
||||||
const { userStore } = useRootStore();
|
|
||||||
|
|
||||||
if (userStore.me && roles && !userHasRoles(userStore.me.roles, roles)) {
|
|
||||||
return <Redirect to='/404' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||||
|
|||||||
6
src/types/index.d.ts
vendored
6
src/types/index.d.ts
vendored
@ -1,9 +1,15 @@
|
|||||||
|
import { UserRoleEnum } from 'enums';
|
||||||
import { IconType } from 'react-icons/lib';
|
import { IconType } from 'react-icons/lib';
|
||||||
|
|
||||||
export type LinkType = {
|
export type LinkType = {
|
||||||
location: string;
|
location: string;
|
||||||
title: string;
|
title: string;
|
||||||
public: boolean;
|
public: boolean;
|
||||||
|
/**
|
||||||
|
* Render link if user roles includes roles
|
||||||
|
* in config
|
||||||
|
*/
|
||||||
|
roles?: UserRoleEnum[];
|
||||||
icon: IconType;
|
icon: IconType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user