| @@ -28,10 +28,12 @@ | |||
| "react-dom": "^17.0.2", | |||
| "react-helmet-async": "^1.0.9", | |||
| "react-i18next": "^11.10.0", | |||
| "react-idle-timer": "^5.4.2", | |||
| "react-redux": "^7.2.4", | |||
| "react-router-dom": "^5.2.0", | |||
| "react-scripts": "4.0.3", | |||
| "react-select": "^4.3.1", | |||
| "react-toastify": "9.0.3", | |||
| "redux": "^4.1.0", | |||
| "redux-saga": "^1.1.3", | |||
| "sass": "^1.34.1", | |||
| @@ -1,24 +1,27 @@ | |||
| import React from 'react'; | |||
| import { Router } from 'react-router-dom'; | |||
| import { Helmet } from 'react-helmet-async'; | |||
| import i18next from 'i18next'; | |||
| import history from './store/utils/history'; | |||
| import AppRoutes from './AppRoutes'; | |||
| import React from "react"; | |||
| import { Router } from "react-router-dom"; | |||
| import { Helmet } from "react-helmet-async"; | |||
| import i18next from "i18next"; | |||
| import history from "./store/utils/history"; | |||
| import AppRoutes from "./AppRoutes"; | |||
| const App = () => ( | |||
| import { ToastContainer } from "react-toastify"; | |||
| import "react-toastify/dist/ReactToastify.css"; | |||
| const App = () => { | |||
| return ( | |||
| <> | |||
| <Router history={history}> | |||
| <Helmet> | |||
| <title> | |||
| {i18next.t('app.title')} | |||
| </title> | |||
| <title>{i18next.t("app.title")}</title> | |||
| </Helmet> | |||
| <main className="l-page"> | |||
| <AppRoutes /> | |||
| </main> | |||
| <ToastContainer bodyClassName="ToastBody" /> | |||
| <main className="l-page"> | |||
| <AppRoutes /> | |||
| </main> | |||
| </Router> | |||
| </> | |||
| ); | |||
| }; | |||
| export default App; | |||
| export default App; | |||
| @@ -1,160 +1,171 @@ | |||
| import React, { useState, useMemo, useContext } from 'react'; | |||
| import React, { useState, useMemo, useContext } from "react"; | |||
| import { | |||
| AppBar, | |||
| Badge, | |||
| Box, | |||
| IconButton, | |||
| Toolbar, | |||
| Typography, | |||
| List, | |||
| ListItem, | |||
| ListItemButton, | |||
| ListItemIcon, | |||
| ListItemText, | |||
| useMediaQuery, | |||
| } from '@mui/material'; | |||
| import { useTheme } from '@mui/system'; | |||
| import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'; | |||
| import ShoppingBasketIcon from '@mui/icons-material/ShoppingBasket'; | |||
| import Brightness4Icon from '@mui/icons-material/Brightness4'; | |||
| import Brightness7Icon from '@mui/icons-material/Brightness7'; | |||
| import MenuList from './MenuListComponent'; | |||
| import Drawer from './DrawerComponent'; | |||
| import { ColorModeContext } from '../../context/ColorModeContext'; | |||
| AppBar, | |||
| Badge, | |||
| Box, | |||
| IconButton, | |||
| Toolbar, | |||
| Typography, | |||
| List, | |||
| ListItem, | |||
| ListItemButton, | |||
| ListItemIcon, | |||
| ListItemText, | |||
| useMediaQuery, | |||
| } from "@mui/material"; | |||
| import { useTheme } from "@mui/system"; | |||
| import MenuOutlinedIcon from "@mui/icons-material/MenuOutlined"; | |||
| import ShoppingBasketIcon from "@mui/icons-material/ShoppingBasket"; | |||
| import LogoutIcon from "@mui/icons-material/Logout"; | |||
| import Brightness4Icon from "@mui/icons-material/Brightness4"; | |||
| import Brightness7Icon from "@mui/icons-material/Brightness7"; | |||
| import MenuList from "./MenuListComponent"; | |||
| import Drawer from "./DrawerComponent"; | |||
| import { ColorModeContext } from "../../context/ColorModeContext"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { logoutUser } from "../../store/actions/login/loginActions"; | |||
| const NavbarComponent = () => { | |||
| const [openDrawer, setOpenDrawer] = useState(false); | |||
| const theme = useTheme(); | |||
| const matches = useMediaQuery(theme.breakpoints.down('sm')); | |||
| const toggleColorMode = useContext(ColorModeContext); | |||
| const dispatch = useDispatch(); | |||
| const [openDrawer, setOpenDrawer] = useState(false); | |||
| const theme = useTheme(); | |||
| const matches = useMediaQuery(theme.breakpoints.down("sm")); | |||
| const toggleColorMode = useContext(ColorModeContext); | |||
| const handleToggleDrawer = () => { | |||
| setOpenDrawer(!openDrawer); | |||
| }; | |||
| const handleToggleDrawer = () => { | |||
| setOpenDrawer(!openDrawer); | |||
| }; | |||
| const drawerContent = useMemo( | |||
| () => ( | |||
| <List> | |||
| <ListItemButton divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 1</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItemButton> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 2</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItem> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemText>Link 3</ListItemText> | |||
| </ListItem> | |||
| <ListItem divider> | |||
| <IconButton onClick={toggleColorMode}> | |||
| <ListItemText>Toggle {theme.palette.mode} mode</ListItemText> | |||
| {theme.palette.mode === 'dark' ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </ListItem> | |||
| </List> | |||
| ), | |||
| [handleToggleDrawer] | |||
| ); | |||
| const handleLogout = () => { | |||
| dispatch(logoutUser()); | |||
| }; | |||
| return ( | |||
| <AppBar | |||
| elevation={2} | |||
| sx={{ backgroundColor: 'background.default', position: 'relative' }} | |||
| > | |||
| <Toolbar> | |||
| <Box | |||
| component="div" | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| alignItems: 'center', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Drawer | |||
| open={openDrawer} | |||
| toggleOpen={handleToggleDrawer} | |||
| content={drawerContent} | |||
| /> | |||
| ) : ( | |||
| <Box sx={{ display: 'flex' }}> | |||
| <Typography | |||
| variant="h6" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: 'pointer', | |||
| color: 'text.primary', | |||
| }} | |||
| > | |||
| Link 1 | |||
| </Typography> | |||
| <Typography | |||
| variant="body1" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: 'pointer', | |||
| color: 'text.primary', | |||
| }} | |||
| > | |||
| Link 2 | |||
| </Typography> | |||
| <Typography | |||
| variant="subtitle1" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: 'pointer', | |||
| color: 'text.primary', | |||
| }} | |||
| > | |||
| Link 3 | |||
| </Typography> | |||
| </Box> | |||
| )} | |||
| <Box> | |||
| <MenuList /> | |||
| </Box> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'center', | |||
| alignItems: 'center', | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Box> | |||
| <IconButton onClick={handleToggleDrawer}> | |||
| <MenuOutlinedIcon /> | |||
| </IconButton> | |||
| </Box> | |||
| ) : ( | |||
| <Box> | |||
| <IconButton> | |||
| <Badge badgeContent={3} color="primary"> | |||
| <ShoppingBasketIcon color="action" /> | |||
| </Badge> | |||
| </IconButton> | |||
| <IconButton sx={{ ml: 1 }} onClick={toggleColorMode}> | |||
| {theme.palette.mode === 'dark' ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| </Toolbar> | |||
| </AppBar> | |||
| ); | |||
| const drawerContent = useMemo( | |||
| () => ( | |||
| <List> | |||
| <ListItemButton divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 1</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItemButton> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 2</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItem> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemText>Link 3</ListItemText> | |||
| </ListItem> | |||
| <ListItem divider> | |||
| <IconButton onClick={toggleColorMode}> | |||
| <ListItemText>Toggle {theme.palette.mode} mode</ListItemText> | |||
| {theme.palette.mode === "dark" ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </ListItem> | |||
| </List> | |||
| ), | |||
| [handleToggleDrawer] | |||
| ); | |||
| return ( | |||
| <AppBar | |||
| elevation={2} | |||
| sx={{ backgroundColor: "background.default", position: "relative" }} | |||
| > | |||
| <Toolbar> | |||
| <Box | |||
| component="div" | |||
| sx={{ | |||
| display: "flex", | |||
| justifyContent: "space-between", | |||
| alignItems: "center", | |||
| width: "100%", | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Drawer | |||
| open={openDrawer} | |||
| toggleOpen={handleToggleDrawer} | |||
| content={drawerContent} | |||
| /> | |||
| ) : ( | |||
| <Box sx={{ display: "flex" }}> | |||
| <Typography | |||
| variant="h6" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: "pointer", | |||
| color: "text.primary", | |||
| }} | |||
| > | |||
| Link 1 | |||
| </Typography> | |||
| <Typography | |||
| variant="body1" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: "pointer", | |||
| color: "text.primary", | |||
| }} | |||
| > | |||
| Link 2 | |||
| </Typography> | |||
| <Typography | |||
| variant="subtitle1" | |||
| sx={{ | |||
| marginRight: 3, | |||
| cursor: "pointer", | |||
| color: "text.primary", | |||
| }} | |||
| > | |||
| Link 3 | |||
| </Typography> | |||
| </Box> | |||
| )} | |||
| <Box> | |||
| <MenuList /> | |||
| </Box> | |||
| <Box | |||
| sx={{ | |||
| display: "flex", | |||
| justifyContent: "center", | |||
| alignItems: "center", | |||
| }} | |||
| > | |||
| {matches ? ( | |||
| <Box> | |||
| <IconButton onClick={handleToggleDrawer}> | |||
| <MenuOutlinedIcon /> | |||
| </IconButton> | |||
| </Box> | |||
| ) : ( | |||
| <Box> | |||
| <IconButton> | |||
| <Badge badgeContent={3} color="primary"> | |||
| <ShoppingBasketIcon color="action" /> | |||
| </Badge> | |||
| </IconButton> | |||
| <IconButton sx={{ ml: 1 }} onClick={toggleColorMode}> | |||
| {theme.palette.mode === "dark" ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| <IconButton onClick={handleLogout}> | |||
| <LogoutIcon /> | |||
| </IconButton> | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| </Toolbar> | |||
| </AppBar> | |||
| ); | |||
| }; | |||
| export default NavbarComponent; | |||
| @@ -14,6 +14,7 @@ export default { | |||
| error: 'Error', | |||
| continue: 'Continue', | |||
| labelUsername: 'Username', | |||
| labelEmail: 'Email', | |||
| labelPassword: 'Password', | |||
| next: 'Next', | |||
| nextPage: 'Next page', | |||
| @@ -88,6 +89,10 @@ export default { | |||
| }, | |||
| apiErrors:{ | |||
| ClientIpAddressIsNullOrEmpty:"Client Ip address is null or empty", | |||
| UsernameDoesNotExist: "Username does not exist" | |||
| UsernameDoesNotExist: "Username does not exist", | |||
| WrongCredentials: "Wrong credentials", | |||
| SomethingWentWrong: "Something went wrong", | |||
| WrongPasswordAccountIsLocked: "Wrong credentials, account is locked", | |||
| AccountIsLocked: "Account is locked" | |||
| } | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export default { | |||
| email: "", | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export default { | |||
| email: "", | |||
| password: "", | |||
| }; | |||
| @@ -1,8 +1,6 @@ | |||
| import React from 'react'; | |||
| import { useFormik } from 'formik'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import * as Yup from 'yup'; | |||
| import i18next from 'i18next'; | |||
| import { | |||
| Box, | |||
| Container, | |||
| @@ -15,12 +13,8 @@ import { | |||
| import Backdrop from '../../components/MUI/BackdropComponent'; | |||
| import { LOGIN_PAGE } from '../../constants/pages'; | |||
| import { NavLink } from 'react-router-dom'; | |||
| const forgotPasswordValidationSchema = Yup.object().shape({ | |||
| email: Yup.string() | |||
| .required(i18next.t('forgotPassword.emailRequired')) | |||
| .email(i18next.t('forgotPassword.emailFormat')), | |||
| }); | |||
| import forgotPasswordValidation from '../../validations/forgotPasswordValidation'; | |||
| import forgotPasswordInitialValues from '../../initialValues/forgotPasswordInitialValues'; | |||
| const ForgotPasswordPage = () => { | |||
| const { t } = useTranslation(); | |||
| @@ -30,10 +24,8 @@ const ForgotPasswordPage = () => { | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: '', | |||
| }, | |||
| validationSchema: forgotPasswordValidationSchema, | |||
| initialValues: forgotPasswordInitialValues, | |||
| validationSchema: forgotPasswordValidation, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| @@ -1,37 +1,37 @@ | |||
| import React from 'react'; | |||
| import { Box, Grid } from '@mui/material'; | |||
| import Navbar from '../../components/MUI/NavbarComponent'; | |||
| import Modals from '../../components/MUI/Examples/ModalsExample'; | |||
| import DataGrid from '../../components/MUI/Examples/DataGridExample'; | |||
| import PagingSortingFiltering from '../../components/MUI/Examples/PagingSortingFilteringExample'; | |||
| import PagingSortingFilteringServerSide from '../../components/MUI/Examples/PagingSortingFilteringExampleServerSide'; | |||
| import RandomDataProvider from '../../context/RandomDataContext'; | |||
| import React from "react"; | |||
| import { Box, Grid } from "@mui/material"; | |||
| import Navbar from "../../components/MUI/NavbarComponent"; | |||
| import Modals from "../../components/MUI/Examples/ModalsExample"; | |||
| import DataGrid from "../../components/MUI/Examples/DataGridExample"; | |||
| import PagingSortingFiltering from "../../components/MUI/Examples/PagingSortingFilteringExample"; | |||
| import PagingSortingFilteringServerSide from "../../components/MUI/Examples/PagingSortingFilteringExampleServerSide"; | |||
| import RandomDataProvider from "../../context/RandomDataContext"; | |||
| const HomePage = () => { | |||
| return ( | |||
| <> | |||
| <Navbar /> | |||
| <Box sx={{ mt: 4, mx: 4 }}> | |||
| <Grid container spacing={2} justifyContent="center"> | |||
| <Grid item xs={12} md={3}> | |||
| <Modals /> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <DataGrid /> | |||
| </Grid> | |||
| <Grid item xs={12} md={9}> | |||
| <PagingSortingFiltering /> | |||
| </Grid> | |||
| <Grid item xs={12} md={9}> | |||
| {/* Move to higher components? */} | |||
| <RandomDataProvider> | |||
| <PagingSortingFilteringServerSide /> | |||
| </RandomDataProvider> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </> | |||
| ); | |||
| return ( | |||
| <> | |||
| <Navbar /> | |||
| <Box sx={{ mt: 4, mx: 4 }}> | |||
| <Grid container spacing={2} justifyContent="center"> | |||
| <Grid item xs={12} md={3}> | |||
| <Modals /> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <DataGrid /> | |||
| </Grid> | |||
| <Grid item xs={12} md={9}> | |||
| <PagingSortingFiltering /> | |||
| </Grid> | |||
| <Grid item xs={12} md={9}> | |||
| {/* Move to higher components? */} | |||
| <RandomDataProvider> | |||
| <PagingSortingFilteringServerSide /> | |||
| </RandomDataProvider> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </> | |||
| ); | |||
| }; | |||
| export default HomePage; | |||
| @@ -1,12 +1,11 @@ | |||
| /* eslint-disable */ | |||
| import React, { useState } from 'react'; | |||
| import React, { useEffect, useState } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { useFormik } from 'formik'; | |||
| import { useDispatch, useSelector } from 'react-redux'; | |||
| import { NavLink } from 'react-router-dom'; | |||
| import * as Yup from 'yup'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import i18next from 'i18next'; | |||
| import { | |||
| clearLoginErrors, | |||
| fetchUser, | |||
| @@ -29,6 +28,8 @@ import Backdrop from '../../components/MUI/BackdropComponent'; | |||
| import ErrorMessage from '../../components/MUI/ErrorMessageComponent'; | |||
| import { selectIsLoadingByActionType } from '../../store/selectors/loadingSelectors'; | |||
| import { LOGIN_USER_LOADING } from '../../store/actions/login/loginActionConstants'; | |||
| import loginValidation from '../../validations/loginValidation'; | |||
| import loginInitialValues from '../../initialValues/loginInitialValues'; | |||
| const LoginPage = ({ history }) => { | |||
| const dispatch = useDispatch(); | |||
| @@ -39,16 +40,10 @@ const LoginPage = ({ history }) => { | |||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||
| // When user refreshes page | |||
| // useEffect(() => { | |||
| // function redirectClient() { | |||
| // if (!tokens.RefreshToken && !tokens.JwtToken) { | |||
| // return; | |||
| // } | |||
| // } | |||
| // redirectClient(); | |||
| // }, [history, tokens]); | |||
| // Clear login errors when user firstly enters the page | |||
| useEffect(() => { | |||
| dispatch(clearLoginErrors()) | |||
| }, []) | |||
| const isLoading = useSelector( | |||
| selectIsLoadingByActionType(LOGIN_USER_LOADING) | |||
| @@ -64,7 +59,7 @@ const LoginPage = ({ history }) => { | |||
| }; | |||
| const handleSubmit = (values) => { | |||
| const { username: Username, password: Password } = values; | |||
| const { email: Username, password: Password } = values; | |||
| dispatch(clearLoginErrors()); | |||
| dispatch( | |||
| fetchUser({ | |||
| @@ -76,14 +71,8 @@ const LoginPage = ({ history }) => { | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| username: '', | |||
| password: '', | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| username: Yup.string().required(t('login.usernameRequired')), | |||
| password: Yup.string().required(t('login.passwordRequired')), | |||
| }), | |||
| initialValues: loginInitialValues, | |||
| validationSchema: loginValidation, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| @@ -110,13 +99,13 @@ const LoginPage = ({ history }) => { | |||
| > | |||
| <Backdrop position="absolute" isLoading={isLoading} /> | |||
| <TextField | |||
| name="username" | |||
| label={t('common.labelUsername')} | |||
| name="email" | |||
| label={t('common.labelEmail')} | |||
| margin="normal" | |||
| value={formik.values.username} | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.username && Boolean(formik.errors.username)} | |||
| helperText={formik.touched.username && formik.errors.username} | |||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| @@ -53,11 +53,64 @@ export const removeHeaderToken = () => { | |||
| delete request.defaults.headers.Authorization; | |||
| }; | |||
| export const attachPostRequestListener = (postRequestListener) => { | |||
| request.interceptors.response.use( | |||
| // If you pass function to interceptor of axios, it only adds that function | |||
| // to existing array of interceptor functions. That causes that same function | |||
| // of interceptors getting called multiple times instead of just one time, as it | |||
| // is supposed to do. Thats why there is 'global' axios interceptor array, which indicates | |||
| // axios to eject previous interceptor. This approach requires that every middleware has its | |||
| // unique name from which it is being recognized. Every object in those arrays contains | |||
| // interceptor name and ID of interceptor function. | |||
| let axiosInterceptorRequests = []; | |||
| let axiosInterceptorResponses = []; | |||
| export const attachPostRequestListener = ( | |||
| postRequestListener, | |||
| interceptorName | |||
| ) => { | |||
| let previousAxiosInterceptor = axiosInterceptorResponses.find( | |||
| (item) => item.name === interceptorName | |||
| ); | |||
| let previousAxiosInterceptorResponses = axiosInterceptorResponses; | |||
| if (previousAxiosInterceptor !== undefined) { | |||
| request.interceptors.response.eject(previousAxiosInterceptor.interceptorID); | |||
| previousAxiosInterceptorResponses = axiosInterceptorResponses.filter( | |||
| (item) => item.interceptorID !== previousAxiosInterceptor.interceptorID | |||
| ); | |||
| } | |||
| let axiosInterceptorID = request.interceptors.response.use( | |||
| (response) => response, | |||
| (response) => postRequestListener(response), | |||
| (response) => postRequestListener(response) | |||
| ); | |||
| previousAxiosInterceptorResponses.push({ | |||
| name: interceptorName, | |||
| interceptorID: axiosInterceptorID, | |||
| }); | |||
| axiosInterceptorResponses = [...previousAxiosInterceptorResponses]; | |||
| }; | |||
| export const attachBeforeRequestListener = ( | |||
| beforeRequestListener, | |||
| interceptorName | |||
| ) => { | |||
| let previousAxiosInterceptor = axiosInterceptorRequests.find( | |||
| (item) => item.name === interceptorName | |||
| ); | |||
| let previousAxiosInterceptorRequests = axiosInterceptorRequests; | |||
| if (previousAxiosInterceptor !== undefined) { | |||
| request.interceptors.request.eject(previousAxiosInterceptor.interceptorID); | |||
| previousAxiosInterceptorRequests = axiosInterceptorRequests.filter( | |||
| (item) => item.interceptorID !== previousAxiosInterceptor.interceptorID | |||
| ); | |||
| } | |||
| let axiosInterceptorID = request.interceptors.request.use( | |||
| (response) => beforeRequestListener(response), | |||
| (response) => response | |||
| ); | |||
| previousAxiosInterceptorRequests.push({ | |||
| name: interceptorName, | |||
| interceptorID: axiosInterceptorID, | |||
| }); | |||
| axiosInterceptorRequests = [...previousAxiosInterceptorRequests]; | |||
| }; | |||
| export const apiDefaultUrl = request.defaults.baseURL; | |||
| @@ -1,12 +1,20 @@ | |||
| import { applyMiddleware, compose, createStore } from 'redux'; | |||
| import createSagaMiddleware from 'redux-saga'; | |||
| import rootReducer from './reducers'; | |||
| import rootSaga from './saga'; | |||
| import loadingMiddleware from './middleware/loadingMiddleware'; | |||
| import requestStatusMiddleware from './middleware/requestStatusMiddleware'; | |||
| import internalServerErrorMiddleware from './middleware/internalServerErrorMiddleware'; | |||
| import { applyMiddleware, compose, createStore } from "redux"; | |||
| import createSagaMiddleware from "redux-saga"; | |||
| import rootReducer from "./reducers"; | |||
| import rootSaga from "./saga"; | |||
| import loadingMiddleware from "./middleware/loadingMiddleware"; | |||
| import requestStatusMiddleware from "./middleware/requestStatusMiddleware"; | |||
| import internalServerErrorMiddleware from "./middleware/internalServerErrorMiddleware"; | |||
| // import accessTokenMiddleware from "./middleware/accessTokenMiddleware"; | |||
| // import authenticationMiddleware from "./middleware/authenticationMiddleware"; | |||
| const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; | |||
| const composeEnhancers = | |||
| (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ && | |||
| window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ | |||
| trace: true, | |||
| traceLimit: 25, | |||
| })) || | |||
| compose; | |||
| const sagaMiddleware = createSagaMiddleware(); | |||
| export default createStore( | |||
| rootReducer, | |||
| @@ -15,9 +23,9 @@ export default createStore( | |||
| sagaMiddleware, | |||
| loadingMiddleware, | |||
| requestStatusMiddleware, | |||
| internalServerErrorMiddleware, | |||
| ), | |||
| ), | |||
| internalServerErrorMiddleware | |||
| ) | |||
| ) | |||
| ); | |||
| sagaMiddleware.run(rootSaga); | |||
| @@ -0,0 +1,29 @@ | |||
| import jwt from "jsonwebtoken"; | |||
| import { JWT_TOKEN } from "../../constants/localStorage"; | |||
| import { attachBeforeRequestListener } from "../../request/index"; | |||
| import { authScopeStringGetHelper } from "../../util/helpers/authScopeHelpers"; | |||
| import { refreshUserToken } from "../actions/login/loginActions"; | |||
| export const accessTokensMiddlewareInterceptorName = "ACCESS_TOKEN_INTERCEPTOR"; | |||
| export default ({ dispatch }) => | |||
| (next) => | |||
| (action) => { | |||
| attachBeforeRequestListener(async (response) => { | |||
| const jwtToken = authScopeStringGetHelper(JWT_TOKEN); | |||
| const jwtTokenDecoded = jwt.decode(jwtToken); | |||
| if (!response.headers?.Authorization) { | |||
| response.headers.Authorization = `Bearer ${jwtToken}`; | |||
| } | |||
| // If access token is expired, refresh access token | |||
| if (new Date() > new Date(jwtTokenDecoded.exp * 1000)) { | |||
| console.log('response', response) | |||
| dispatch(refreshUserToken()); | |||
| } | |||
| return Promise.resolve(response); | |||
| }, accessTokensMiddlewareInterceptorName); | |||
| next(action); | |||
| }; | |||
| @@ -0,0 +1,25 @@ | |||
| import i18next from "i18next"; | |||
| import { attachPostRequestListener } from "../../request"; | |||
| import { logoutUser } from "../actions/login/loginActions"; | |||
| import { makeErrorToastMessage } from "../../util/helpers/toastMessage"; | |||
| export const authenticationMiddlewareInterceptorName = | |||
| "AUTHENTICATION_MIDDLEWARE"; | |||
| export default ({ dispatch }) => | |||
| (next) => | |||
| (action) => { | |||
| attachPostRequestListener((error) => { | |||
| if (!error.response) { | |||
| makeErrorToastMessage(i18next.t("apiErrors.SomethingWentWrong")); | |||
| return Promise.reject(error); | |||
| } | |||
| if (error.response.status === 401) { | |||
| dispatch(logoutUser()); | |||
| return Promise.reject(error); | |||
| } | |||
| return Promise.resolve(); | |||
| }, authenticationMiddlewareInterceptorName); | |||
| next(action); | |||
| }; | |||
| @@ -1,17 +1,20 @@ | |||
| import { ERROR_PAGE } from '../../constants/pages'; | |||
| import { attachPostRequestListener } from '../../request'; | |||
| import history from '../utils/history'; | |||
| import { attachPostRequestListener } from "../../request"; | |||
| import { makeErrorToastMessage } from "../../util/helpers/toastMessage"; | |||
| import i18next from "i18next"; | |||
| export const serverErrorMiddlewareInterceptorName = | |||
| "INTERNAL_SERVER_ERROR_MIDDLEWARE_INTERCEPTOR"; | |||
| export default () => (next) => (action) => { | |||
| attachPostRequestListener((error) => { | |||
| if (!error.response) { | |||
| return Promise.reject(error); | |||
| return makeErrorToastMessage(i18next.t("apiErrors.SomethingWentWrong")); | |||
| } | |||
| if (error.response.status === 500) { | |||
| return history.push(ERROR_PAGE); | |||
| return makeErrorToastMessage(i18next.t("apiErrors.SomethingWentWrong")); | |||
| } | |||
| return Promise.reject(error); | |||
| }); | |||
| }, serverErrorMiddlewareInterceptorName); | |||
| next(action); | |||
| }; | |||
| @@ -1,22 +1,27 @@ | |||
| import { attachPostRequestListener } from '../../request'; | |||
| import apiEndpoints from '../../request/apiEndpoints'; | |||
| import { logoutUser } from '../actions/login/loginActions'; | |||
| import { attachPostRequestListener } from "../../request"; | |||
| import apiEndpoints from "../../request/apiEndpoints"; | |||
| import { logoutUser } from "../actions/login/loginActions"; | |||
| export default ({ dispatch }) => (next) => (action) => { | |||
| attachPostRequestListener((error) => { | |||
| if (!error.response) { | |||
| export const requestStatusMiddlewareInterceptorName = | |||
| "REQUEST_STATUS_MIDDLEWARE_INTERCEPTOR"; | |||
| export default ({ dispatch }) => | |||
| (next) => | |||
| (action) => { | |||
| attachPostRequestListener((error) => { | |||
| if (!error.response) { | |||
| return Promise.reject(error); | |||
| } | |||
| if ( | |||
| error.response.config.url !== apiEndpoints.authentications.login && | |||
| error.response.config.url !== | |||
| apiEndpoints.authentications.confirmSecurityQuestion && | |||
| error.response.status === 401 | |||
| ) { | |||
| return dispatch(logoutUser()); | |||
| } | |||
| return Promise.reject(error); | |||
| } | |||
| if ( | |||
| error.response.config.url !== apiEndpoints.authentications.login && | |||
| error.response.config.url !== | |||
| apiEndpoints.authentications.confirmSecurityQuestion && | |||
| error.response.status === 401 | |||
| ) { | |||
| return dispatch(logoutUser()); | |||
| } | |||
| return Promise.reject(error); | |||
| }); | |||
| }, requestStatusMiddlewareInterceptorName); | |||
| next(action); | |||
| }; | |||
| next(action); | |||
| }; | |||
| @@ -60,9 +60,6 @@ function* fetchUser({ payload }) { | |||
| } | |||
| } catch (e) { | |||
| if (e.response && e.response.data) { | |||
| if (payload.handleApiResponseSuccess) { | |||
| yield call(payload.handleApiResponseSuccess); | |||
| } | |||
| const errorMessage = yield call(rejectErrorCodeHelper, e); | |||
| yield put(fetchUserError(errorMessage)); | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| import history from "../../store/utils/history"; | |||
| export const routeMatches = (route, secondRoute = null) => { | |||
| let routeToCheck = secondRoute || history.location.pathname; | |||
| if ( | |||
| routeToCheck === route || | |||
| routeToCheck + "/" === route || | |||
| routeToCheck.slice(0, -1) === route | |||
| ) { | |||
| return true; | |||
| } | |||
| return false; | |||
| }; | |||
| export const replaceInRoute = (route, pathVariables = {}) => { | |||
| const keys = Object.keys(pathVariables); | |||
| return keys.reduce( | |||
| (acc, key) => acc.replace(`:${key}`, pathVariables[`${key}`]), | |||
| route | |||
| ); | |||
| }; | |||
| export const dynamicRouteMatches = (dynamicRoute) => { | |||
| let indexOfDynamicChar = dynamicRoute.indexOf(":"); | |||
| if (indexOfDynamicChar === -1) return false; | |||
| const charactersToDelete = (dynamicRoute.length - indexOfDynamicChar) * -1; | |||
| const newDynamicRoute = dynamicRoute.slice(0, charactersToDelete); | |||
| return history.location.pathname.includes(newDynamicRoute); | |||
| }; | |||
| export const isInRoute = (routeToCheck) => { | |||
| return ( | |||
| history.location.pathname.includes(routeToCheck) || | |||
| dynamicRouteMatches(routeToCheck) | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,14 @@ | |||
| import { toast } from "react-toastify"; | |||
| const defaultOptions = { | |||
| position: "top-center", | |||
| autoClose: 3000, | |||
| hideProgressBar: true, | |||
| closeOnClick: true, | |||
| pauseOnHover: true, | |||
| pauseOnFocusLoss: false, | |||
| draggable: true, | |||
| }; | |||
| export const makeToastMessage = (message, options = defaultOptions) => toast(message, options); | |||
| export const makeErrorToastMessage = (message, options = defaultOptions) => toast.error(message, options); | |||
| @@ -0,0 +1,8 @@ | |||
| import * as Yup from "yup"; | |||
| import i18next from "i18next"; | |||
| export default Yup.object().shape({ | |||
| email: Yup.string() | |||
| .email(i18next.t("login.emailFormat")) | |||
| .required(i18next.t("login.emailRequired")), | |||
| }); | |||
| @@ -0,0 +1,11 @@ | |||
| import * as Yup from "yup"; | |||
| import i18next from "i18next"; | |||
| export default Yup.object().shape({ | |||
| email: Yup.string() | |||
| .email(i18next.t("login.emailFormat")) | |||
| .required(i18next.t("login.emailRequired")), | |||
| password: Yup.string() | |||
| .required(i18next.t("login.passwordRequired")) | |||
| .min(8, i18next.t("login.passwordLength")), | |||
| }); | |||
| @@ -10129,6 +10129,11 @@ react-i18next@^11.10.0: | |||
| "@babel/runtime" "^7.14.0" | |||
| html-parse-stringify "^3.0.1" | |||
| react-idle-timer@^5.4.2: | |||
| version "5.4.2" | |||
| resolved "https://registry.yarnpkg.com/react-idle-timer/-/react-idle-timer-5.4.2.tgz#69e20044cf2ecc421aef99cd82298c526d8acf37" | |||
| integrity sha512-ofCS/qpFjm6ZguEyePvtf9YMDnLj7zZfeLXRWGRpsC6Ga47H4dm7EvoUW8MsozGEGy8zCvPK0Sk6YuAnwLEzRQ== | |||
| react-input-autosize@^3.0.0: | |||
| version "3.0.0" | |||
| resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-3.0.0.tgz#6b5898c790d4478d69420b55441fcc31d5c50a85" | |||
| @@ -10271,6 +10276,13 @@ react-select@^4.3.1: | |||
| react-input-autosize "^3.0.0" | |||
| react-transition-group "^4.3.0" | |||
| react-toastify@9.0.3: | |||
| version "9.0.3" | |||
| resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.0.3.tgz#8e6d22651c85cb584c5ebd0b5e2c3bf0d7ec06ee" | |||
| integrity sha512-0QZJk0SqYBxouRBGCFU3ymvjlwimRRhVH7SzqGRiVrQ001KSoUNbGKx9Yq42aoPv18n45yJzEFG82zqv3HnASg== | |||
| dependencies: | |||
| clsx "^1.1.1" | |||
| react-transition-group@^4.3.0, react-transition-group@^4.4.2: | |||
| version "4.4.2" | |||
| resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" | |||