Project Structure(Backend)
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.
//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}`);
});
// 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;
// ./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 };
// ./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:
Forgot & Reset Password Feature with React and Node JS
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