소스 검색

Added helper functions, interceptors and validators

master
Lazar Kostic 3 년 전
부모
커밋
0811622a44

+ 2
- 0
package.json 파일 보기

@@ -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",

+ 18
- 15
src/App.js 파일 보기

@@ -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;

+ 162
- 151
src/components/MUI/NavbarComponent.js 파일 보기

@@ -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;

+ 6
- 1
src/i18n/resources/en.js 파일 보기

@@ -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"
}
};

+ 4
- 0
src/initialValues/forgotPasswordInitialValues.js 파일 보기

@@ -0,0 +1,4 @@
export default {
email: "",
};

+ 4
- 0
src/initialValues/loginInitialValues.js 파일 보기

@@ -0,0 +1,4 @@
export default {
email: "",
password: "",
};

+ 4
- 12
src/pages/ForgotPasswordPage/ForgotPasswordPageMUI.js 파일 보기

@@ -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,

+ 32
- 32
src/pages/HomePage/HomePageMUI.js 파일 보기

@@ -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;

+ 15
- 26
src/pages/LoginPage/LoginPageMUI.js 파일 보기

@@ -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
/>

+ 56
- 3
src/request/index.js 파일 보기

@@ -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;

+ 19
- 11
src/store/index.js 파일 보기

@@ -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);

+ 29
- 0
src/store/middleware/accessTokenMiddleware.js 파일 보기

@@ -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);
};

+ 25
- 0
src/store/middleware/authenticationMiddleware.js 파일 보기

@@ -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);
};

+ 9
- 6
src/store/middleware/internalServerErrorMiddleware.js 파일 보기

@@ -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);
};

+ 24
- 19
src/store/middleware/requestStatusMiddleware.js 파일 보기

@@ -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);
};

+ 0
- 3
src/store/saga/loginSaga.js 파일 보기

@@ -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));
}

+ 38
- 0
src/util/helpers/routeHelpers.js 파일 보기

@@ -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)
);
};

+ 14
- 0
src/util/helpers/toastMessage.js 파일 보기

@@ -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);

+ 8
- 0
src/validations/forgotPasswordValidation.js 파일 보기

@@ -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")),
});

+ 11
- 0
src/validations/loginValidation.js 파일 보기

@@ -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")),
});

+ 12
- 0
yarn.lock 파일 보기

@@ -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"

Loading…
취소
저장