クラウドのクラウドストレージで Mern スタックを使用してパスワードを忘れた場合はどうすればよいですか?

王林
リリース: 2024-07-17 18:20:36
オリジナル
1044 人が閲覧しました

How to forgot password using Mern stack with cloudinary cloud storage?

はじめに
このブログ投稿では、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。ユーザーモデルの作成
モデル ディレクトリを作成し、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.認証ルートの作成
ルート ディレクトリを作成し、認証ルート用の 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: multipart/form-data を処理するためのミドルウェア。
  • multer-storage-cloudinary: multer と 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。トークン スキーマを定義します
モデル ディレクトリを作成し、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。ユーザーコントローラーの実装
コントローラー ディレクトリを作成し、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,
};

ログイン後にコピー
  1. 登録(req, res) 新規ユーザーの登録を処理します。その機能の内訳は次のとおりです:
  • 入力: リクエスト本文 (req.body) からのデータには、名、姓、電子メール、パスワード、確認パスワード、モバイル番号、生年月日、およびオプションで画像 (プロフィール写真用) などのユーザー情報が含まれることが期待されます。
  • 手順:
  • セキュリティのために bcrypt を使用してパスワードをハッシュします。

  • 電子メールがデータベースに既に存在するかどうかを確認します (User.findOne({ email }))。

  • 電子メールが存在する場合、409 ステータス (競合) で応答します。

  • パスワード (password とconfirmPassword) が一致しない場合、400 ステータス (不正なリクエスト) で応答します。

  • すべてが有効な場合、ハッシュ化されたパスワードを使用して User.create() を使用して MongoDB に新しいユーザー ドキュメントを作成し、プロフィール写真を Cloudinary にアップロードします。

  • 成功メッセージまたはユーザー データと、generateToken(newUser._id) を使用して生成された JWT トークンで応答します。

  • ログイン(req, 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.

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

以上がクラウドのクラウドストレージで Mern スタックを使用してパスワードを忘れた場合はどうすればよいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート