簡介
在這篇文章中,我們將逐步介紹使用 MERN 堆疊(MongoDB、Express.js、React、Node.js)和 Next.js 設定「忘記密碼」功能的過程。我們將涵蓋從使用 Node.js 和 Express 設定後端到使用 Next.js 建立前端的所有內容。最後,您將全面了解如何實現安全且使用者友好的密碼重設功能。
先決條件
JavaScript 和 React 的基礎知識。
了解 Node.js 和 Express.js。
熟悉 MongoDB 和 Mongoose。
Next.js 經驗。
第 1 步:設定後端
1.1.初始化 Node.js 專案
為您的專案建立一個新目錄並初始化 Node.js 專案:
mkdir mern-password-reset cd mern-password-reset npm init -y
1.2。安裝依賴項
安裝必要的依賴項:
npm install express mongoose bcryptjs jsonwebtoken nodemailer dotenv
express:Node.js 的 Web 框架。
mongoose:MongoDB 物件建模工具。
bcryptjs:哈希密碼的函式庫。
jsonwebtoken:用於產生和驗證 JWT 令牌。
nodemailer:用於傳送電子郵件。
dotenv:管理環境變數。
1.3。設定環境變數
在專案的根目錄中建立一個 .env 檔案來儲存環境變數:
PORT=5000 MONGO_URI=your_mongodb_connection_string JWT_SECRET=your_jwt_secret EMAIL_USER=your_email@example.com EMAIL_PASS=your_email_password
1.4。設定 Express 伺服器
建立一個 server.js 檔案並設定基本的 Express 伺服器:
const express = require('express'); const mongoose = require('mongoose'); const dotenv = require('dotenv'); const app = express(); dotenv.config(); const port = process.env.PORT || 5001; //middlewares app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cors()); mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, }); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); });
1.5。建立使用者模型
建立一個 models 目錄並為 User 模型新增 User.js 檔案:
const mongoose = require("mongoose"); const userSchema = new mongoose.Schema( { firstName: { type: String, require: true }, lastName: { type: String, require: true }, email: { type: String, require: true, unique: true }, password: { type: String, require: true }, mobileNo: { type: String, require: true }, image: { type: String, default: "" }, dob: { type: String, require: true }, }, { timestamps: true } ); const User = mongoose.model("user", userSchema); module.exports = User;
1.6。建立身份驗證路由
建立一個routes目錄並新增一個auth.js檔案用於驗證路由:
const express = require("express"); const { register, login, requestPasswordResetController, resetPasswordController, } = require("../controllers/user"); const router = express.Router(); router.post("/login", login); router.post("/requestPasswordReset", requestPasswordResetController); router.post("/resetPassword", resetPasswordController); module.exports = router;
1.7。新增 Cloudinary 上傳個人資料圖片
安裝 Cloudinary 必要的依賴項:
npm install cloudinary multer multer-storage-cloudinary
1.8。配置 Cloudinary
建立config目錄並加入cloudinaryConfig.js檔:
const cloudinary = require('cloudinary').v2; const { CloudinaryStorage } = require('multer-storage-cloudinary'); cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, }); const storage = new CloudinaryStorage({ cloudinary, params: { folder: 'profile_pics', allowedFormats: ['jpg', 'png'], }, }); module.exports = { cloudinary, storage };
更新您的 .env 檔案以包含 Cloudinary 配置:
CLOUDINARY_CLOUD_NAME=your_cloud_name CLOUDINARY_API_KEY=your_api_key CLOUDINARY_API_SECRET=your_api_secret
1.9。定義令牌架構
建立 models 目錄並新增 tokenSchema.js 檔案:
const mongoose = require("mongoose"); const tokenSchema = new mongoose.Schema({ userId: { type: mongoose.Schema.Types.ObjectId, require: true, ref: "User" }, token: { type: String, require: true }, createdAt: { type: Date, default: Date.now, expires: 3600 }, }); const Token = mongoose.model("Token", tokenSchema); module.exports = Token;
1.10。實施身分驗證服務
建立 services 目錄並新增 authService.js 檔案:
const Token = require("../models/tokenSchema"); const User = require("../models/userSchema"); const crypto = require("crypto"); const bcrypt = require("bcryptjs"); const sendEmail = require("../config/sendEmail"); const requestPasswordReset = async (email) => { try { const user = await User.findOne({ email }); if (!user) { throw new Error("User doesn't exist"); } const name = `${user.firstName} ${user.lastName}`; let token = await Token.findOne({ userId: user._id }); if (token) await token.deleteOne(); let resetToken = crypto.randomBytes(32).toString("hex"); const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(resetToken, salt); const newToken = await Token.create({ userId: user._id, token: hash, createdAt: Date.now(), }); const link = `${process.env.CLIENT_URL}/forgot-password/reset?token=${resetToken}&id=${user._id}`; const htmlContent = ` <h1>Hi, ${name}</h1> <p>You requested to reset your password.</p> <p>Please, click the link below to reset your password.</p> <a href="${link}">Reset Password</a>`; sendEmail(`${email}`, "Request to reset password", htmlContent); return link; } catch (error) { console.log(error.message); } }; const resetPassword = async (userId, token, password) => { try { let passwordResetToken = await Token.findOne({ userId }); if (!passwordResetToken) { throw new Error("Invalid or expired password reset token"); } const isValid = await bcrypt.compare(token, passwordResetToken.token); if (!isValid) { throw new Error("Invalid or expired password reset token"); } const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(password, salt); await User.updateOne( { _id: userId }, { $set: { password: hash } }, { new: true } ); const user = await User.findById({ _id: userId }); const name = `${user.firstName} ${user.lastName}`; const htmlContent = `<h1>Hi, ${name}</h1> <p>Your password reset successfully.</p>`; sendEmail(user.email, "Password Reset Successfully", htmlContent); await passwordResetToken.deleteOne(); return "Password Reset Successfully"; } catch (error) { console.log(error.message); } }; module.exports = { requestPasswordReset, resetPassword };
requestPasswordReset(email):產生密碼重設令牌,將其儲存在資料庫中,並向使用者發送包含重設連結的電子郵件。
resetPassword(userId, token, password):驗證令牌,散列新密碼,更新資料庫中的使用者密碼,並發送確認電子郵件。
1.11。實現使用者控制器
建立controllers目錄並加入userController.js檔:
const User = require("../models/userSchema"); const bcrypt = require("bcryptjs"); const generateToken = require("../config/generateToken"); const { requestPasswordReset, resetPassword } = require("../services/authService"); const Token = require("../models/tokenSchema"); const cloudinary = require("cloudinary").v2; const register = async (req, res) => { try { const { firstName, lastName, email, password, confirmPassword, mobileNo, dob, } = req.body; const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); const image = req.file.path; const imagePublicId = req.file.filename; const emailExist = await User.findOne({ email }); if (emailExist) { await cloudinary.uploader.destroy(imagePublicId); res.status(409).send({ message: "User with this email already exists" }); } if (password != confirmPassword) { await cloudinary.uploader.destroy(imagePublicId); res .status(400) .send({ message: "Password and Confirm password did not match" }); } if (!emailExist && password === confirmPassword) { const newUser = await User.create({ firstName: firstName, lastName: lastName, email: email, password: hashedPassword, mobileNo: mobileNo, dob: dob, image: image, }); res.send({ firstName: firstName, lastName: lastName, email: email, password: hashedPassword, mobileNo: mobileNo, dob: dob, token: generateToken(newUser._id), }); } } catch (error) { if (req.file && req.file.filename) { await cloudinary.uploader.destroy(req.file.filename); } console.log(error); } }; const login = async (req, res) => { try { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user) { res.status(404).send({ message: "Email id is wrong" }); } if (user) { const validPassword = await bcrypt.compare(password, user.password); if (!validPassword) { res.status(401).send({ message: "Password is wrong" }); } else { res.status(200).send({ user: user, token: generateToken(user._id), }); } } } catch (error) { console.log(error); } }; const requestPasswordResetController = async (req, res) => { try { const email = req.body.email; const user = await User.findOne({ email }); if (!user) { res.status(404).send({ message: "User not found" }); } const requestPasswordResetService = await requestPasswordReset(email); return res.status(200).send(requestPasswordResetService); } catch (error) { res.status(500).send({ message: error.message }); console.log(error.message); } }; const resetPasswordController = async (req, res) => { try { const userId = req.body.userId; const token = req.body.token; const password = req.body.password; let passwordResetToken = await Token.findOne({ userId }); const resetPasswordService = await resetPassword(userId, token, password); if (!passwordResetToken) { res.status(404).send({ message: "Token not found or expired" }); } return res.status(200).send(resetPasswordService); } catch (error) { console.log(error.message); return res.status(500).send({ message: error.message }); } }; module.exports = { register, login, requestPasswordResetController, resetPasswordController, };
使用 bcrypt 對密碼進行雜湊處理以確保安全。
檢查資料庫中是否已存在電子郵件 (User.findOne({ email }))。
如果電子郵件存在,它會回應 409 狀態(衝突)。
如果密碼(密碼和確認密碼)不匹配,則會回應 400 狀態(錯誤要求)。
如果一切有效,它會使用 User.create() 和雜湊密碼在 MongoDB 中建立一個新的使用者文檔,並將個人資料圖片上傳到 Cloudinary。
回應成功訊息或使用者資料以及使用generateToken(newUser._id)產生的JWT令牌。
登入(請求,res)
處理使用者登入驗證。其運作原理如下:
輸入:需要來自請求正文 (req.body) 的電子郵件和密碼。
步驟:
透過電子郵件在資料庫中尋找使用者 (User.findOne({ email }))。
如果未找到用戶,則回應 404 狀態(未找到)。
使用 bcrypt 將提供的密碼與儲存的雜湊密碼進行比較。
如果密碼匹配,則產生 JWT 令牌 (generateToken(user._id)) 並將其與回應中的使用者資料一起傳送。
如果密碼不匹配,則回應 401 狀態(未經授權)。
requestPasswordResetController(req, res)
Initiates the password reset process for a user:
Input: Expects email from the request body (req.body).
Steps:
Finds a user in the database by email (User.findOne({ email })).
If no user is found, responds with a 404 status (not found).
Calls requestPasswordReset(email) from authService to initiate the password reset process.
Sends a response with the result of the password reset request.
Input: Expects userId, token, and password from the request body (req.body).
Steps:
Finds a password reset token for the user (Token.findOne({ userId })).
If no token is found, responds with a 404 status (not found).
Verifies the provided token against the stored hashed token using bcrypt.
If the token is valid, hashes the new password using bcrypt and updates the user's password in the database (User.updateOne()).
Sends a password reset confirmation email using sendEmail() and deletes the used password reset token (passwordResetToken.deleteOne()).
Responds with a success message.
1.12. Send Email Utility
Create a config directory and add a sendEmail.js file:
const nodemailer = require("nodemailer"); const sendEmail = async (email, subject, text) => { try { const transporter = nodemailer.createTransport({ host: process.env.HOST, service: process.env.SERVICE, port: 465, secure: true, auth: { user: process.env.USER, pass: process.env.PASS, }, }); await transporter.sendMail({ from: "Your App Name", to: email, subject: subject, html: text, }); console.log("Email sent successfully"); } catch (error) { console.log(error, "Email not sent"); } }; module.exports = sendEmail;
1.13. Integrate Routes into the Server and cloudinary cloud image update
Update server.js to include the authentication routes:
const express = require("express"); const dotenv = require("dotenv"); const cors = require("cors"); const connectDB = require("./db/db"); const authRoutes = require("./routes/user"); const bodyParser = require("body-parser"); const multer = require("multer"); const { storage } = require("./config/storage"); const { register } = require("./controllers/user"); const employeeRoutes = require("./routes/employee"); const transactionRoutes = require("./routes/transaction"); const app = express(); dotenv.config(); const port = process.env.PORT || 5001; //middlewares app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use(cors()); const upload = multer({ storage }); //routes app.use("/api/auth", authRoutes); //routes with files app.post("/api/auth/register", upload.single("file"), register); mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, }); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); });
Step 2: Setting Up the Frontend
2.1. Initialize the Next.js Project
Create a new Next.js project:
npx create-next-app@latest mern-password-reset-client cd mern-password-reset-client
2.2. Install Dependencies
Install the necessary dependencies:
npm install redux react-redux @reduxjs/toolkit react-toastify @heroicons/react @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome axios next-themes
2.3. Create Pages and Components
Create pages and components for registration, login, forgot password, and reset password.
app/register/page.js
import Register from "@/components/Auth/Register"; import React from "react"; const page = () => { return ( <div> <Register /> </div> ); }; export default page;
component/register.jsx
"use client"; import React, { useEffect, useState } from "react"; import styles from "./auth.module.css"; import Logo from "../misc/Logo/Logo"; import Link from "next/link"; import axios from "axios"; import { redirect } from "next/navigation"; import { Bounce, toast } from "react-toastify"; import { useTheme } from "next-themes"; import { useDispatch, useSelector } from "react-redux"; import { setRegister } from "@/redux/slice/authSlice"; const Register = () => { const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmpassword] = useState(""); const [mobileNo, setMobileNo] = useState(""); const [dob, setDob] = useState(""); const [file, setFile] = useState(""); const [empData, setEmpData] = useState(); const { theme } = useTheme(); const token = useSelector((state) => state.auth.token); const dispatch = useDispatch(); const mergeEmployeeDetails = async () => { try { const config = { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }; const result = await axios.get( "http://localhost:5000/api/employee/employeeDetails", config ); setEmpData(result); } catch (error) { console.log(error); } }; const handleRegister = async () => { const formData = new FormData(); formData.append("firstName", firstName); formData.append("lastName", lastName); formData.append("email", email); formData.append("password", password); formData.append("confirmPassword", confirmPassword); formData.append("mobileNo", mobileNo); formData.append("dob", dob); formData.append("file", file); try { const config = { headers: { "Content-Type": "multipart/form-data", }, }; const result = await axios.post( "http://localhost:5000/api/auth/register", formData, config ); const regUser = result.data; console.log(regUser); if (regUser) { dispatch( setRegister({ token: regUser.token, }) ); } toast.success("Successfully Registered", { position: "top-right", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: theme === "light" ? "light" : theme === "dark" ? "dark" : "dark", transition: Bounce, }); mergeEmployeeDetails(); redirect("/login"); } catch (error) { toast.error(error.response.data.message, { position: "top-right", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: theme === "light" ? "light" : theme === "dark" ? "dark" : "dark", transition: Bounce, }); console.log(error.response.data); } }; useEffect(() => { if (token) { mergeEmployeeDetails(); } }, [token]); return ( <div className={styles.container}> <div className={styles.auth_container}> <div className={styles.auth}> <div className={styles.auth_headers}> <div className={styles.upper_part}> <div className={styles.upper_part_text}>Welcome to</div>{" "} <div> <Logo /> </div> </div> <div className={styles.lower_part}> Register yourself to N&N finance </div> </div> <div className={styles.auth_form}> <div className={styles.input_group}> <div className={styles.input}> <div className={styles.inputTwo}> <div> <div> <label htmlFor="">First Name</label> </div> <div> {" "} <input type="text" placeholder="Enter your first name" name="firstName" value={firstName} onChange={(e) => setFirstName(e.target.value)} /> </div> </div> <div> <div> <label htmlFor="">Last Name</label> </div> <div> {" "} <input type="text" placeholder="Enter your last name" name="lastName" value={lastName} onChange={(e) => setLastName(e.target.value)} /> </div> </div> </div> </div> <div className={styles.input}> <div> <label htmlFor="">Email</label> </div> <div> <input type="email" placeholder="Enter your email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} /> </div> </div> <div className={styles.input}> <div> <label htmlFor="">Mobile No</label> </div> <div> <input type="text" placeholder="Enter your mobile number" name="mobileNo" value={mobileNo} onChange={(e) => setMobileNo(e.target.value)} /> </div> </div> <div className={styles.input}> <div> <label htmlFor="">Password</label> </div> <div> <input type="password" placeholder="Enter password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> </div> <div className={styles.input}> <div> {" "} <label htmlFor="">Confirm Password</label> </div> <div> <input type="password" placeholder="Confirm Password" name="confirmPassword" value={confirmPassword} onChange={(e) => setConfirmpassword(e.target.value)} /> </div> </div> <div className={styles.input}> <div> <label htmlFor="">Date of Birth</label> <input type="date" placeholder="Enter your DOB" name="dob" value={dob} onChange={(e) => setDob(e.target.value)} /> </div> </div> <div className={styles.input}> <div> <input type="file" placeholder="Enter your image" name="file" onChange={(e) => setFile(e.target.files[0])} /> </div> </div> <div className={styles.btn}> <button onClick={handleRegister}>Register</button> </div> </div> <div className={styles.bottom_part}> Already have account? <Link href="/">Login</Link> </div> </div> </div> </div> </div> ); }; export default Register;
app/login/page.js
"use client"; import Login from "@/components/Auth/Login"; import { setLogin } from "@/redux/slice/authSlice"; import { handleLogin } from "@/services/api"; import axios from "axios"; import { useTheme } from "next-themes"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useDispatch } from "react-redux"; import { Bounce, toast } from "react-toastify"; export default function Home() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const dispatch = useDispatch(); const router = useRouter(); const theme = useTheme(); return ( <> <Login email={email} password={password} setEmail={setEmail} setPassword={setPassword} handleLogin={() => handleLogin(email, password, dispatch, router, theme) } /> </> ); }
component/login.jsx
"use client"; import React from "react"; import styles from "./auth.module.css"; import Link from "next/link"; import Logo from "../misc/Logo/Logo"; const Login = ({ email, password, setEmail, setPassword, handleLogin }) => { return ( <div className={styles.auth_container_login}> <div className={styles.auth}> <div className={styles.auth_headers}> <div className={styles.upper_part}> <div className={styles.upper_part_text}>Welcome to</div>{" "} <div> <Logo /> </div> </div> <div className={styles.lower_part}> Logged In yourself to N&N finance </div> </div> <div className={styles.auth_form}> <div className={styles.input_group}> <div className={styles.input}> <div> <label htmlFor="">Email</label> </div> <div> <input type="email" placeholder="Enter your email" value={email} onChange={(e) => setEmail(e.target.value)} /> </div> </div> <div className={styles.input}> <div> <label htmlFor="">Password</label> </div> <div> <input type="password" placeholder="Enter password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> </div> <div className={styles.btn}> <button onClick={handleLogin}>Login</button> </div> </div> <div className={styles.forgot_password}> <Link href="/forgot-password">Forgot Password?</Link> </div> <div className={styles.bottom_part}> Already have account? <Link href="/register">Sign Up</Link> </div> </div> </div> </div> ); }; export default Login;
service/api.js
import axios from "axios"; import { Bounce, toast } from "react-toastify"; import { setLogin } from "@/redux/slice/authSlice"; export const handleLogin = async (email, password, dispatch, router, theme) => { try { const config = { headers: { "Content-Type": "application/json", }, }; const result = await axios.post( "http://localhost:5000/api/auth/login", { email, password }, config ); router.push("/dashboard"); const loggedIn = result.data; if (loggedIn) { dispatch( setLogin({ user: loggedIn.user, token: loggedIn.token, }) ); } toast.success("Successfully Logged In", { position: "top-right", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: theme === "light" ? "light" : theme === "dark" ? "dark" : "dark", transition: Bounce, }); } catch (error) { toast.error(error.response.data.message, { position: "top-right", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: theme === "light" ? "light" : theme === "dark" ? "dark" : "system", transition: Bounce, }); console.log(error.response.data); } };
app/forgot-password.js
"use client"; import ForgotPasswordRequest from "@/components/Auth/ForgotPasswordRequest"; import EmailSuccess from "@/components/misc/authModal/EmailSuccess"; import axios from "axios"; import { useTheme } from "next-themes"; import React, { useEffect, useState } from "react"; import { Bounce, toast } from "react-toastify"; const ForgotPassword = () => { const [email, setEmail] = useState(""); const theme = useTheme(); const [isEmailSent, setIsEmailSent] = useState(false); const verifyEmail = async () => { try { const result = await axios.post( "http://localhost:5000/api/auth/requestPasswordReset", { email } ); toast.success( "Password reset code has been sent successfully to your email", { position: "top-right", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: theme === "light" ? "light" : theme === "dark" ? "dark" : "system", transition: Bounce, } ); setIsEmailSent(true); console.log(result); } catch (error) { toast.error(error.response?.data.message, { position: "top-right", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: theme === "light" ? "light" : theme === "dark" ? "dark" : "system", transition: Bounce, }); console.log("error: ", error); } }; useEffect(() => { localStorage.setItem("email", email); }, [email]); return ( <> {!isEmailSent ? ( <ForgotPasswordRequest email={email} setEmail={setEmail} verifyEmail={verifyEmail} /> ) : ( <EmailSuccess email={email} /> )} </> ); }; export default ForgotPassword;
components/forgotPassword/ForgotPassword.jsx
"use client"; import React from "react"; import styles from "./auth.module.css"; import Link from "next/link"; import Logo from "../misc/Logo/Logo"; import { toast } from "react-toastify"; const ForgotPasswordRequest = ({ email, setEmail, verifyEmail }) => { return ( <div className={styles.auth_container_login}> <div className={styles.auth}> <div className={styles.auth_headers}> <div className={styles.upper_part}> <div className={styles.upper_part_text}></div>{" "} <div> <Logo /> </div> </div> <div className={styles.lower_part}>Find Your Account</div> </div> <div className={styles.auth_form}> <div className={styles.input_group}> <div className={styles.input}> <div> <label htmlFor=""> Please enter your email address or mobile number to search for your account. </label> </div> <div> <input type="email" placeholder="Enter your email" value={email} onChange={(e) => setEmail(e.target.value)} /> </div> </div> <div className={styles.btn}> <button onClick={verifyEmail}>Confirm your email address</button> </div> </div> <div className={styles.bottom_part}> <Link href="/">Go Back</Link> </div> </div> </div> </div> ); }; export default ForgotPasswordRequest;
components/forgotPassword/EmailSuccess.jsx
import React from "react"; import styles from "./emailSuccess.module.css"; import Image from "next/image"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEnvelope } from "@fortawesome/free-solid-svg-icons"; import Link from "next/link"; import { ArrowLongLeftIcon } from "@heroicons/react/24/outline"; const EmailSuccess = ({ email }) => { const openEmailApp = () => { // Mailto link to open email client window.location.href = "mailto:"; }; return ( <div className={styles.container}> <div className={styles.card_email}> <div className={styles.card_top}> <FontAwesomeIcon icon={faEnvelope} shake size="3x" color="#03c03c" /> </div> <div className={styles.card_middle}> <div className={styles.text}>We sent a password reset link to</div> <div className={styles.text_email}>{email}</div> <div className={styles.btn}> <button onClick={openEmailApp}>Open email app</button> </div> </div> <div className={styles.card_bottom}> <div className={styles.text_resend}> Did not receive the email? <button>Click to resend</button> </div> <div className={styles.backTO}> <ArrowLongLeftIcon style={{ height: "1rem", width: "1rem", cursor: "pointer", marginRight: "10px", }} />{" "} <Link href="/">Back to login</Link> </div> </div> </div> </div> ); }; export default EmailSuccess;
app/forgot-password/reset/page.jsx
"use client"; import ResetPasswordLayout from "@/components/Auth/ResetPasswordLayout"; import PasswordResetSuccess from "@/components/misc/authModal/passwordResetSuccess/PasswordResetSuccess"; import React, { useEffect, useState } from "react"; import { Bounce, toast } from "react-toastify"; import axios from "axios"; import { useRouter, useSearchParams } from "next/navigation"; import { useTheme } from "next-themes"; import { useDispatch } from "react-redux"; import { handleLogin } from "@/services/api"; const ResetPassword = () => { const [isSuccess, setIsSuccess] = useState(false); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmpassword] = useState(""); const searchParams = useSearchParams(); const token = searchParams.get("token"); const userId = searchParams.get("id"); const [message, setMessage] = useState(""); const dispatch = useDispatch(); const router = useRouter(); const theme = useTheme(); useEffect(() => { if (password && confirmPassword && password === confirmPassword) { setMessage("Password Matched"); const hideTimeout = setTimeout(() => { setMessage(""); }, 3000); return () => clearTimeout(hideTimeout); } setEmail(localStorage.getItem("email")); }, [password, confirmPassword]); const resetPassword = async () => { try { const result = await axios.post( "http://localhost:5000/api/auth/resetPassword", { userId, token, password } ); toast.success("Password Successfully changed", { position: "top-right", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: theme === "light" ? "light" : theme === "dark" ? "dark" : "dark", transition: Bounce, }); console.log(result); setIsSuccess(true); } catch (error) { toast.error(error.response?.data.message, { position: "top-right", autoClose: 5000, hideProgressBar: false, closeOnClick: true, pauseOnHover: true, draggable: true, progress: undefined, theme: theme === "light" ? "light" : theme === "dark" ? "dark" : "system", transition: Bounce, }); } }; return ( <> {!isSuccess ? ( <ResetPasswordLayout password={password} confirmPassword={confirmPassword} setPassword={setPassword} setConfirmpassword={setConfirmpassword} resetPassword={resetPassword} message={message} /> ) : ( <PasswordResetSuccess handleLogin={() => handleLogin(email, password, dispatch, router, theme) } /> )} </> ); }; export default ResetPassword;
components/forgotPassword/ResetPasswordLayout.jsx
import React, { useEffect, useState } from "react"; import styles from "./auth.module.css"; import Link from "next/link"; import Logo from "../misc/Logo/Logo"; const ResetPasswordLayout = ({ password, confirmPassword, setPassword, setConfirmpassword, resetPassword, message, }) => { return ( <div className={styles.auth_container_login}> <div className={styles.auth}> <div className={styles.auth_headers}> <div className={styles.upper_part}> <div className={styles.upper_part_text}></div>{" "} <div> <Logo /> </div> </div> <div className={styles.lower_part}>Set New Password</div> <div className={styles.lower_part} style={{ fontSize: "12px", marginTop: "5px" }} > Your new password must be different from previously used password. </div> </div> <div className={styles.auth_form}> <div className={styles.input_group}> <div className={styles.input}> <div> <label htmlFor="">Password</label> </div> <div> <input type="password" placeholder="Enter your new password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> <div className={styles.input}> <div> <label htmlFor="">Confirm Password</label> </div> <div> <input type="password" placeholder="Confirm your new password" value={confirmPassword} onChange={(e) => setConfirmpassword(e.target.value)} /> </div> {password != confirmPassword && confirmPassword != "" ? ( <div className={styles.warning}> Password and Confirm password did not match </div> ) : confirmPassword != "" ? ( <div className={styles.success}>{message}</div> ) : null} </div> </div> <div className={styles.btn}> <button onClick={resetPassword}>Reset Password</button> </div> </div> <div className={styles.bottom_part}> <Link href="/">Back to login</Link> </div> </div> </div> </div> ); }; export default ResetPasswordLayout;
components/forgotPassword/PasswordResetSuccess.jsx
import React from "react"; import styles from "./passwordResetSuccess.module.css"; import { ArrowLongLeftIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCircleCheck } from "@fortawesome/free-solid-svg-icons"; const PasswordResetSuccess = ({ handleLogin }) => { return ( <div className={styles.container}> <div className={styles.card_email}> <div className={styles.card_top}> <FontAwesomeIcon icon={faCircleCheck} beat size="3x" color="#03c03c" />{" "} </div> <div className={styles.card_middle}> <div className={styles.bigText}>Password reset</div> <div className={styles.text}> Your password has been successfully reset. </div> <div className={styles.text_email}> Click below to login magically. </div> <div className={styles.btn}> <button onClick={handleLogin}>Continue</button> </div> </div> <div className={styles.card_bottom}> <div className={styles.backTO}> <ArrowLongLeftIcon style={{ height: "1rem", width: "1rem", cursor: "pointer", marginRight: "10px", }} />{" "} <Link href="/">Back to login</Link> </div> </div> </div> </div> ); }; export default PasswordResetSuccess;
**Conclusion
**In conclusion, implementing a forgot password feature in a MERN stack application using Next.js on the frontend and Node.js with Express on the backend involves creating secure routes for password reset requests and token management, integrating email notifications with Nodemailer, and ensuring robust data security with bcrypt for password hashing and token validation. This approach enhances user experience by providing a reliable mechanism for resetting passwords while maintaining data integrity and security throughout the process.
以上是如何使用 Mern stack 和 cloudinary 雲端儲存忘記密碼?的詳細內容。更多資訊請關注PHP中文網其他相關文章!