Open In App

Forgot & Reset Password Feature with React and Node JS

Last Updated : 15 Mar, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

Almost all the websites that you use nowadays require you to signup and then login. But, with the tremendous rise in number of websites being used by you, it is difficult for you to remember the passwords for so many websites if you use different passwords on different websites.

Ever had that “Oh no, I forgot my password” moment? Fear not! The “Forgot and Reset Password” feature on these website acts as a life saver for you. So, the next time you draw a blank on your password, just click that “Forgot Password” link and reset your password. In this article, we will look at how we can implement this most important funcationality in any website i.e. Forgot and Reset Password functionality using React and NodeJS.

Output Preview: Let us have a look at how the final output will look like

Login-Page

Output preview

Prerequisites:

Approach to Create Forgot and Reset Password with React and NodeJS:

  • List all the requirements for the project and create the folder structure of the project accordingly.
  • Choose and install the required dependencies necessary for the project.
  • Define your database schema and table structure.
  • Create different routes and pages for login, forgot password and reset password.
  • When user clicks on “Forgot Password” on the Login page, the user gets redirected to ForgotPassword page where the user is asked to enter his/her email id.
  • Once the user clicks the “Reset Password’ button, a request goes to backend, where the server generates a token and sends the reset password link to the user’s email id.
  • When the user clicks on the Reset Password Link received in the email, he is asked to enter a new password. If user clicks submit, token validation will be done at backend and the new hashed password will be stored in database. The server then sends a success status indicating password reset was successful.

Steps to Setup a React App:

Step 1: Create a new React App project by opening a terminal and using the following command:

npx create-react-app frontend

Step 2: Navigate to your project folder created above, i.e. ‘frontend’, using the following command:

cd frontend

Step 3: Install the necessary packages for this project.

npm install –save react-router-dom @mui/material @emotion/react @emotion/styled @mui/icons-material axios react-toastify

Project Structure(Frontend):

frontend-folder-structure

React Folder Structure

The updated dependencies in package.json file will look like:

"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.12",
"@mui/material": "^5.15.12",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"react-toastify": "^10.0.4",
"web-vitals": "^2.1.4"
}

Step 4: Create a .env file to hide your secret key(s) or variable(s).

REACT_APP_BACKEND_URL=http://localhost:4000

Step 5: Create the required files as seen in the folder structure and add the following codes.

Javascript
//App.js

import "./App.css";
import {
    BrowserRouter,
    Navigate,
    Route,
    Routes
} from "react-router-dom";
import Login from "./pages/Login";
import ForgotPassword from "./pages/ForgotPassword";
import ResetPassword from "./pages/ResetPassword";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.min.css";

const App = () => {
    return (
        <div className="App">
            <ToastContainer />
            <BrowserRouter>
                <Routes>
                    <Route path="/" 
                           element={<Navigate to="/login" />} />
                    <Route path="/login" element={<Login />} />
                    <Route path="forgotPassword" 
                           element={<ForgotPassword />} />
                    <Route path="resetPassword" 
                           element={<ResetPassword />} />
                </Routes>
            </BrowserRouter>
        </div>
    );
};

export default App;
Javascript
//Login.js

import { React } from "react";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Link from "@mui/material/Link";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import {
    Card,
    CardContent
} from "@mui/material";

const Login = () => {
    const handleSubmit = (e) => {
        e.preventDefault();
        const data = new FormData(e.currentTarget);
        console.log({
            email: data.get("email"),
            password: data.get("password"),
        });
    };

    return (
        <Container maxWidth="sm">
            <Box
                sx={{
                    marginTop: 10,
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                }}
            >
                <Card sx={{ boxShadow: "4" }}>
                    <CardContent sx={{ m: 3 }}>
                        <Avatar sx={{
                            m: "auto",
                            bgcolor: "primary.main"
                        }}>
                            <LockOutlinedIcon />
                        </Avatar>
                        <Typography component="h1" variant="h5">
                            Login
                        </Typography>

                        <Box
                            component="form"
                            onSubmit={handleSubmit}
                            noValidate
                            sx={{ mt: 1 }}
                        >
                            <TextField
                                margin="normal"
                                required
                                fullWidth
                                id="email"
                                label="Email Address"
                                name="email"
                                autoComplete="email"
                                autoFocus
                            />
                            <TextField
                                margin="normal"
                                required
                                fullWidth
                                name="password"
                                label="Password"
                                type="password"
                                id="password"
                                autoComplete="current-password"
                            />
                            <Button
                                type="submit"
                                fullWidth
                                variant="contained"
                                sx={{ mt: 3, mb: 2 }}
                            >
                                Sign In
                            </Button>
                            <Grid container>
                                <Grid item sm>
                                    <Link
                                        href="/forgotPassword"
                                        variant="body2"
                                        style={{textDecoration:"None"}}
                                    >
                                        Forgot password?
                                    </Link>
                                </Grid>
                            </Grid>
                        </Box>
                    </CardContent>
                </Card>
            </Box>
        </Container>
    );
};

export default Login;
Javascript
//ForgotPassword.js

import { React } from "react";
import axios from "axios";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import {
    Card,
    CardContent
} from "@mui/material";
import { toast } from "react-toastify";

const ForgotPassword = () => {
    const handleSubmit = async (e) => {
        e.preventDefault();
        const data = new FormData(e.currentTarget);
        const email = data.get("email");
        const url = process
                        .env
                        .REACT_APP_BACKEND_URL + "/api/forgotPassword";
        const res = await axios.post(url, { email: email });
        if (res.data.success === false) {
            toast.error(res.data.message, {
                autoClose: 5000,
                position: "top-right",
            });
        } else {
            toast.success(res.data.message, {
                autoClose: 5000,
                position: "top-right",
            });
        }
    };
    return (
        <Container maxWidth="sm">
            <Box
                sx={{
                    marginTop: 10,
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                }}
            >
                <Card sx={{ boxShadow: "4" }}>
                    <CardContent sx={{ m: 3 }}>
                        <Avatar sx={{
                            m: "auto",
                            bgcolor: "primary.main"
                        }}>
                            <LockOutlinedIcon />
                        </Avatar>
                        <Typography component="h1"
                            variant="h5" sx={{ mt: 1 }}>
                            Forgot Password
                        </Typography>

                        <Box component="form"
                            onSubmit={handleSubmit} sx={{ mt: 1 }}>
                            <TextField
                                margin="normal"
                                required
                                fullWidth
                                id="email"
                                label="Email Address"
                                name="email"
                                autoComplete="email"
                                autoFocus
                            />
                            <Button
                                type="submit"
                                fullWidth
                                variant="contained"
                                sx={{ mt: 3, mb: 2 }}
                            >
                                Reset Password
                            </Button>
                        </Box>
                    </CardContent>
                </Card>
            </Box>
        </Container>
    );
};

export default ForgotPassword;
Javascript
//ResetPassword.js 

import { React } from "react";
import {
    useSearchParams,
    useNavigate
} from "react-router-dom";
import axios from "axios";
import Avatar from "@mui/material/Avatar";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import LockResetIcon from "@mui/icons-material/LockReset";
import Typography from "@mui/material/Typography";
import Container from "@mui/material/Container";
import {
    Card,
    CardContent
} from "@mui/material";
import { toast } from "react-toastify";

const ResetPassword = () => {
    const [searchParams] = useSearchParams();
    let navigate = useNavigate();
    const userId = searchParams.get("id");
    const token = searchParams.get("token");

    const handleSubmit = async (e) => {
        e.preventDefault();
        const data = new FormData(e.currentTarget);
        const newpassword = data.get("newpassword");
        const confirmpassword = data.get("confirmpassword");
        if (newpassword !== confirmpassword)
            toast.error(`New Password and 
                         Confirm Password do not match !`, {
                autoClose: 5000,
                position: "top-right",
            });
        else {
            const url = process.env.REACT_APP_BACKEND_URL 
                                        + "/api/resetPassword";
            const res = await axios.post(url, {
                password: newpassword,
                token: token,
                userId: userId,
            });
            if (res.data.success === false) {
                toast.error(res.data.message, {
                    autoClose: 5000,
                    position: "top-right",
                });
            } else {
                toast.success(res.data.message, {
                    autoClose: 5000,
                    position: "top-right",
                });
                setTimeout(() => {
                    navigate("/login");
                }, 2000);
            }
        }
    };

    return (
        <Container maxWidth="sm">
            <Box
                sx={{
                    marginTop: 10,
                    display: "flex",
                    flexDirection: "column",
                    alignItems: "center",
                }}
            >
                <Card sx={{ boxShadow: "4" }}>
                    <CardContent sx={{ m: 3 }}>
                        <Avatar sx={{ m: "auto", 
                                      bgcolor: "primary.main" }}>
                            <LockResetIcon />
                        </Avatar>
                        <Typography component="h1" 
                                    variant="h5" 
                                    sx={{ mt: 1 }}>
                            Reset Password
                        </Typography>

                        <Box component="form"
                             onSubmit={handleSubmit} 
                             sx={{ mt: 1 }}>
                            <TextField
                                margin="normal"
                                required
                                fullWidth
                                type="password"
                                name="newpassword"
                                id="newpassword"
                                label="New Password"
                                autoFocus
                            />
                            <TextField
                                margin="normal"
                                required
                                fullWidth
                                type="password"
                                name="confirmpassword"
                                id="confirmpassword"
                                label="Confirm Password"
                            />
                            <Button
                                type="submit"
                                fullWidth
                                variant="contained"
                                sx={{ mt: 3, mb: 2 }}
                            >
                                Submit
                            </Button>
                        </Box>
                    </CardContent>
                </Card>
            </Box>
        </Container>
    );
};

export default ResetPassword;

Step 6: To start the frontend run the following command:

npm start

Steps to Create a NodeJS App and Installing Module:

Step 1: Create a npm project inside a new folder `backend` folder as follows:

mkdir backend
cd backend

Step 2: Initialize the Node application using the following command:

npm init -y

Step 3: Install necessary packages.

npm install --save express bcrypt cors crypto dotenv express mysql2 nodemailer 

Project Structure(Backend):

backend-folder-structure

Backend Folder Structure

The updated dependencies in package.json file of backend will look like:

"dependencies": {
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"mysql2": "^3.9.2",
"nodemailer": "^6.9.12"
}

Step 4: Create a .env file in your backend’s root folder and put your email id and password inside it to hide them.

PORT=4000
DATABASE_HOST=localhost
DATABSE_USER=<your_db_username>
DATABSE_PASSWORD=<your_db_password>
DATABASE_PORT=3306
DATABASE_NAME=users
DATABASE_CONNECTION_LIMIT=20
EMAIL_ID=<your_email>
EMAIL_PASSWORD=<your_email_app_password>
FORGOT_PASSWORD_TOKEN_SECRET=somesecret
FRONTEND_URL=http://localhost:3000
NO_OF_SALT_ROUNDS=10

Step 5: Create the files `server.js`, `routes.js`, `email.js` and `db.js` which will run our Express Server and serve the React App with responses to routes hit.

Javascript
//server.js

require("dotenv").config();

const express = require("express");
const cors = require("cors");
const Route = require("./routes/route");

const app = express();
const PORT = process.env.PORT || 4000;

app.use(express.json());
app.use(
  cors({
    origin: "http://localhost:3000",
    methods: ["GET", "POST"],
  })
);

app.use("/api", Route);

app.listen(PORT, () => {
  console.log(`Server started on PORT ${PORT}`);
});
Javascript
// db.js

require("dotenv").config();

const mysql = require("mysql2");

const pool = mysql.createPool({
  host: process.env.DATABASE_HOST,
  port: process.env.DATABASE_PORT,
  user: process.env.DATABSE_USER,
  password: process.env.DATABSE_PASSWORD,
  database: process.env.DATABASE_NAME,
  connectionLimit: process.env.DATABASE_CONNECTION_LIMIT,
});

let db = {};

db.get_user_by_email = (email) => {
  const query = `SELECT email, id from details WHERE email = "${email}";`;
  return execute(query);
};

db.update_forgot_password_token = (id, token) => {
  const createdAt = new Date().toISOString();
  const expiresAt = new Date(Date.now() + 60 * 60 * 24 * 1000).toISOString();
  const query = `INSERT INTO reset_tokens(token, created_at, expires_at, user_id) VALUES('${token}', '${createdAt}', '${expiresAt}', ${id})`;
  return execute(query);
};

db.get_password_reset_token = (id) => {
  const query = `SELECT token, expires_at from reset_tokens WHERE user_id = ${id} ORDER BY created_at DESC LIMIT 1;`;
  return execute(query);
};

db.update_password_reset_token = (id) => {
  const query = `DELETE FROM reset_tokens WHERE user_id = ${id}`;
  return execute(query);
};

db.update_user_password = (id, password) => {
  const query = `UPDATE details SET password = '${password}' WHERE id = ${id}`;
  return execute(query);
};

const execute = (query) => {
  return new Promise((resolve, reject) => {
    pool.query(query, (err, results) => {
      if (err) return reject(err);
      return resolve(results);
    });
  });
};

module.exports = db;
Javascript
// ./utils/email.js

require("dotenv").config();

const nodemailer = require("nodemailer");

const sendEmail = async (option) => {
  try {
    const transporter = nodemailer.createTransport({
      service: "gmail",
      auth: {
        user: process.env.EMAIL_ID,
        pass: process.env.EMAIL_PASSWORD,
      },
    });
    const mailOption = {
      from: process.env.EMAIL_ID,
      to: option.email,
      subject: option.subject,
      html: option.message,
    };
    await transporter.sendMail(mailOption, (err, info) => {
      if (err) console.log(err);
    });
  } catch (err) {
    console.log(err);
  }
};

const mailTemplate = (content, buttonUrl, buttonText) => {
  return `<!DOCTYPE html>
  <html>
  <body style="text-align: center; font-family: 'Verdana', serif; color: #000;">
    <div
      style="
        max-width: 400px;
        margin: 10px;
        background-color: #fafafa;
        padding: 25px;
        border-radius: 20px;
      "
    >
      <p style="text-align: left;">
        ${content}
      </p>
      <a href="${buttonUrl}" target="_blank">
        <button
          style="
            background-color: #444394;
            border: 0;
            width: 200px;
            height: 30px;
            border-radius: 6px;
            color: #fff;
          "
        >
          ${buttonText}
        </button>
      </a>
      <p style="text-align: left;">
        If you are unable to click the above button, copy paste the below URL into your address bar
      </p>
      <a href="${buttonUrl}" target="_blank">
          <p style="margin: 0px; text-align: left; font-size: 10px; text-decoration: none;">
            ${buttonUrl}
          </p>
      </a>
    </div>
  </body>
</html>`;
};

module.exports = { sendEmail, mailTemplate };
Javascript
// ./routes/route.js

require("dotenv").config();

const express = require("express");
const crypto = require("crypto");
const bcrypt = require("bcrypt");
const router = express.Router();

const db = require("../db");
const { sendEmail, mailTemplate } = require("../utils/email");

const NumSaltRounds = Number(process.env.NO_OF_SALT_ROUNDS);

router.post("/forgotPassword", async (req, res) => {
  try {
    const email = req.body.email;
    const user = await db.get_user_by_email(email);

    if (!user || user.length === 0) {
      res.json({
        success: false,
        message: "Your are not registered!",
      });
    } else {
      const token = crypto.randomBytes(20).toString("hex");
      const resetToken = crypto
        .createHash("sha256")
        .update(token)
        .digest("hex");
      await db.update_forgot_password_token(user[0].id, resetToken);

      const mailOption = {
        email: email,
        subject: "Forgot Password Link",
        message: mailTemplate(
          "We have received a request to reset your password. Please reset your password using the link below.",
          `${process.env.FRONTEND_URL}/resetPassword?id=${user[0].id}&token=${resetToken}`,
          "Reset Password"
        ),
      };
      await sendEmail(mailOption);
      res.json({
        success: true,
        message: "A password reset link has been sent to your email.",
      });
    }
  } catch (err) {
    console.log(err);
  }
});

router.post("/resetPassword", async (req, res) => {
  try {
    const { password, token, userId } = req.body;
    const userToken = await db.get_password_reset_token(userId);
    if (!res || res.length === 0) {
      res.json({
        success: false,
        message: "Some problem occurred!",
      });
    } else {
      const currDateTime = new Date();
      const expiresAt = new Date(userToken[0].expires_at);
      if (currDateTime > expiresAt) {
        res.json({
          success: false,
          message: "Reset Password link has expired!",
        });
      } else if (userToken[0].token !== token) {
        res.json({
          success: false,
          message: "Reset Password link is invalid!",
        });
      } else {
        await db.update_password_reset_token(userId);
        const salt = await bcrypt.genSalt(NumSaltRounds);
        const hashedPassword = await bcrypt.hash(password, salt);
        await db.update_user_password(userId, hashedPassword);
        res.json({
          success: true,
          message: "Your password reset was successfully!",
        });
      }
    }
  } catch (err) {
    console.log(err);
  }
});

module.exports = router;

Step 12: Assuming that we are using MySQL database which contains `users` database with two tables `details` and `reset_tokens` whose schema is as follows:

CREATE TABLE details
(
id int auto_increment primary key,
email varchar(255) not null unique,
name varchar(255) not null,
password varchar(255) not null
);

create table reset_tokens
(
token varchar(255) not null,
created_at varchar(255) not null,
expires_at varchar(255) not null,
user_id int not null,
PRIMARY KEY(user_id, token)
);

To start the backend server run the following command:

node server.js

Output:

Demo

Forgot and Reset Password with React and Node JS



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads