问题描述
我正在 MERN 应用中实现密码重置功能。当用户输入他们想要重置密码的电子邮件地址时,他们会在他们的邮件中收到一个重置密码链接。现在,当他们访问该链接时,他们应该会看到在屏幕上呈现的 PasswordResetFormSecond 组件。 (无论令牌是否有效)。
但是,当我访问路径“/account/reset/:token”时,我没有看到 PasswordResetFormSecond 在屏幕上呈现。但是,我得到了正确的服务器响应。此外,未找到 redux 存储。 我做错了什么?
代码片段如下:
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,token },{ rejectWithValue }) => {
try {
const { data } = await axios.post(`/account/reset/${token}`,{
password,});
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",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,});
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",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 仓库:https://github.com/sundaray/password-reset
解决方法
检查您的 GitHub 代码后,我发现您将请求代理到 /account/reset/:token
,这与 localhost:5000
的前端路由相同。一个简单的解决方法是将前端路由重命名为其他名称。例如/password/reset/:token
。