| ERROR_PAGE, | ERROR_PAGE, | ||||
| BASE_PAGE, | BASE_PAGE, | ||||
| GOOGLE_AUTH_CALLBACK_PAGE, | GOOGLE_AUTH_CALLBACK_PAGE, | ||||
| REGISTER_PAGE, | |||||
| } from "./constants/pages"; | } from "./constants/pages"; | ||||
| import LoginPage from "./pages/LoginPage/LoginPageMUI"; | import LoginPage from "./pages/LoginPage/LoginPageMUI"; | ||||
| import ForgotPasswordPage from "./pages/ForgotPasswordPage/ForgotPasswordPageMUI"; | import ForgotPasswordPage from "./pages/ForgotPasswordPage/ForgotPasswordPageMUI"; | ||||
| import PrivateRoute from "./components/Router/PrivateRoute"; | import PrivateRoute from "./components/Router/PrivateRoute"; | ||||
| import GoogleAuthCallback from "./pages/GoogleAuthCallback/GoogleAuthCallback"; | import GoogleAuthCallback from "./pages/GoogleAuthCallback/GoogleAuthCallback"; | ||||
| import RegisterPage from "./pages/RegisterPage/RegisterPageMUI"; | |||||
| const AppRoutes = () => ( | const AppRoutes = () => ( | ||||
| <Switch> | <Switch> | ||||
| <Route exact path={BASE_PAGE} component={LoginPage} /> | <Route exact path={BASE_PAGE} component={LoginPage} /> | ||||
| <Route exact path={LOGIN_PAGE} component={LoginPage} /> | <Route exact path={LOGIN_PAGE} component={LoginPage} /> | ||||
| <Route exact path={REGISTER_PAGE} component={RegisterPage} /> | |||||
| <Route path={GOOGLE_AUTH_CALLBACK_PAGE} component={GoogleAuthCallback} /> | <Route path={GOOGLE_AUTH_CALLBACK_PAGE} component={GoogleAuthCallback} /> | ||||
| <Route path={NOT_FOUND_PAGE} component={NotFoundPage} /> | <Route path={NOT_FOUND_PAGE} component={NotFoundPage} /> | ||||
| <Route path={ERROR_PAGE} component={ErrorPage} /> | <Route path={ERROR_PAGE} component={ErrorPage} /> |
| export const BASE_PAGE = '/'; | export const BASE_PAGE = '/'; | ||||
| export const LOGIN_PAGE = '/login'; | export const LOGIN_PAGE = '/login'; | ||||
| export const REGISTER_PAGE = '/register'; | |||||
| export const FORGOT_PASSWORD_PAGE = '/forgot-password'; | export const FORGOT_PASSWORD_PAGE = '/forgot-password'; | ||||
| export const HOME_PAGE = '/home'; | export const HOME_PAGE = '/home'; | ||||
| export const ERROR_PAGE = '/error-page'; | export const ERROR_PAGE = '/error-page'; |
| range: '{{start}} to {{end}}', | range: '{{start}} to {{end}}', | ||||
| }, | }, | ||||
| }, | }, | ||||
| register: { | |||||
| registerTitle: "Register", | |||||
| usernameRequired: 'Username is required.', | |||||
| emailFormat: 'Invalid email address format.', | |||||
| emailRequired: 'An email or username is required.', | |||||
| passwordLength: 'Your password contain between 8 and 50 characters.', | |||||
| passwordRequired: 'A Password is required.', | |||||
| }, | |||||
| login: { | login: { | ||||
| welcome: 'React template', | welcome: 'React template', | ||||
| dontHaveAccount: "Don't have an account? ", | dontHaveAccount: "Don't have an account? ", |
| export default { | |||||
| username: "", | |||||
| email: "", | |||||
| password: "", | |||||
| }; | |||||
| fetchUser, | fetchUser, | ||||
| } from "../../store/actions/login/loginActions"; | } from "../../store/actions/login/loginActions"; | ||||
| import { selectLoginError } from "../../store/selectors/loginSelectors"; | import { selectLoginError } from "../../store/selectors/loginSelectors"; | ||||
| import { FORGOT_PASSWORD_PAGE, HOME_PAGE } from "../../constants/pages"; | |||||
| import { FORGOT_PASSWORD_PAGE, HOME_PAGE, REGISTER_PAGE } from "../../constants/pages"; | |||||
| import { | import { | ||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| sx={{ textAlign: { xs: "center", md: "right" } }} | sx={{ textAlign: { xs: "center", md: "right" } }} | ||||
| > | > | ||||
| <Link | <Link | ||||
| to="#" | |||||
| to={REGISTER_PAGE} | |||||
| component={NavLink} | component={NavLink} | ||||
| variant="body2" | variant="body2" | ||||
| underline="hover" | underline="hover" |
| /* eslint-disable */ | |||||
| 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 { useTranslation } from "react-i18next"; | |||||
| import { | |||||
| clearRegisterErrors, | |||||
| registerUser, | |||||
| } from "../../store/actions/register/registerActions"; | |||||
| import { selectRegisterError } from "../../store/selectors/registerSelectors"; | |||||
| import { | |||||
| FORGOT_PASSWORD_PAGE, | |||||
| HOME_PAGE, | |||||
| LOGIN_PAGE, | |||||
| } from "../../constants/pages"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Container, | |||||
| Grid, | |||||
| IconButton, | |||||
| InputAdornment, | |||||
| Link, | |||||
| TextField, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { Visibility, VisibilityOff } from "@mui/icons-material"; | |||||
| import Backdrop from "../../components/MUI/BackdropComponent"; | |||||
| import ErrorMessage from "../../components/MUI/ErrorMessageComponent"; | |||||
| import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors"; | |||||
| import { LOGIN_USER_SCOPE } from "../../store/actions/login/loginActionConstants"; | |||||
| import GoogleIcon from "@mui/icons-material/Google"; | |||||
| import registerInitialValues from "../../initialValues/registerInitialValues"; | |||||
| import registerValidation from "../../validations/registerValidation"; | |||||
| import { REGISTER_USER_SCOPE } from "../../store/actions/register/registerActionConstants"; | |||||
| import { makeToastMessage } from "../../util/helpers/toastMessage"; | |||||
| const RegisterPage = ({ history }) => { | |||||
| const dispatch = useDispatch(); | |||||
| const { t } = useTranslation(); | |||||
| const error = useSelector(selectRegisterError); | |||||
| const [showPassword, setShowPassword] = useState(false); | |||||
| const handleClickShowPassword = () => setShowPassword(!showPassword); | |||||
| const handleMouseDownPassword = () => setShowPassword(!showPassword); | |||||
| const handleGoogle = () => { | |||||
| window.location = "http://localhost:1337/api/connect/google"; | |||||
| }; | |||||
| // Clear login errors when user firstly enters the page | |||||
| useEffect(() => { | |||||
| dispatch(clearRegisterErrors()); | |||||
| }, []); | |||||
| const isLoading = useSelector( | |||||
| selectIsLoadingByActionType(REGISTER_USER_SCOPE) | |||||
| ); | |||||
| const handleApiResponseSuccess = () => { | |||||
| history.push({ | |||||
| pathname: LOGIN_PAGE, | |||||
| state: { | |||||
| from: history.location.pathname, | |||||
| }, | |||||
| }); | |||||
| makeToastMessage("User successfuly registered. Please login."); | |||||
| }; | |||||
| const handleSubmit = (values) => { | |||||
| const { username, email, password } = values; | |||||
| dispatch(clearRegisterErrors()); | |||||
| dispatch( | |||||
| registerUser({ username, email, password, handleApiResponseSuccess }) | |||||
| ); | |||||
| }; | |||||
| const formik = useFormik({ | |||||
| initialValues: registerInitialValues, | |||||
| validationSchema: registerValidation, | |||||
| onSubmit: handleSubmit, | |||||
| validateOnBlur: true, | |||||
| enableReinitialize: true, | |||||
| }); | |||||
| return ( | |||||
| <Container component="main" maxWidth="md"> | |||||
| <Box | |||||
| sx={{ | |||||
| marginTop: 32, | |||||
| display: "flex", | |||||
| flexDirection: "column", | |||||
| alignItems: "center", | |||||
| }} | |||||
| > | |||||
| <Typography component="h1" variant="h5"> | |||||
| {t("register.registerTitle")} | |||||
| </Typography> | |||||
| {error && <ErrorMessage error={error} />} | |||||
| <Box | |||||
| component="form" | |||||
| onSubmit={formik.handleSubmit} | |||||
| sx={{ position: "relative", mt: 1, p: 1 }} | |||||
| > | |||||
| <Backdrop position="absolute" isLoading={isLoading} /> | |||||
| <TextField | |||||
| name="username" | |||||
| label={t("common.labelUsername")} | |||||
| margin="normal" | |||||
| value={formik.values.username} | |||||
| onChange={formik.handleChange} | |||||
| error={formik.touched.username && Boolean(formik.errors.username)} | |||||
| helperText={formik.touched.username && formik.errors.username} | |||||
| autoFocus | |||||
| fullWidth | |||||
| /> | |||||
| <TextField | |||||
| name="email" | |||||
| label={t("common.labelEmail")} | |||||
| margin="normal" | |||||
| value={formik.values.email} | |||||
| onChange={formik.handleChange} | |||||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||||
| helperText={formik.touched.email && formik.errors.email} | |||||
| fullWidth | |||||
| /> | |||||
| <TextField | |||||
| name="password" | |||||
| label={t("common.labelPassword")} | |||||
| margin="normal" | |||||
| type={showPassword ? "text" : "password"} | |||||
| value={formik.values.password} | |||||
| onChange={formik.handleChange} | |||||
| error={formik.touched.password && Boolean(formik.errors.password)} | |||||
| helperText={formik.touched.password && formik.errors.password} | |||||
| fullWidth | |||||
| InputProps={{ | |||||
| endAdornment: ( | |||||
| <InputAdornment position="end"> | |||||
| <IconButton | |||||
| onClick={handleClickShowPassword} | |||||
| onMouseDown={handleMouseDownPassword} | |||||
| > | |||||
| {showPassword ? <Visibility /> : <VisibilityOff />} | |||||
| </IconButton> | |||||
| </InputAdornment> | |||||
| ), | |||||
| }} | |||||
| /> | |||||
| <Button | |||||
| type="submit" | |||||
| variant="contained" | |||||
| sx={{ mt: 3, mb: 2 }} | |||||
| fullWidth | |||||
| > | |||||
| {t("register.registerTitle")} | |||||
| </Button> | |||||
| <Button | |||||
| onClick={handleGoogle} | |||||
| startIcon={<GoogleIcon />} | |||||
| fullWidth | |||||
| variant="outlined" | |||||
| > | |||||
| Connect with Google | |||||
| </Button> | |||||
| <Grid container> | |||||
| <Grid | |||||
| item | |||||
| xs={12} | |||||
| md={6} | |||||
| sx={{ textAlign: { xs: "center", md: "left" } }} | |||||
| > | |||||
| <Link | |||||
| to={FORGOT_PASSWORD_PAGE} | |||||
| component={NavLink} | |||||
| variant="body2" | |||||
| underline="hover" | |||||
| > | |||||
| {t("login.forgotYourPassword")} | |||||
| </Link> | |||||
| </Grid> | |||||
| <Grid | |||||
| item | |||||
| xs={12} | |||||
| md={6} | |||||
| sx={{ textAlign: { xs: "center", md: "right" } }} | |||||
| > | |||||
| <Link | |||||
| to={LOGIN_PAGE} | |||||
| component={NavLink} | |||||
| variant="body2" | |||||
| underline="hover" | |||||
| > | |||||
| {t("login.logIn")} | |||||
| </Link> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </Box> | |||||
| </Container> | |||||
| ); | |||||
| }; | |||||
| RegisterPage.propTypes = { | |||||
| history: PropTypes.shape({ | |||||
| replace: PropTypes.func, | |||||
| push: PropTypes.func, | |||||
| location: PropTypes.shape({ | |||||
| pathname: PropTypes.string, | |||||
| }), | |||||
| }), | |||||
| }; | |||||
| export default RegisterPage; |
| headers: { | headers: { | ||||
| "Content-Type": "application/json", | "Content-Type": "application/json", | ||||
| }, | }, | ||||
| // withCredentials: true, | |||||
| withCredentials: true, | |||||
| // paramsSerializer: (params) => | // paramsSerializer: (params) => | ||||
| // queryString.stringify(params, { arrayFormat: 'comma' }), | // queryString.stringify(params, { arrayFormat: 'comma' }), | ||||
| }); | }); |
| export const attemptLogin = (payload) => | export const attemptLogin = (payload) => | ||||
| postRequest(apiEndpoints.authentications.login, payload); | postRequest(apiEndpoints.authentications.login, payload); | ||||
| export const attemptRegister = (payload) => | |||||
| postRequest(apiEndpoints.authentications.register, payload); | |||||
| export const updateSecurityAnswer = (payload) => | export const updateSecurityAnswer = (payload) => | ||||
| postRequest(apiEndpoints.authentications.confirmSecurityQuestion, payload); | postRequest(apiEndpoints.authentications.confirmSecurityQuestion, payload); | ||||
| export const refreshTokenRequest = (payload) => | export const refreshTokenRequest = (payload) => | ||||
| postRequest(apiEndpoints.authentications.refreshToken, payload); | postRequest(apiEndpoints.authentications.refreshToken, payload); | ||||
| export const logoutUserRequest = () => | |||||
| getRequest(apiEndpoints.users.logout); | |||||
| export const logoutUserRequest = () => getRequest(apiEndpoints.users.logout); | |||||
| export const generateTokenRequest = (payload) => | export const generateTokenRequest = (payload) => | ||||
| postRequest(apiEndpoints.authentications.generateToken, payload); | postRequest(apiEndpoints.authentications.generateToken, payload); |
| import { | |||||
| createClearType, | |||||
| createErrorType, | |||||
| createFetchType, | |||||
| createLoadingType, | |||||
| createSuccessType, | |||||
| } from "../actionHelpers"; | |||||
| export const REGISTER_USER_SCOPE = "REGISTER_USER"; | |||||
| export const REGISTER_USER_FETCH = createFetchType(REGISTER_USER_SCOPE); | |||||
| export const REGISTER_USER_SUCCESS = createSuccessType(REGISTER_USER_SCOPE); | |||||
| export const REGISTER_USER_ERROR = createErrorType(REGISTER_USER_SCOPE); | |||||
| export const CLEAR_REGISTER_USER_ERROR = createClearType( | |||||
| `${REGISTER_USER_SCOPE}_ERROR` | |||||
| ); | |||||
| export const REGISTER_USER_LOADING = createLoadingType(REGISTER_USER_SCOPE); | |||||
| export const RESET_REGISTER_STATE = "RESET_REGISTER_STATE"; |
| import { | |||||
| CLEAR_REGISTER_USER_ERROR, | |||||
| REGISTER_USER_ERROR, | |||||
| REGISTER_USER_FETCH, | |||||
| REGISTER_USER_SUCCESS, | |||||
| RESET_REGISTER_STATE, | |||||
| } from "./registerActionConstants"; | |||||
| export const registerUser = (payload) => ({ | |||||
| type: REGISTER_USER_FETCH, | |||||
| payload, | |||||
| }); | |||||
| export const registerUserSuccess = (payload) => ({ | |||||
| type: REGISTER_USER_SUCCESS, | |||||
| payload, | |||||
| }); | |||||
| export const registerUserError = (payload) => ({ | |||||
| type: REGISTER_USER_ERROR, | |||||
| payload, | |||||
| }); | |||||
| export const resetRegisterState = () => ({ | |||||
| type: RESET_REGISTER_STATE, | |||||
| }); | |||||
| export const clearRegisterErrors = () => ({ | |||||
| type: CLEAR_REGISTER_USER_ERROR, | |||||
| }); |
| import loadingReducer from './loading/loadingReducer'; | import loadingReducer from './loading/loadingReducer'; | ||||
| import userReducer from './user/userReducer'; | import userReducer from './user/userReducer'; | ||||
| import randomDataReducer from './randomData/randomDataReducer'; | import randomDataReducer from './randomData/randomDataReducer'; | ||||
| import registerReducer from './register/registerReducer' | |||||
| export default combineReducers({ | export default combineReducers({ | ||||
| login: loginReducer, | login: loginReducer, | ||||
| user: userReducer, | user: userReducer, | ||||
| loading:loadingReducer, | loading:loadingReducer, | ||||
| randomData: randomDataReducer | |||||
| randomData: randomDataReducer, | |||||
| register: registerReducer, | |||||
| }); | }); |
| import createReducer from '../../utils/createReducer'; | |||||
| import { | |||||
| CLEAR_REGISTER_USER_ERROR, | |||||
| REGISTER_USER_ERROR, | |||||
| REGISTER_USER_SUCCESS, | |||||
| RESET_REGISTER_STATE, | |||||
| } from '../../actions/register/registerActionConstants'; | |||||
| const initialState = { | |||||
| token: { | |||||
| JwtToken: '', | |||||
| }, | |||||
| errorMessage: '', | |||||
| }; | |||||
| export default createReducer( | |||||
| { | |||||
| [REGISTER_USER_SUCCESS]: setUser, | |||||
| [RESET_REGISTER_STATE]: resetRegisterState, | |||||
| [REGISTER_USER_ERROR]: setError, | |||||
| [CLEAR_REGISTER_USER_ERROR]: clearRegisterErrors, | |||||
| }, | |||||
| initialState, | |||||
| ); | |||||
| function setUser(state, action) { | |||||
| return { | |||||
| ...state, | |||||
| token: { | |||||
| ...state.token, | |||||
| JwtToken: action.payload.jwt, | |||||
| }, | |||||
| }; | |||||
| } | |||||
| function setError(state, action) { | |||||
| return { | |||||
| ...state, | |||||
| errorMessage: action.payload, | |||||
| }; | |||||
| } | |||||
| function resetRegisterState() { | |||||
| return initialState; | |||||
| } | |||||
| function clearRegisterErrors(state) { | |||||
| return { | |||||
| ...state, | |||||
| errorMessage: '', | |||||
| }; | |||||
| } |
| import { all } from 'redux-saga/effects'; | |||||
| import loginSaga from './loginSaga'; | |||||
| import { all } from "redux-saga/effects"; | |||||
| import loginSaga from "./loginSaga"; | |||||
| import registerSaga from "./registerSaga"; | |||||
| export default function* rootSaga() { | export default function* rootSaga() { | ||||
| yield all([ | |||||
| loginSaga(), | |||||
| ]); | |||||
| yield all([loginSaga(), registerSaga()]); | |||||
| } | } |
| import { all, call, put, takeLatest } from "@redux-saga/core/effects"; | |||||
| import { REGISTER_USER_FETCH } from "../actions/register/registerActionConstants"; | |||||
| import { attemptRegister } from "../../request/loginRequest"; | |||||
| import { | |||||
| registerUserError, | |||||
| registerUserSuccess, | |||||
| } from "../actions/register/registerActions"; | |||||
| import { JWT_TOKEN } from "../../constants/localStorage"; | |||||
| import { authScopeSetHelper } from "../../util/helpers/authScopeHelpers"; | |||||
| import { rejectErrorCodeHelper } from "../../util/helpers/rejectErrorCodeHelper"; | |||||
| function* registerUser({ payload }) { | |||||
| try { | |||||
| const { data } = yield call(attemptRegister, payload); | |||||
| if (data?.jwt) { | |||||
| yield call(authScopeSetHelper, JWT_TOKEN, data.jwt); | |||||
| } | |||||
| yield put(registerUserSuccess(data.jwt)); | |||||
| if (payload.handleApiResponseSuccess) { | |||||
| yield call(payload.handleApiResponseSuccess); | |||||
| } | |||||
| } catch (e) { | |||||
| if (e.response && e.response.data) { | |||||
| const errorMessage = yield call(rejectErrorCodeHelper, e); | |||||
| yield put(registerUserError(errorMessage)); | |||||
| } | |||||
| } | |||||
| } | |||||
| export default function* registerSaga() { | |||||
| yield all([takeLatest(REGISTER_USER_FETCH, registerUser)]); | |||||
| } |
| import { createSelector } from 'reselect'; | |||||
| const registerSelector = (state) => state.register; | |||||
| export const selectRegisterError = createSelector( | |||||
| registerSelector, | |||||
| (state) => state.errorMessage, | |||||
| ); |
| import * as Yup from "yup"; | |||||
| import i18next from "i18next"; | |||||
| export default Yup.object().shape({ | |||||
| username: Yup.string().required("register.usernameRequired"), | |||||
| email: Yup.string() | |||||
| .email(i18next.t("register.emailFormat")) | |||||
| .required(i18next.t("register.emailRequired")), | |||||
| password: Yup.string() | |||||
| .required(i18next.t("register.passwordRequired")) | |||||
| .min(8, i18next.t("register.passwordLength")), | |||||
| }); |