Cloudinary 클라우드 스토리지와 함께 Mern 스택을 사용하여 비밀번호를 잊어버리는 방법은 무엇입니까?
소개
이 블로그 게시물에서는 Next.js와 함께 MERN 스택(MongoDB, Express.js, React, Node.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용 웹 프레임워크
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. 익스프레스 서버 설정
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. 사용자 모델 생성
모델 디렉토리를 생성하고 사용자 모델에 대한 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. 인증 경로 생성
경로 디렉터리를 생성하고 인증 경로를 위한 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
- cloudinary: 클라우드 기반 이미지 및 영상 관리.
- multer: 멀티파트/양식 데이터를 처리하기 위한 미들웨어.
- multer-storage-cloudinary: multer와 cloudinary의 통합
1.8. Cloudinary 구성
구성 디렉터리를 생성하고 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 };
Cloudinary 구성을 포함하도록 .env 파일을 업데이트하세요.
CLOUDINARY_CLOUD_NAME=your_cloud_name CLOUDINARY_API_KEY=your_api_key CLOUDINARY_API_SECRET=your_api_secret
1.9. 토큰 스키마 정의
모델 디렉토리를 생성하고 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. 인증 서비스 구현
서비스 디렉토리를 생성하고 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, 비밀번호): 토큰을 확인하고, 새 비밀번호를 해시하고, 데이터베이스에서 사용자 비밀번호를 업데이트하고, 확인 이메일을 보냅니다.
1.11. 사용자 컨트롤러 구현
컨트롤러 디렉토리를 생성하고 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, };
- 등록(req, res) 신규 사용자 등록을 처리합니다. 기능은 다음과 같습니다.
- 입력: 요청 본문(req.body)에서 firstName, lastName, email, 비밀번호, verifyPassword, mobileNo, dob 및 선택적으로 이미지(프로필 사진용)와 같은 사용자 정보가 포함된 데이터를 예상합니다.
- 단계:
보안을 위해 bcrypt를 사용하여 비밀번호를 해시합니다.
이메일이 데이터베이스(User.findOne({ email }))에 이미 존재하는지 확인합니다.
이메일이 존재하는 경우 409 상태(충돌)로 응답합니다.
비밀번호(password 및 verifyPassword)가 일치하지 않으면 400 상태(잘못된 요청)로 응답합니다.
모든 것이 유효하면 User.create()를 사용하여 해시된 비밀번호로 MongoDB에 새 사용자 문서를 생성하고 프로필 사진을 Cloudinary에 업로드합니다.
generateToken(newUser._id)을 사용하여 생성된 JWT 토큰과 함께 성공 메시지 또는 사용자 데이터로 응답합니다.
로그인(요청, 해상도)
사용자 로그인 인증을 처리합니다. 작동 방식은 다음과 같습니다.
입력: 요청 본문(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.
- resetPasswordController(req, res) Handles resetting a user's password:
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;
- The sendEmail function integrates Nodemailer to handle email sending operations in your application. It's configured with SMTP settings using environment variables for security, sends HTML-formatted emails, and includes error handling to manage potential transmission issues effectively. This abstraction simplifies email-related tasks across your application, ensuring reliable communication with users.
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.
위 내용은 Cloudinary 클라우드 스토리지와 함께 Mern 스택을 사용하여 비밀번호를 잊어버리는 방법은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

Python은 부드러운 학습 곡선과 간결한 구문으로 초보자에게 더 적합합니다. JavaScript는 가파른 학습 곡선과 유연한 구문으로 프론트 엔드 개발에 적합합니다. 1. Python Syntax는 직관적이며 데이터 과학 및 백엔드 개발에 적합합니다. 2. JavaScript는 유연하며 프론트 엔드 및 서버 측 프로그래밍에서 널리 사용됩니다.

웹 개발에서 JavaScript의 주요 용도에는 클라이언트 상호 작용, 양식 검증 및 비동기 통신이 포함됩니다. 1) DOM 운영을 통한 동적 컨텐츠 업데이트 및 사용자 상호 작용; 2) 사용자가 사용자 경험을 향상시키기 위해 데이터를 제출하기 전에 클라이언트 확인이 수행됩니다. 3) 서버와의 진실한 통신은 Ajax 기술을 통해 달성됩니다.

실제 세계에서 JavaScript의 응용 프로그램에는 프론트 엔드 및 백엔드 개발이 포함됩니다. 1) DOM 운영 및 이벤트 처리와 관련된 TODO 목록 응용 프로그램을 구축하여 프론트 엔드 애플리케이션을 표시합니다. 2) Node.js를 통해 RESTFULAPI를 구축하고 Express를 통해 백엔드 응용 프로그램을 시연하십시오.

보다 효율적인 코드를 작성하고 성능 병목 현상 및 최적화 전략을 이해하는 데 도움이되기 때문에 JavaScript 엔진이 내부적으로 작동하는 방식을 이해하는 것은 개발자에게 중요합니다. 1) 엔진의 워크 플로에는 구문 분석, 컴파일 및 실행; 2) 실행 프로세스 중에 엔진은 인라인 캐시 및 숨겨진 클래스와 같은 동적 최적화를 수행합니다. 3) 모범 사례에는 글로벌 변수를 피하고 루프 최적화, Const 및 Lets 사용 및 과도한 폐쇄 사용을 피하는 것이 포함됩니다.

Python과 JavaScript는 커뮤니티, 라이브러리 및 리소스 측면에서 고유 한 장점과 단점이 있습니다. 1) Python 커뮤니티는 친절하고 초보자에게 적합하지만 프론트 엔드 개발 리소스는 JavaScript만큼 풍부하지 않습니다. 2) Python은 데이터 과학 및 기계 학습 라이브러리에서 강력하며 JavaScript는 프론트 엔드 개발 라이브러리 및 프레임 워크에서 더 좋습니다. 3) 둘 다 풍부한 학습 리소스를 가지고 있지만 Python은 공식 문서로 시작하는 데 적합하지만 JavaScript는 MDNWebDocs에서 더 좋습니다. 선택은 프로젝트 요구와 개인적인 이익을 기반으로해야합니다.

개발 환경에서 Python과 JavaScript의 선택이 모두 중요합니다. 1) Python의 개발 환경에는 Pycharm, Jupyternotebook 및 Anaconda가 포함되어 있으며 데이터 과학 및 빠른 프로토 타이핑에 적합합니다. 2) JavaScript의 개발 환경에는 Node.js, VScode 및 Webpack이 포함되어 있으며 프론트 엔드 및 백엔드 개발에 적합합니다. 프로젝트 요구에 따라 올바른 도구를 선택하면 개발 효율성과 프로젝트 성공률이 향상 될 수 있습니다.

C와 C는 주로 통역사와 JIT 컴파일러를 구현하는 데 사용되는 JavaScript 엔진에서 중요한 역할을합니다. 1) C는 JavaScript 소스 코드를 구문 분석하고 추상 구문 트리를 생성하는 데 사용됩니다. 2) C는 바이트 코드 생성 및 실행을 담당합니다. 3) C는 JIT 컴파일러를 구현하고 런타임에 핫스팟 코드를 최적화하고 컴파일하며 JavaScript의 실행 효율을 크게 향상시킵니다.

Python은 데이터 과학 및 자동화에 더 적합한 반면 JavaScript는 프론트 엔드 및 풀 스택 개발에 더 적합합니다. 1. Python은 데이터 처리 및 모델링을 위해 Numpy 및 Pandas와 같은 라이브러리를 사용하여 데이터 과학 및 기계 학습에서 잘 수행됩니다. 2. 파이썬은 간결하고 자동화 및 스크립팅이 효율적입니다. 3. JavaScript는 프론트 엔드 개발에 없어서는 안될 것이며 동적 웹 페이지 및 단일 페이지 응용 프로그램을 구축하는 데 사용됩니다. 4. JavaScript는 Node.js를 통해 백엔드 개발에 역할을하며 전체 스택 개발을 지원합니다.
