1

I am implementing a password reset functionality in a MERN app. When a user enters the email address for which they want to reset the password, they get a rest password link in their mail. Now, when they visit that link, they should see the PasswordResetFormSecond component rendered on the screen. (irrespective of whether the token is valid or not).

However, when I am visiting the path "/account/reset/:token", I don't see the PasswordResetFormSecond being rendered on the screen. I get the correct server responses however. Also, the redux store is not found. enter image description here What am I doing wrong?

Code snippets are given below:

client/src/components/PasswordResetFormsecond.js

import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useFormik } from "formik";
import * as Yup from "yup";
import {
  fetchPasswordResetMount,
  fetchPasswordResetSubmit,
} from "./stateSlices/passwordResetPasswordSlice";

const PasswordResetFormSecond = ({ history, match }) => {
  const { successMount, errorMount, successSubmit, errorSubmit } = useSelector(
    (state) => state.passwordResetPasswordStage
  );

  const dispatch = useDispatch();

  useState(() => {
    dispatch(fetchPasswordResetMount(match.params.token));
  }, []);

  const formik = useFormik({
    initialValues: {
      password: "",
      confirmPassword: "",
    },
    validationSchema: Yup.object({
      password: Yup.string().required("Please enter your password"),
      confirmPassword: Yup.string().required("Please enter your password"),
    }),
    onSubmit: async (values, { resetForm }) => {
      const { password, confirmPassword } = values;
      dispatch(
        fetchPasswordResetSubmit({
          password,
          confirmPassword,
          token: match.params.token,
        })
      );
      if (successSubmit) {
        history.push("/registerLogin");
      }
    },
  });

  let condition = successMount || errorMount;

  return (
    <div className="col-10 col-sm-8 col-md-5 mx-auto">
      {condition && (
        <div className="login-form-wrapper">
          <div className="col-10 col-sm-8 col-md-5 mx-auto">
            <h1 className="font-weight-bold">Reset Password</h1>
          </div>
          <form onSubmit={formik.handleSubmit}>
            <div className="form-group col-10 col-sm-8 col-md-5 mx-auto mt-5">
              {errorSubmit && (
                <div className="alert alert-danger" role="alert">
                  {errorSubmit}
                </div>
              )}
            </div>
            <div className="form-group col-10 col-sm-8 col-md-5 mx-auto">
              <label htmlFor="password">Password</label>
              <input
                className="form-control form-control-lg"
                id="password"
                name="password"
                type="password"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.password}
              />
              {formik.touched.password && formik.errors.password ? (
                <small className="form-text text-danger">
                  {formik.errors.password}
                </small>
              ) : null}
            </div>
            <div className="form-group col-10 col-sm-8 col-md-5 mx-auto">
              <label htmlFor="confirmPassword">Confirm Password</label>
              <input
                className="form-control form-control-lg"
                id="confirmPassword"
                name="confirmPassword"
                type="password"
                onChange={formik.handleChange}
                onBlur={formik.handleBlur}
                value={formik.values.password}
              />
              {formik.touched.confirmPassword &&
              formik.errors.confirmPassword ? (
                <small className="form-text text-danger">
                  {formik.errors.confirmPassword}
                </small>
              ) : null}
            </div>

            <div className="col-10 col-sm-8 col-md-5 mx-auto">
              <button
                type="submit"
                className="btn btn-lg btn-primary btn-block login-button"
              >
                Reset Password
              </button>
            </div>
          </form>
        </div>
      )}
    </div>
  );
};

export default PasswordResetFormSecond;

client/src/App.js

import React, { useState } from "react";
import Header from "./components/Header";
import Home from "./components/Home";
import About from "./components/About";
import CV from "./components/CV";
import Projects from "./components/Projects";
import RegisterForm from "./components/RegisterForm";
import LoginForm from "./components/LoginForm";
import PasswordResetFormFirst from "./components/PasswordResetFormFirst";
import PasswordResetFormSecond from "./components/PasswordResetFormSecond";
import { Route, Switch } from "react-router-dom";

const App = () => {
  const [menuOpen, setMenuOpen] = useState(false);
  const handleMenuClick = () => {
    setMenuOpen(!menuOpen);
  };

  const handleOverlayClick = () => {
    setMenuOpen(!menuOpen);
  };

  const handleSidedrawerNavbarLinkClick = () => {
    setMenuOpen(!menuOpen);
  };
  return (
    <>
      <Header
        menuOpen={menuOpen}
        onMenuClick={handleMenuClick}
        onSidedrawerNavbarLinkClick={handleSidedrawerNavbarLinkClick}
        onOverlayClick={handleOverlayClick}
      />
      <Switch>
        <Route
          path="/account/reset/:token"
          component={PasswordResetFormSecond}
        />
        <Route path="/account/forgot" component={PasswordResetFormFirst} />
        <Route path="/about" component={About} />
        <Route path="/cv" component={CV} />
        <Route path="/projects" component={Projects} />
        <Route path="/registerLogin" component={LoginForm} />
        <Route path="/register" component={RegisterForm} />
        <Route path="/" exact component={Home} />
      </Switch>
    </>
  );
};

export default App;

client/src/staeSlices/passwordResetPasswordSlice.js

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

const initialState = {
  user: null,
  successMount: null,
  successSubmit: null,
  errorMount: null,
  updatedUser: null,
  errorSubmit: null,
};

export const fetchPasswordResetMount = createAsyncThunk(
  "passwordReset/fetchPasswordResetMount",
  async (token, { rejectWithValue }) => {
    try {
      const { data } = await axios.get(`/account/reset/${token}`);
      return data;
    } catch (err) {
      return rejectWithValue(err.response.data);
    }
  }
);
export const fetchPasswordResetSubmit = createAsyncThunk(
  "passwordResetPassword/fetchPasswordResetInfo",
  async ({ password, confirmPassword, token }, { rejectWithValue }) => {
    try {
      const { data } = await axios.post(`/account/reset/${token}`, {
        password,
        confirmPassword,
      });
      return data;
    } catch (err) {
      return rejectWithValue(err.response.data);
    }
  }
);

export const passwordResetSlice = createSlice({
  name: "passwordReset",
  initialState,
  reducers: {},
  extraReducers: {
    [fetchPasswordResetMount.fulfilled]: (state, action) => {
      state.user = action.payload;
      state.successMount = true;
    },
    [fetchPasswordResetMount.rejected]: (state, action) => {
      state.errorMount = action.payload.message;
    },
    [fetchPasswordResetSubmit.fulfilled]: (state, action) => {
      state.updatedUser = action.payload;
      state.successSubmit = true;
    },
    [fetchPasswordResetSubmit.rejected]: (state, action) => {
      state.errorSubmit = action.payload.message;
    },
  },
});

export default passwordResetSlice.reducer;

server/routes/passwordResetRoutes.js

const express = require("express");
const crypto = require("crypto");
const asyncHandler = require("express-async-handler");
const User = require("../models/userModel");

const router = express.Router();

router.get(
  "/reset/:token",
  asyncHandler(async (req, res, next) => {
    const user = await User.findOne({
      passwordResetToken: req.params.token,
      passwordResetExpires: { $gt: Date.now() },
    });

    if (user) {
      res.json(user);
    } else {
      const err = new Error("Password reset token is invalid or has expired");
      err.status = 404;
      next(err);
    }
  })
);
router.post(
  "/reset/:token",
  asyncHandler(async (req, res, next) => {
    if (req.body.password === req.body.confirmPassword) {
      next();
    } else {
      const err = new Error("Passwords don't match.");
      err.status = 404;
      next(err);
    }
    const user = await User.findOne({
      passwordResetToken: req.params.token,
      passwordResetExpires: { $gt: Date.now() },
    });

    if (user) {
      user.password = req.body.password;
      user.passwordResetToken = undefined;
      user.passwordResetExpires = undefined;
      const updatedUser = await user.save();
      res.json(updatedUser);
    } else {
      const err = new Error("Password reset token is invalid or has expired");
      err.status = 404;
      next(err);
    }
  })
);
router.post(
  "/forgot",
  asyncHandler(async (req, res, next) => {
    const user = await User.findOne({ email: req.body.email });

    if (user) {
      user.passwordResetToken = crypto.randomBytes(20).toString("hex");
      user.passwordResetExpires = Date.now() + 3600000;
      await user.save();

      res.json({
        message: "You have been emailed a password reset link",
      });
    } else {
      const err = new Error("No account with that email exists");
      err.status = 404;
      next(err);
    }
  })
);

module.exports = router;

store.js

import { configureStore } from "@reduxjs/toolkit";
import loginReducer from "./components/stateSlices/loginSlice";
import registerReducer from "./components/stateSlices/registerSlice";
import passwordResetEmailReducer from "./components/stateSlices/passwordResetEmailSlice";
import passwordResetPasswordReducer from "./components/stateSlices/passwordResetPasswordSlice";

const loggedInUserFromStorage = localStorage.getItem("loggedInUser")
  ? JSON.parse(localStorage.getItem("loggedInUser"))
  : null;

const preloadedState = {
  login: {
    user: loggedInUserFromStorage,
  },
};

export default configureStore({
  reducer: {
    login: loginReducer,
    register: registerReducer,
    passwordResetEmail: passwordResetEmailReducer,
    passwordResetPassword: passwordResetPasswordReducer,
  },
  preloadedState,
});

GITHUB REPO: https://github.com/sundaray/password-reset

HKS
  • 526
  • 8
  • 32
  • Have you [configured](https://redux-toolkit.js.org/api/configureStore) your store properly? If yes, and you want it to appear in "browser dev tools" extension, make sure that `devTools` (boolean) option (mentioned in docs) is not set to `false`. – Ajeet Shah Mar 23 '21 at 03:56
  • When I visit any other route, the store works fine. When I visit "/account/reset/:token", I don't see any store. I don't know what is the issue. If you want, I will send you the github repo address. – HKS Mar 23 '21 at 04:17

1 Answers1

0

Upon inspecting your GitHub code, I see you're proxying requests to /account/reset/:token which is the same as the frontend route to localhost:5000. A simple fix would be to rename the frontend route to something else. e.g. /password/reset/:token.

Arun Kumar Mohan
  • 11,517
  • 3
  • 23
  • 44
  • I am visiting http://localhost:3000/account/reset/:token. The JSON response is coming from the server, because on component mount I am sending a GET request to backend API. This is all fine. My question is I should see the form on the frontend irrespective of the type of response I get from the backend. – HKS Mar 23 '21 at 05:24
  • @hemanta Which ports are the frontend app and the API running on? – Arun Kumar Mohan Mar 23 '21 at 05:24
  • frontend: 3000, backend: 5000 – HKS Mar 23 '21 at 05:25
  • @hemanta Weird. What do you see when you visit `localhost:3000`? – Arun Kumar Mohan Mar 23 '21 at 05:28
  • I am seeing the home page. – HKS Mar 23 '21 at 05:32
  • Yes it's served by the API because localhost://3000 gets proxyed to localhost://5000. – HKS Mar 23 '21 at 05:44
  • Should I send my github repo address? – HKS Mar 23 '21 at 05:44
  • Added the repo to the question – HKS Mar 23 '21 at 06:35
  • @hemanta I see you've committed API keys to the repo and it's visible in the commit history. I would recommend removing them and regenerating a new set of keys. – Arun Kumar Mohan Mar 23 '21 at 07:52
  • @hemanta Where are you using the `setupProxy.js` module? – Arun Kumar Mohan Mar 23 '21 at 07:53
  • I am using it for proxying API requests. – HKS Mar 23 '21 at 08:14
  • @hemanta Where's it being used? I don't see any references to `setupProxy` in the code. Not sure if I'm missing something. – Arun Kumar Mohan Mar 23 '21 at 08:16
  • I don't need to add this file anywhere. It is automatically registered when I start the development server. You can check the details here: https://create-react-app.dev/docs/proxying-api-requests-in-development/ – HKS Mar 23 '21 at 08:18
  • Instead of enabling CORS on my server, I use setupProxy.js in all my MERN projects for proxying API requests. – HKS Mar 23 '21 at 08:21
  • 1
    @hemanta Ah, I see. The issue is you're proxying requests to `/account/reset/:token` which is the same as the frontend route to http://localhost:5000. A simple fix would be to rename the frontend route to something else. e.g. `/password/reset/:token`. – Arun Kumar Mohan Mar 23 '21 at 08:26
  • Can you pls. take a look at the following question: https://stackoverflow.com/q/67322234/9409877 – HKS Apr 29 '21 at 17:53