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.
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