From d2aa72f5d96d5c509d65fa3a804b644c51cf1798 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 20 Jul 2020 13:52:24 -0700 Subject: [PATCH] Added sagas + pages for password reset. WIP BOD-165 --- bodyshop_translations.babel | 105 ++++++++++++++++++ client/src/App/App.js | 4 + .../sign-in-form/sign-in-form.component.jsx | 14 ++- .../user-request-reset-pw.component.jsx | 70 ++++++++++++ .../user-validate-pw-reset.component.jsx | 85 ++++++++++++++ client/src/firebase/firebase.utils.js | 2 + .../reset-password.component.jsx | 31 ++++++ client/src/redux/user/user.actions.js | 26 +++++ client/src/redux/user/user.reducer.js | 24 ++++ client/src/redux/user/user.sagas.js | 41 +++++++ client/src/redux/user/user.selectors.js | 5 + client/src/redux/user/user.types.js | 8 +- client/src/translations/en_us/common.json | 5 + client/src/translations/es/common.json | 5 + client/src/translations/fr/common.json | 5 + 15 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 client/src/components/user-request-pw-reset/user-request-reset-pw.component.jsx create mode 100644 client/src/components/user-validate-pw-reset/user-validate-pw-reset.component.jsx create mode 100644 client/src/pages/reset-password/reset-password.component.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 08d6a5ad2..ba556935d 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -5662,6 +5662,27 @@ + + resetpassword + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + save false @@ -5924,6 +5945,27 @@ + + confirmpassword + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + email false @@ -6218,6 +6260,69 @@ + + passwordresetsuccess + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + passwordresetsuccess_sub + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + passwordsdonotmatch + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + print false diff --git a/client/src/App/App.js b/client/src/App/App.js index 0ecdf6147..eac4b499d 100644 --- a/client/src/App/App.js +++ b/client/src/App/App.js @@ -15,6 +15,9 @@ import PrivateRoute from "../utils/private-route"; import "./App.styles.scss"; const LandingPage = lazy(() => import("../pages/landing/landing.page")); +const ResetPassword = lazy(() => + import("../pages/reset-password/reset-password.component") +); const ManagePage = lazy(() => import("../pages/manage/manage.page.container")); const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page")); const Unauthorized = lazy(() => @@ -51,6 +54,7 @@ export function App({ checkUserSession, currentUser }) { + ({ emailSignInStart: (email, password) => dispatch(emailSignInStart({ email, password })), + sendPasswordReset: (email) => dispatch(sendPasswordReset(email)), }); export function SignInComponent({ emailSignInStart, currentUser, signInError, + sendPasswordReset, }) { const apolloClient = useApolloClient(); const { t } = useTranslation(); @@ -39,6 +44,7 @@ export function SignInComponent({ }; const [form] = Form.useForm(); + //TODO - may be able to run this only on new user creation. if (currentUser.authorized === true) { apolloClient .mutate({ @@ -55,6 +61,7 @@ export function SignInComponent({ } if (currentUser.authorized === true) return ; + return (
@@ -90,6 +97,9 @@ export function SignInComponent({ {t("general.actions.login")} + + +
); } diff --git a/client/src/components/user-request-pw-reset/user-request-reset-pw.component.jsx b/client/src/components/user-request-pw-reset/user-request-reset-pw.component.jsx new file mode 100644 index 000000000..f885dde72 --- /dev/null +++ b/client/src/components/user-request-pw-reset/user-request-reset-pw.component.jsx @@ -0,0 +1,70 @@ +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + sendPasswordReset, + validatePasswordResetStart, +} from "../../redux/user/user.actions"; +import queryString from "query-string"; +import { useLocation } from "react-router-dom"; +import { Input, Form, Button, Result } from "antd"; +import React, { useState } from "react"; +import EmailFormItemComponent from "../form-items-formatted/email-form-item.component"; +import { useTranslation } from "react-i18next"; +import { selectPasswordReset } from "../../redux/user/user.selectors"; +import AlertComponent from "../alert/alert.component"; + +const mapStateToProps = createStructuredSelector({ + passwordReset: selectPasswordReset, +}); +const mapDispatchToProps = (dispatch) => ({ + sendPasswordReset: (email) => dispatch(sendPasswordReset(email)), +}); + +export function UserRequestResetPw({ passwordReset, sendPasswordReset }) { + const handleFinish = (values) => { + try { + sendPasswordReset(values.email); + } catch (error) { + console.log(error); + } + }; + + const { t } = useTranslation(); + if (passwordReset.success) + return ( + sendPasswordReset(passwordReset.email)}> + {t("general.labels.sendagain")} + , + ]} + /> + ); + + return ( +
+
+ + + + {passwordReset.error ? ( + + ) : null} + + +
+ ); +} +export default connect(mapStateToProps, mapDispatchToProps)(UserRequestResetPw); diff --git a/client/src/components/user-validate-pw-reset/user-validate-pw-reset.component.jsx b/client/src/components/user-validate-pw-reset/user-validate-pw-reset.component.jsx new file mode 100644 index 000000000..e3c0a1fa7 --- /dev/null +++ b/client/src/components/user-validate-pw-reset/user-validate-pw-reset.component.jsx @@ -0,0 +1,85 @@ +import { LockOutlined } from "@ant-design/icons"; +import { Button, Form, Input } from "antd"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { validatePasswordResetStart } from "../../redux/user/user.actions"; +import { selectPasswordReset } from "../../redux/user/user.selectors"; +import AlertComponent from "../alert/alert.component"; + +const mapStateToProps = createStructuredSelector({ + passwordReset: selectPasswordReset, +}); +const mapDispatchToProps = (dispatch) => ({ + validatePasswordReset: (emailAndPin) => + dispatch(validatePasswordResetStart(emailAndPin)), +}); + +export function UserValidatePwReset({ + passwordReset, + validatePasswordReset, + oobCode, +}) { + console.log("UserValidatePwReset -> oobCode", oobCode); + const { t } = useTranslation(); + + const handleFinish = (values) => { + validatePasswordReset({ code: oobCode, password: values.password }); + }; + + return ( +
+
+ + } + type='password' + placeholder={t("general.labels.password")} + /> + + ({ + validator(rule, value) { + if (!value || getFieldValue("password") === value) { + return Promise.resolve(); + } + return Promise.reject(t("general.labels.passwordsdonotmatch")); + }, + }), + ]}> + } + type='password' + placeholder={t("general.labels.password")} + /> + + {passwordReset.error ? ( + + ) : null} + + +
+ ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(UserValidatePwReset); diff --git a/client/src/firebase/firebase.utils.js b/client/src/firebase/firebase.utils.js index bc0f56536..78f6f6517 100644 --- a/client/src/firebase/firebase.utils.js +++ b/client/src/firebase/firebase.utils.js @@ -24,6 +24,8 @@ export const getCurrentUser = () => { }); }; + + export const updateCurrentUser = (userDetails) => { return new Promise((resolve, reject) => { const unsubscribe = auth.onAuthStateChanged((userAuth) => { diff --git a/client/src/pages/reset-password/reset-password.component.jsx b/client/src/pages/reset-password/reset-password.component.jsx new file mode 100644 index 000000000..534652c8d --- /dev/null +++ b/client/src/pages/reset-password/reset-password.component.jsx @@ -0,0 +1,31 @@ +import React from "react"; + +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + sendPasswordReset, + validatePasswordResetStart, +} from "../../redux/user/user.actions"; +import queryString from "query-string"; +import { useLocation } from "react-router-dom"; +import UserValidatePwReset from "../../components/user-validate-pw-reset/user-validate-pw-reset.component"; +import UserRequestResetPw from "../../components/user-request-pw-reset/user-request-reset-pw.component"; + +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser +}); +const mapDispatchToProps = (dispatch) => ({ + sendPasswordReset: (email) => dispatch(sendPasswordReset(email)), + validatePasswordReset: (emailAndPin) => + dispatch(validatePasswordResetStart(emailAndPin)), +}); + +export default function ResetPassword({}) { + const searchParams = queryString.parse(useLocation().search); + const { mode, oobCode } = searchParams; + console.log("ResetPassword -> mode, oobCode", mode, oobCode); + + if (mode === "passwordReset") + return ; + return ; +} diff --git a/client/src/redux/user/user.actions.js b/client/src/redux/user/user.actions.js index ace9cd0a4..ddd7c0f95 100644 --- a/client/src/redux/user/user.actions.js +++ b/client/src/redux/user/user.actions.js @@ -72,3 +72,29 @@ export const setLocalFingerprint = (fingerprint) => ({ type: UserActionTypes.SET_LOCAL_FINGERPRINT, payload: fingerprint, }); + +export const sendPasswordReset = (email) => ({ + type: UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START, + payload: email, +}); +export const sendPasswordResetFailure = (error) => ({ + type: UserActionTypes.SEND_PASSWORD_RESET_EMAIL_FAILURE, + payload: error, +}); +export const sendPasswordResetSuccess = () => ({ + type: UserActionTypes.SEND_PASSWORD_RESET_EMAIL_SUCCESS, +}); + +export const validatePasswordResetStart = (emailAndPin) => ({ + type: UserActionTypes.VALIDATE_PASSWORD_RESET_START, + payload: emailAndPin, +}); + +export const validatePasswordResetSuccess = () => ({ + type: UserActionTypes.VALIDATE_PASSWORD_RESET_SUCCESS, +}); + +export const validatePasswordResetFailure = (error) => ({ + type: UserActionTypes.VALIDATE_PASSWORD_RESET_FAILURE, + payload: error, +}); diff --git a/client/src/redux/user/user.reducer.js b/client/src/redux/user/user.reducer.js index e8976546a..ac5c2421f 100644 --- a/client/src/redux/user/user.reducer.js +++ b/client/src/redux/user/user.reducer.js @@ -9,6 +9,11 @@ const INITIAL_STATE = { fingerprint: null, error: null, conflict: false, + passwordreset: { + email: null, + error: null, + success: false, + }, }; const userReducer = (state = INITIAL_STATE, action) => { @@ -19,6 +24,25 @@ const userReducer = (state = INITIAL_STATE, action) => { return { ...state, conflict: false }; case UserActionTypes.SET_INSTANCE_CONFLICT: return { ...state, conflict: true }; + case UserActionTypes.VALIDATE_PASSWORD_RESET_START: + case UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START: + return { + ...state, + passwordreset: { + email: action.payload, + error: null, + success: false, + }, + }; + case UserActionTypes.VALIDATE_PASSWORD_RESET_FAILURE: + case UserActionTypes.SEND_PASSWORD_RESET_EMAIL_FAILURE: + return { ...state, passwordreset: { error: action.payload } }; + case UserActionTypes.VALIDATE_PASSWORD_RESET_SUCCESS: + case UserActionTypes.SEND_PASSWORD_RESET_EMAIL_SUCCESS: + return { + ...state, + passwordreset: { ...state.passwordreset, success: true }, + }; case UserActionTypes.SIGN_IN_SUCCESS: return { ...state, diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js index 0a61b504f..a1e6a59bb 100644 --- a/client/src/redux/user/user.sagas.js +++ b/client/src/redux/user/user.sagas.js @@ -19,6 +19,10 @@ import { signOutSuccess, unauthorizedUser, updateUserDetailsSuccess, + sendPasswordResetFailure, + sendPasswordResetSuccess, + validatePasswordResetSuccess, + validatePasswordResetFailure } from "./user.actions"; import UserActionTypes from "./user.types"; @@ -161,6 +165,41 @@ export function* signInSuccessSaga({ payload }) { yield logImEXEvent("redux_sign_in_success"); } +export function* onSendPasswordResetStart() { + yield takeLatest( + UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START, + sendPasswordResetEmail + ); +} +export function* sendPasswordResetEmail({ payload }) { + try { + yield auth.sendPasswordResetEmail(payload, { + url: "https://imex.online/passwordreset", + }); + console.log("Good should send."); + yield put(sendPasswordResetSuccess()); + } catch (error) { + yield put(sendPasswordResetFailure(error.message)); + } +} + +export function* onValidatePasswordResetStart() { + yield takeLatest( + UserActionTypes.VALIDATE_PASSWORD_RESET_START, + validatePasswordResetStart + ); +} +export function* validatePasswordResetStart({ payload: { password, code } }) { + try { + yield auth.confirmPasswordReset(code, password); + console.log("Good should send."); + yield put(validatePasswordResetSuccess()); + } catch (error) { + console.log("function*validatePasswordResetStart -> error", error); + yield put(validatePasswordResetFailure(error.message)); + } +} + export function* userSagas() { yield all([ call(onEmailSignInStart), @@ -170,5 +209,7 @@ export function* userSagas() { call(onSetInstanceId), call(onCheckInstanceId), call(onSignInSuccess), + call(onSendPasswordResetStart), + call(onValidatePasswordResetStart) ]); } diff --git a/client/src/redux/user/user.selectors.js b/client/src/redux/user/user.selectors.js index 17b3f4e61..6d4a0bc19 100644 --- a/client/src/redux/user/user.selectors.js +++ b/client/src/redux/user/user.selectors.js @@ -21,3 +21,8 @@ export const selectInstanceConflict = createSelector( [selectUser], (user) => user.conflict ); + +export const selectPasswordReset = createSelector( + [selectUser], + (user) => user.passwordreset +); diff --git a/client/src/redux/user/user.types.js b/client/src/redux/user/user.types.js index 18a806960..e394beb13 100644 --- a/client/src/redux/user/user.types.js +++ b/client/src/redux/user/user.types.js @@ -19,6 +19,12 @@ const UserActionTypes = { SET_INSTANCE_ID: "SET_INSTANCE_ID", CHECK_INSTANCE_ID: "CHECK_INSTANCE_ID", SET_INSTANCE_CONFLICT: "SET_INSTANCE_CONFLICT", - SET_LOCAL_FINGERPRINT: "SET_LOCAL_FINGERPRINT" + SET_LOCAL_FINGERPRINT: "SET_LOCAL_FINGERPRINT", + SEND_PASSWORD_RESET_EMAIL_START: "SEND_PASSWORD_RESET_EMAIL_START", + SEND_PASSWORD_RESET_EMAIL_FAILURE: "SEND_PASSWORD_RESET_EMAIL_FAILURE", + SEND_PASSWORD_RESET_EMAIL_SUCCESS: "SEND_PASSWORD_RESET_EMAIL_SUCCESS", + VALIDATE_PASSWORD_RESET_START: "VALIDATE_PASSWORD_RESET_START", + VALIDATE_PASSWORD_RESET_SUCCESS: "VALIDATE_PASSWORD_RESET_SUCCESS", + VALIDATE_PASSWORD_RESET_FAILURE: "VALIDATE_PASSWORD_RESET_FAILURE", }; export default UserActionTypes; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 88cf79d66..61832f502 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -385,6 +385,7 @@ "login": "Login", "refresh": "Refresh", "reset": "Reset your changes.", + "resetpassword": "Reset Password", "save": "Save", "saveandnew": "Save and New", "submit": "Submit", @@ -401,6 +402,7 @@ "actions": "Actions", "areyousure": "Are you sure?", "barcode": "Barcode", + "confirmpassword": "Confirm Password", "email": "Email", "errors": "Errors", "exceptiontitle": "An error has occurred.", @@ -415,6 +417,9 @@ "no": "No", "out": "Out", "password": "Password", + "passwordresetsuccess": "A password reset link has been sent to you.", + "passwordresetsuccess_sub": "You should receive this email in the next few minutes. Please check your email including any junk or spam folders. ", + "passwordsdonotmatch": "The passwords you have entered do not match.", "print": "Print", "search": "Search...", "selectdate": "Select date...", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index e5ced7a0e..461f93938 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -385,6 +385,7 @@ "login": "", "refresh": "", "reset": " Restablecer a original.", + "resetpassword": "", "save": "Salvar", "saveandnew": "", "submit": "", @@ -401,6 +402,7 @@ "actions": "Comportamiento", "areyousure": "", "barcode": "código de barras", + "confirmpassword": "", "email": "", "errors": "", "exceptiontitle": "", @@ -415,6 +417,9 @@ "no": "", "out": "Afuera", "password": "", + "passwordresetsuccess": "", + "passwordresetsuccess_sub": "", + "passwordsdonotmatch": "", "print": "", "search": "Buscar...", "selectdate": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index a2d2a284f..bc88efce5 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -385,6 +385,7 @@ "login": "", "refresh": "", "reset": " Rétablir l'original.", + "resetpassword": "", "save": "sauvegarder", "saveandnew": "", "submit": "", @@ -401,6 +402,7 @@ "actions": "actes", "areyousure": "", "barcode": "code à barre", + "confirmpassword": "", "email": "", "errors": "", "exceptiontitle": "", @@ -415,6 +417,9 @@ "no": "", "out": "En dehors", "password": "", + "passwordresetsuccess": "", + "passwordresetsuccess_sub": "", + "passwordsdonotmatch": "", "print": "", "search": "Chercher...", "selectdate": "",