| @@ -9,6 +9,7 @@ import { | |||
| ERROR_PAGE, | |||
| BASE_PAGE, | |||
| GOOGLE_AUTH_CALLBACK_PAGE, | |||
| REGISTER_PAGE, | |||
| } from "./constants/pages"; | |||
| import LoginPage from "./pages/LoginPage/LoginPageMUI"; | |||
| @@ -18,11 +19,13 @@ import ErrorPage from "./pages/ErrorPages/ErrorPage"; | |||
| import ForgotPasswordPage from "./pages/ForgotPasswordPage/ForgotPasswordPageMUI"; | |||
| import PrivateRoute from "./components/Router/PrivateRoute"; | |||
| import GoogleAuthCallback from "./pages/GoogleAuthCallback/GoogleAuthCallback"; | |||
| import RegisterPage from "./pages/RegisterPage/RegisterPageMUI"; | |||
| const AppRoutes = () => ( | |||
| <Switch> | |||
| <Route exact path={BASE_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={NOT_FOUND_PAGE} component={NotFoundPage} /> | |||
| <Route path={ERROR_PAGE} component={ErrorPage} /> | |||
| @@ -1,5 +1,6 @@ | |||
| export const BASE_PAGE = '/'; | |||
| export const LOGIN_PAGE = '/login'; | |||
| export const REGISTER_PAGE = '/register'; | |||
| export const FORGOT_PASSWORD_PAGE = '/forgot-password'; | |||
| export const HOME_PAGE = '/home'; | |||
| export const ERROR_PAGE = '/error-page'; | |||
| @@ -40,6 +40,14 @@ export default { | |||
| 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: { | |||
| welcome: 'React template', | |||
| dontHaveAccount: "Don't have an account? ", | |||
| @@ -0,0 +1,6 @@ | |||
| export default { | |||
| username: "", | |||
| email: "", | |||
| password: "", | |||
| }; | |||
| @@ -10,7 +10,7 @@ import { | |||
| fetchUser, | |||
| } from "../../store/actions/login/loginActions"; | |||
| 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 { | |||
| Box, | |||
| Button, | |||
| @@ -173,7 +173,7 @@ const LoginPage = ({ history }) => { | |||
| sx={{ textAlign: { xs: "center", md: "right" } }} | |||
| > | |||
| <Link | |||
| to="#" | |||
| to={REGISTER_PAGE} | |||
| component={NavLink} | |||
| variant="body2" | |||
| underline="hover" | |||
| @@ -0,0 +1,215 @@ | |||
| /* 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; | |||
| @@ -6,7 +6,7 @@ const request = axios.create({ | |||
| headers: { | |||
| "Content-Type": "application/json", | |||
| }, | |||
| // withCredentials: true, | |||
| withCredentials: true, | |||
| // paramsSerializer: (params) => | |||
| // queryString.stringify(params, { arrayFormat: 'comma' }), | |||
| }); | |||
| @@ -9,14 +9,16 @@ export const getUsernames = (emailorusername) => | |||
| export const attemptLogin = (payload) => | |||
| postRequest(apiEndpoints.authentications.login, payload); | |||
| export const attemptRegister = (payload) => | |||
| postRequest(apiEndpoints.authentications.register, payload); | |||
| export const updateSecurityAnswer = (payload) => | |||
| postRequest(apiEndpoints.authentications.confirmSecurityQuestion, payload); | |||
| export const refreshTokenRequest = (payload) => | |||
| postRequest(apiEndpoints.authentications.refreshToken, payload); | |||
| export const logoutUserRequest = () => | |||
| getRequest(apiEndpoints.users.logout); | |||
| export const logoutUserRequest = () => getRequest(apiEndpoints.users.logout); | |||
| export const generateTokenRequest = (payload) => | |||
| postRequest(apiEndpoints.authentications.generateToken, payload); | |||
| @@ -0,0 +1,18 @@ | |||
| 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"; | |||
| @@ -0,0 +1,30 @@ | |||
| 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, | |||
| }); | |||
| @@ -3,10 +3,12 @@ import loginReducer from './login/loginReducer'; | |||
| import loadingReducer from './loading/loadingReducer'; | |||
| import userReducer from './user/userReducer'; | |||
| import randomDataReducer from './randomData/randomDataReducer'; | |||
| import registerReducer from './register/registerReducer' | |||
| export default combineReducers({ | |||
| login: loginReducer, | |||
| user: userReducer, | |||
| loading:loadingReducer, | |||
| randomData: randomDataReducer | |||
| randomData: randomDataReducer, | |||
| register: registerReducer, | |||
| }); | |||
| @@ -0,0 +1,54 @@ | |||
| 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: '', | |||
| }; | |||
| } | |||
| @@ -1,8 +1,7 @@ | |||
| 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() { | |||
| yield all([ | |||
| loginSaga(), | |||
| ]); | |||
| yield all([loginSaga(), registerSaga()]); | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| 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)]); | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| import { createSelector } from 'reselect'; | |||
| const registerSelector = (state) => state.register; | |||
| export const selectRegisterError = createSelector( | |||
| registerSelector, | |||
| (state) => state.errorMessage, | |||
| ); | |||
| @@ -0,0 +1,12 @@ | |||
| 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")), | |||
| }); | |||