This article is a follow-up to a previous article: Complete JWT Authentication and Authorization System for MySQL/Node.js API. However, in this tutorial, we are not using the JWT token to store our password reset token, So you can easily adapt it depending on your database and API.
This tutorial will show you how to add and implement a forgot and reset password feature to your MySQL + node.js API by using the Nodemailer module to send an email link to the user account and allow him to reset his password when he forgets it securely.
To add the forgot/reset password feature to your node.js API, you need to follow these steps:
- Set up Nodejs + MySQL Boilerplate API
- Install Nodemailer Module
- Add the forgotPassword middleware
- Add a forgot password link to the Login Page
- Define the sendEmail() function
- Define the sendPasswordResetEmail() function
- Add the forgotPassword Route to the Router
- Add the resetPassword middleware
- Define the validateResetToken middleware function
- Add the resetPassword Route to the Router
- Test forgotPassword route with postman
- Test resetPassword with Postman
Related Articles:
- Best practices for optimizing MySQL database performance.
- How to add Custom Error Handler Middleware to your Node.js/Express API.
- Complete JWT Authentication and Authorization System for MySQL/Node.js API.
- Nodemailer + Gmail: How to Send Emails from Node.js API using Gmail.
- Node.js + Nodemailer : How to send Emails via SMTP with Nodemailer
- How to get Request Origin in Express.js?
- How to get the Request Host in express.js?
- How to Get Current Date and Time in JavaScript.
- How to Build a Complete API for User Login and Authentication using MySQL and Node.js.
- How to store Session in MySQL Database using express-mysql-session.
- Complete JWT Authentication and Authorization System for MySQL/Node.js API
1. Node.js + MySQL Boilerplate API
For this tutorial, we will use the same boilerplate MySQL/Node.js API that we used in the previous article: Complete JWT Authentication and Authorization System for MySQL/Node.js API. and we will name it here “forgotResetPassword-API”. The project directory contains the following files:
- server.js: Our boilerplate for the node.js server uses express as a web server. We mount the existing apiRouter into server.js.
- .env: To save all your environment variables and the configuration data and database connection options for the MySQL database.
- db.js: In this file, we will write our queries to connect the database and interact with its tables. We define these queries as methods of the db object in db.js.
- apiRouter.js: This apiRouter file will be the starting point for all your API routes, so we will add our forgotPassword and resetPassword routes in this file.
If you want to know more details about the start-server we are using in this article, go ahead and check this article: Complete Guide to Build a RESTful API with Node.js and Express
server.js
require("dotenv").config();
const express =require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const apiRouter = require('./apiRouter');
const app = express();
const PORT= process.env.PORT;
app.use(bodyParser.json());
app.use(cors());
app.use('/apiRouter',apiRouter)
app.listen(PORT, ()=>{
console.log(`server is listening on ${PORT}`);
});
module.exports = app;
.env
PORT = 3000
DB_PORT = “yourDBport”
DB_HOST = localhost
DB_USER = “yourusername”
DB_PASS = “yourDBpassword”
MYSQL_DB = “yourDBname”
CONNECTION_LIMIT = 10
package.json
//package.json
{
"name": "forgotResetPassword-API",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon server.js "
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"mysql": "^2.18.1",
"nodemon": "^2.0.4",
"save": "^2.4.0"
}
}
apiRouter.js
//apiRouter.js
const express =require('express');
const apiRouter = express.Router();
module.exports = apiRouter;
This tutorial will store our data in a MySQL database named “forgotResetPasswordDB”. Two tables are required to implement the forgot/reset password features:
- User table: This table should have an email address set up when signing up.
- ResetPasswordToken: to save the reset Token.
We will use the following queries to create the User table:
//create the User table
pool.query('CREATE TABLE User (' +
'id int(11) NOT NULL AUTO_INCREMENT,' +
'user_name varchar(255) NOT NULL,' +
'role varchar(255) default "employee",' +
'email varchar(255) NOT NULL,' +
'password varchar(255) NOT NULL,' +
'PRIMARY KEY (id),'+
'UNIQUE KEY email_UNIQUE (email),' +
'UNIQUE KEY password_UNIQUE (password))', function (err, result) {
if (err) throw err;
console.log("User created");
}
);
And we will use the following queries to create the ResetPasswordToken table:
//create the resetPasswordToken table
pool.query('CREATE TABLE ResetPasswordToken (' +
'id INT NOT NULL AUTO_INCREMENT,' +
'email VARCHAR(255) NOT NULL,' +
'Token_value VARCHAR(350) NOT NULL,' +
'created_at datetime NOT NULL ,' +
'expired_at datetime NOT NULL,' +
'used INT(11) NOT NULL default "0",' +
'inserted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id),' +
'UNIQUE INDEX id_UNIQUE (id ASC))' , function (err, result) {
if (err) throw err;
console.log("resetPasswordToken created");
}
);
Check the following link to see how to create a database using node.js: How to create MySQL database using node.js.
We use connection pooling instead of a single connection to connect our database. If you want to know more about connection pooling, read this article: Why is Connection Pooling better than Single Connection?.
db.js
//db.js
const mysql = require('mysql');
const pool = mysql.createPool({
connectionLimit: process.env.CONNECTION_LIMIT, // the number of connections node.js will hold open to our database
password: process.env.DB_PASS,
user: process.env.DB_USER,
database: process.env.MYSQL_DB,
host: process.env.DB_HOST,
port: process.env.DB_PORT
});
//create the User table
pool.query('CREATE TABLE User (' +
'id int(11) NOT NULL AUTO_INCREMENT,' +
'user_name varchar(255) NOT NULL,' +
'role varchar(255) default "employee",' +
'email varchar(255) NOT NULL,' +
'password varchar(255) NOT NULL,' +
'PRIMARY KEY (id),'+
'UNIQUE KEY email_UNIQUE (email),' +
'UNIQUE KEY password_UNIQUE (password))', function (err, result) {
if (err) throw err;
console.log("User created");
}
);
//create the resetPasswordToken table
pool.query('CREATE TABLE ResetPasswordToken (' +
'id INT NOT NULL AUTO_INCREMENT,' +
'email VARCHAR(255) NOT NULL,' +
'Token_value VARCHAR(350) NOT NULL,' +
'created_at datetime NOT NULL ,' +
'expired_at datetime NOT NULL,' +
'used INT(11) NOT NULL default "0",' +
'inserted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id),' +
'UNIQUE INDEX id_UNIQUE (id ASC))' , function (err, result) {
if (err) throw err;
console.log("resetPasswordToken created");
}
);
let db = {}; //create an empty object you will use later to write and export your queries.
module.exports = db
2. Install Nodemailer Module
Nodemailer is a Node.js package that makes sending emails as simple as pie. The project began in 2010 when there was no logical way to send email messages, and it is now the default solution for most Node.js users. If you want to know more details about this module and how to use it, you can check these links:
- Nodemailer + Gmail: How to Send Emails from Node.js API using Gmail.
- Node.js + Nodemailer : How to send Emails via SMTP with Nodemailer.
In this tutorial, we use the nodemailer module to send password reset emails from our API to the user account.
To use the Nodemailer module, we have first to download and install it using npm:
>npm install --save nodemailer
The next step is to include the module in our application by adding the following line of code to our apiRouter.js file.
const nodemailer = require('nodemailer');
Now we are ready to send emails using nodemailer in our forgotPassword middleware.
3. Add the forgotPassword middleware
apiRouter.post('/forgotPassword', async(req, res, next)=>{
});
Step1: Add The Link To Your Login Page
Before adding forgotPassword and resetPassword routes to the API, you need first to add a link to your login page (in your front-end app ) that will redirect the user to the rest-password page where he is required to enter his email address. After entering the email address, a request is sent to the backend (our node.js API) to see if this email address already exists in the User table.
Step2: Define the sendEmail() function
We define this function in our apiRouter.js file. The sendEmail() function will create a reusable Nodemailer transporter(smtpOptions) using the SMTP transport mechanism and then deliver the message object using the sendMailer() method.
You can notice that we are using:
- A generated test SMTP service account from ethereal.email: an online tool for email testing in a pre-production environment; this tool will catch our messages, display how they should look in a real email client, and help analyze and debug them. However, if you want to see how to use your real Gmail account for test, you can check these articles:
- async..await with Nodemailer. To use it, you need to have at least Node v8.0.0.
async function sendEmail({ to, subject, html, from = process.env.EMAIL_FROM }) {
const transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
auth: {
user: process.env.USER, // generated ethereal user
pass: process.env.PASS // generated ethereal password
}
})
await transporter.sendMail({ from, to, subject, html });
console.log("email sent sucessfully");
};
.env
PORT = 3000
DB_PORT = “yourDBport”
DB_HOST = localhost
DB_USER = “yourusername”
DB_PASS = “yourDBpassword”
MYSQL_DB = “forgotResetPasswordDB”
CONNECTION_LIMIT = 10
EMAIL_FROM = "putYourEmail"
NAME = "userName"
USER = "userEmail"
PASS = "userPassword"
Step3: Define the sendPasswordResetEmail() function
In the sendPasswordResetEmail() function, we set up the HTML message that we will send in our email body and define the sendEmail object for the sendEmail() function defined in the section above.
To learn more about request Origin and request Host, you can read these two articles:
async function sendPasswordResetEmail(email, resetToken, origin) {
let message;
if (origin) {
const resetUrl = `${origin}/apiRouter/resetPassword?token=${resetToken} email=${email}`;
message = `<p>Please click the below link to reset your password, the following link will be valid for only 1 hour:</p>
<p><a href="${resetUrl}">${resetUrl}</a></p>`;
} else {
message = `<p>Please use the below token to reset your password with the <code>/apiRouter/reset-password</code> api route:</p>
<p><code>${resetToken}</code></p>`;
}
await sendEmail({
from: process.env.EMAIL_FROM,
to: email,
subject: ' Reset your Password',
html: `<h4>Reset Password</h4>
${message}`
});
}
Step4: Add the forgotPassword Route to the apiRouter
In this route, we need first to get the email, and the origin from the request, then verify that we have a user with the email by using getUserByEmail () method defined in the db.js file. However, we always return ok responses to prevent email enumeration and do not let attackers know which email exists and which one does not exist.
To learn more about request Origin and request Host, you can read these two articles:
The next step is to get all the previously set tokens for this user and set used to 1. This will prevent old and expired tokens from being used.
Then we need to create a random reset token that expires after 1 hour. We need to download and install the crypto npm module and then use the db.expireOldTokens() method.
> npm install --save crypto
Then include it in your apiRouter.js
const crypto = require('crypto');
After creating the reset token, we can save it by inserting it into the resetPasswordToken table using the insertResetToken() method.
The last step is the send the email containing the HTML message using the function defined in the section above: sendPasswordResetEmail().
apiRouter.post('/forgotPassword', async(req, res, next)=>{
try{
const email = req.body.email;
const origin = req.header('Origin'); // we are getting the request origin from the origin header.
const user = await db.getUserByEmail(email);
if(!user){
// here we always return ok response to prevent email enumeration
return res.json({status: 'ok'});
}
// Get all the tokens that were previously set for this user and set used to 1. This will prevent old and expired tokens from being used.
await db.expireOldTokens(email, 1);
// create reset token that expires after 1 hours
const resetToken = crypto.randomBytes(40).toString('hex');
const resetTokenExpires = new Date(Date.now() + 60*60*1000);
const createdAt = new Date(Date.now());
const expiredAt = resetTokenExpires;
//insert the new token into resetPasswordToken table
await db.insertResetToken(email, resetToken,createdAt, expiredAt, 0);
// send email
await sendPasswordResetEmail(email,resetToken, origin);
res.json({ message: 'Please check your email for a new password' });
} catch(e){
console.log(e);
}
});
db.js
//db.js
const mysql = require('mysql');
const pool = mysql.createPool({
connectionLimit: process.env.CONNECTION_LIMIT, // the number of connections node.js will hold open to our database
password: process.env.DB_PASS,
user: process.env.DB_USER,
database: process.env.MYSQL_DB,
host: process.env.DB_HOST,
port: process.env.DB_PORT
});
//create the User table
pool.query('CREATE TABLE User (' +
'id int(11) NOT NULL AUTO_INCREMENT,' +
'user_name varchar(255) NOT NULL,' +
'role varchar(255) default "employee",' +
'email varchar(255) NOT NULL,' +
'password varchar(255) NOT NULL,' +
'PRIMARY KEY (id),'+
'UNIQUE KEY email_UNIQUE (email),' +
'UNIQUE KEY password_UNIQUE (password))', function (err, result) {
if (err) throw err;
console.log("User created");
}
);
//create the resetPasswordToken table
pool.query('CREATE TABLE ResetPasswordToken (' +
'id INT NOT NULL AUTO_INCREMENT,' +
'email VARCHAR(255) NOT NULL,' +
'Token_value VARCHAR(350) NOT NULL,' +
'created_at datetime NOT NULL ,' +
'expired_at datetime NOT NULL,' +
'used INT(11) NOT NULL default "0",' +
'inserted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id),' +
'UNIQUE INDEX id_UNIQUE (id ASC))' , function (err, result) {
if (err) throw err;
console.log("resetPasswordToken created");
}
);
let db = {
// ***Requests to the User table ***
db.getUserByEmail = (email) =>{
return new Promise((resolve, reject)=>{
pool.query('SELECT * FROM User WHERE email = ?', [email], (error, users)=>{
if(error){
return reject(error);
}
return resolve(users[0]);
});
});
};
// ***Requests to the resetPasswordToken table ***
db.insertResetToken = (email,tokenValue, createdAt, expiredAt, used) =>{
return new Promise((resolve, reject)=>{
pool.query('INSERT INTO ResetPasswordToken ( email, Token_value,created_at, expired_at, used) VALUES (?, ?,?, ?, ?)', [email,tokenValue, createdAt, expiredAt, used], (error, result)=>{
if(error){
return reject(error);
}
return resolve(result.insertId);
});
});
};
};
db.expireOldTokens = (email, used) =>{
return new Promise((resolve, reject)=>{
pool.query('UPDATE ResetPasswordToken SET used = ? WHERE email = ?', [ used, email], (error)=>{
if(error){
return reject(error);
}
return resolve();
});
});
};
module.exports = db
4. Add the resetPassword middleware
In the apiRouter.js file, we will define the resetPassword route following these two steps:
- Define the validateResetToken middleware function.
- Add the resetPassword Route to the apiRouter.
apiRouter.post('/resetPassword', validateResetToken, async(req, res, next)=>{
});
Step1: Define the validateResetToken middleware function
This function will get first the email, and the token from the request, then verify if the token exists in the resetPasswordToken and has not expired using the findValidToken() method. If the token exists, go to next, and if it does not, throw an error.
In the next section, we will add the validateResetToken middleware function to the resetPassword route.
// Reset token validate
async function validateResetToken (req, res, next){
const email = req.body.email;
const resetToken = req.body.token;
if (!resetToken || !email) {
return res.sendStatus(400);
}
// then we need to verify if the token exist in the resetPasswordToken and not expired.
const currentTime = new Date(Date.now());
const token = await db.findValidToken(resetToken, email, currentTime);
if (!token) {
res.json ( 'Invalid token, please try again.');
}
next();
};
db.js
//db.js
const mysql = require('mysql');
const pool = mysql.createPool({
connectionLimit: process.env.CONNECTION_LIMIT, // the number of connections node.js will hold open to our database
password: process.env.DB_PASS,
user: process.env.DB_USER,
database: process.env.MYSQL_DB,
host: process.env.DB_HOST,
port: process.env.DB_PORT
});
//create the User table
pool.query('CREATE TABLE User (' +
'id int(11) NOT NULL AUTO_INCREMENT,' +
'user_name varchar(255) NOT NULL,' +
'role varchar(255) default "employee",' +
'email varchar(255) NOT NULL,' +
'password varchar(255) NOT NULL,' +
'PRIMARY KEY (id),'+
'UNIQUE KEY email_UNIQUE (email),' +
'UNIQUE KEY password_UNIQUE (password))', function (err, result) {
if (err) throw err;
console.log("User created");
}
);
//create the resetPasswordToken table
pool.query('CREATE TABLE ResetPasswordToken (' +
'id INT NOT NULL AUTO_INCREMENT,' +
'email VARCHAR(255) NOT NULL,' +
'Token_value VARCHAR(350) NOT NULL,' +
'created_at datetime NOT NULL ,' +
'expired_at datetime NOT NULL,' +
'used INT(11) NOT NULL default "0",' +
'inserted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id),' +
'UNIQUE INDEX id_UNIQUE (id ASC))' , function (err, result) {
if (err) throw err;
console.log("resetPasswordToken created");
}
);
let db = {
// ***Requests to the User table ***
db.getUserByEmail = (email) =>{
return new Promise((resolve, reject)=>{
pool.query('SELECT * FROM User WHERE email = ?', [email], (error, users)=>{
if(error){
return reject(error);
}
return resolve(users[0]);
});
});
};
// ***Requests to the resetPasswordToken table ***
db.insertResetToken = (email,tokenValue, createdAt, expiredAt, used) =>{
return new Promise((resolve, reject)=>{
pool.query('INSERT INTO ResetPasswordToken ( email, Token_value,created_at, expired_at, used) VALUES (?, ?,?, ?, ?)', [email,tokenValue, createdAt, expiredAt, used], (error, result)=>{
if(error){
return reject(error);
}
return resolve(result.insertId);
});
});
};
db.expireOldTokens = (email, used) =>{
return new Promise((resolve, reject)=>{
pool.query('UPDATE ResetPasswordToken SET used = ? WHERE email = ?', [ used, email], (error)=>{
if(error){
return reject(error);
}
return resolve();
});
});
};
};
db.findValidToken = (token, email, currentTime) =>{
return new Promise((resolve, reject)=>{
pool.query('SELECT * FROM ResetPasswordToken WHERE (email = ? AND Token_value = ? AND expired_at > ?)', [email,token, currentTime ], (error, tokens)=>{
if(error){
return reject(error);
}
return resolve(tokens[0]);
//return resolve(token);
});
});
};
module.exports = db
Step2: Add the resetPassword Route to the apiRouter
After getting the new password from the request, we must verify and hash it for security reasons before saving and updating it in the User table using db.updateUserPassword() method. We use the bcrypt module to generate a password hash and store it in the database.
Download and install the bcrypt module:
>npm install --save bcrypt
include it into your apiRouter:
const { hashSync, genSaltSync } = require("bcrypt");
apiRouter.post('/resetPassword', validateResetToken, async(req, res, next)=>{
try{
const newPassword = req.body.password;
const email = req.body.email;
if (!newPassword) {
return res.sendStatus(400);
}
const user = await db.getUserByEmail(email);
const salt = genSaltSync(10);
const password = hashSync(newPassword, salt);
await db.updateUserPassword(password, user.id);
res.json({ message: 'Password reset successful, you can now login with the new password' });
} catch(e){
console.log(e);
}
})
db.js
//db.js
const mysql = require('mysql');
const pool = mysql.createPool({
connectionLimit: process.env.CONNECTION_LIMIT, // the number of connections node.js will hold open to our database
password: process.env.DB_PASS,
user: process.env.DB_USER,
database: process.env.MYSQL_DB,
host: process.env.DB_HOST,
port: process.env.DB_PORT
});
//create the User table
// pool.query('CREATE TABLE User (' +
// 'id int(11) NOT NULL AUTO_INCREMENT,' +
// 'user_name varchar(255) NOT NULL,' +
// 'role varchar(255) default "employee",' +
// 'email varchar(255) NOT NULL,' +
// 'password varchar(255) NOT NULL,' +
// 'PRIMARY KEY (id),'+
// 'UNIQUE KEY email_UNIQUE (email),' +
// 'UNIQUE KEY password_UNIQUE (password))', function (err, result) {
// if (err) throw err;
// console.log("User created");
// }
// );
//create the resetPasswordToken table
// pool.query('CREATE TABLE ResetPasswordToken (' +
// 'id INT NOT NULL AUTO_INCREMENT,' +
// 'email VARCHAR(255) NOT NULL,' +
// 'Token_value VARCHAR(350) NOT NULL,' +
// 'created_at datetime NOT NULL ,' +
// 'expired_at datetime NOT NULL,' +
// 'used INT(11) NOT NULL default "0",' +
// 'inserted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
// 'PRIMARY KEY (id),' +
// 'UNIQUE INDEX id_UNIQUE (id ASC))' , function (err, result) {
// if (err) throw err;
// console.log("resetPasswordToken created");
// }
// );
let db = {};
// ***Requests to the User table ***
db.allUser = () =>{
return new Promise((resolve, reject)=>{
pool.query('SELECT * FROM User ', (error, users)=>{
if(error){
return reject(error);
}
return resolve(users);
});
});
};
db.getUserByEmail = (email) =>{
return new Promise((resolve, reject)=>{
pool.query('SELECT * FROM User WHERE email = ?', [email], (error, users)=>{
if(error){
return reject(error);
}
return resolve(users[0]);
});
});
};
db.insertUser = (userName, email, password) =>{
return new Promise((resolve, reject)=>{
pool.query('INSERT INTO User (user_name, email, password) VALUES (?, ?, ?)', [userName, email, password], (error, result)=>{
if(error){
return reject(error);
}
return resolve(result.insertId);
});
});
};
db.updateUser = (userName, role, email, password, id) =>{
return new Promise((resolve, reject)=>{
pool.query('UPDATE User SET user_name = ?, role= ?, email= ?, password=? WHERE id = ?', [userName, role, email, password, id], (error)=>{
if(error){
return reject(error);
}
return resolve();
});
});
};
db.updateUserPassword = ( password, id) =>{
return new Promise((resolve, reject)=>{
pool.query('UPDATE User SET password=? WHERE id = ?', [ password, id], (error)=>{
if(error){
return reject(error);
}
return resolve();
});
});
};
db.deleteUser = (id) =>{
return new Promise((resolve, reject)=>{
pool.query('DELETE FROM User WHERE id = ?', [id], (error)=>{
if(error){
return reject(error);
}
return resolve(console.log("User deleted"));
});
});
};
// ***Requests to the resetPasswordToken table ***
db.expireOldTokens = (email, used) =>{
return new Promise((resolve, reject)=>{
pool.query('UPDATE ResetPasswordToken SET used = ? WHERE email = ?', [ used, email], (error)=>{
if(error){
return reject(error);
}
return resolve();
});
});
};
db.insertResetToken = (email,tokenValue, createdAt, expiredAt, used) =>{
return new Promise((resolve, reject)=>{
pool.query('INSERT INTO ResetPasswordToken ( email, Token_value,created_at, expired_at, used) VALUES (?, ?,?, ?, ?)', [email,tokenValue, createdAt, expiredAt, used], (error, result)=>{
if(error){
return reject(error);
}
return resolve(result.insertId);
});
});
};
db.findValidToken = (token, email, currentTime) =>{
return new Promise((resolve, reject)=>{
pool.query('SELECT * FROM ResetPasswordToken WHERE (email = ? AND Token_value = ? AND expired_at > ?)', [email,token, currentTime ], (error, tokens)=>{
if(error){
return reject(error);
}
return resolve(tokens[0]);
});
});
};
module.exports = db
apiRouter.js
const express =require('express');
const apiRouter = express.Router();
const db = require('./db');
const { hashSync, genSaltSync } = require("bcrypt");
const nodemailer = require('nodemailer');
const crypto = require('crypto');
apiRouter.post('/forgotPassword', async(req, res, next)=>{
try{
const email = req.body.email;
console.log(email);
const origin = req.header('Origin'); // we are getting the request origin from the HOST header
const user = await db.getUserByEmail(email);
if(!user){
// here we always return ok response to prevent email enumeration
return res.json({status: 'ok'});
}
// Get all the tokens that were previously set for this user and set used to 1. This will prevent old and expired tokens from being used.
await db.expireOldTokens(email, 1);
// create reset token that expires after 1 hours
const resetToken = crypto.randomBytes(40).toString('hex');
const resetTokenExpires = new Date(Date.now() + 60*60*1000);
const createdAt = new Date(Date.now());
const expiredAt = resetTokenExpires;
//insert the new token into resetPasswordToken table
await db.insertResetToken(email, resetToken,createdAt, expiredAt, 0);
// send email
await sendPasswordResetEmail(email,resetToken, origin);
res.json({ message: 'Please check your email for a new password' });
} catch(e){
console.log(e);
}
});
async function sendEmail({ to, subject, html, from = process.env.EMAIL_FROM }) {
const transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
auth: {
user: process.env.USER, // generated ethereal user
pass: process.env.PASS // generated ethereal password
}
})
await transporter.sendMail({ from, to, subject, html });
console.log("email sent sucessfully");
};
async function sendPasswordResetEmail(email, resetToken, origin) {
let message;
if (origin) {
const resetUrl = `${origin}/apiRouter/resetPassword?token=${resetToken} email=${email}`;
message = `<p>Please click the below link to reset your password, the link will be valid for 1 hour:</p>
<p><a href="${resetUrl}">${resetUrl}</a></p>`;
} else {
message = `<p>Please use the below token to reset your password with the <code>/apiRouter/reset-password</code> api route:</p>
<p><code>${resetToken}</code></p>`;
}
await sendEmail({
from: process.env.EMAIL_FROM,
to: email,
subject: ' Reset your Password',
html: `<h4>Reset Password </h4>
${message}`
});
}
// Reset token validate
async function validateResetToken (req, res, next){
const email = req.body.email;
const resetToken = req.body.token;
if (!resetToken || !email) {
return res.sendStatus(400);
}
// then we need to verify if the token exist in the resetPasswordToken and not expired.
const currentTime = new Date(Date.now());
const token = await db.findValidToken(resetToken, email, currentTime);
if (!token) {
res.json ( 'Invalid token, please try again.');
}
next();
};
apiRouter.post('/resetPassword', validateResetToken, async(req, res, next)=>{
try{
const newPassword = req.body.password;
const email = req.body.email;
if (!newPassword) {
return res.sendStatus(400);
}
const user = await db.getUserByEmail(email);
const salt = genSaltSync(10);
const password = hashSync(newPassword, salt);
await db.updateUserPassword(password, user.id);
res.json({ message: 'Password reset successful, you can now login with the new password' });
} catch(e){
console.log(e);
}
})
module.exports = apiRouter;
5. Our Database in MYSQLWorkbench
The following MYAQLWorkbench interface confirms the creation of our forgotResetPasswordDB and the two tables: User and resetPasswordToken.

6. How to test forgotPassword route with postman
To reenable access to a user account with a forgotten password, the user should submit his email address to the /apiRouter/forgotPassword route, which will deliver a token in the email, allowing the user to reset his password. we can test this feature in our API using the following setting in your Postman:
- Path: apiRouter/forgotPassword.
- Method: POST.
- URL: http://localhost:3000/apiRouter/forgotPassword.
- Change the body type to “raw”.
- Change the format to “JSON”.
- The json object:
{ “email”: “[email protected]”}
- click send and you will get an email with the reset token.

Ethereal mailbox

7. How to test resetPassword with Postman
To test the resetPassword route of our API, the user should submit the reset token with the new password to the /apiRouter/resetPassword route. We can test this feature in our API using the following setting in your Postman:
- Path: apiRouter/resetPassword.
- Method: POST.
- URL: http://localhost:3000/apiRouter/resetPassword.
- Change the body type to “raw”.
- Change the format to “JSON”.
- The json object:
{ “token”: “rest token here”, “password”: “new password”, “email”: “[email protected]”}
- click send, and you will receive a “200 OK” response with a “Password reset successful, you can now login with the new password” message in the response body.

Conclusion
This article was a tutorial on adding and implementing a forgot and reset password feature to your MySQL + node.js API by using the Nodemailer module to send an email link to the user account and allow him to reset his password when he forgets it securely. I hope this was helpful.
You might also like:
Best practices for optimizing MySQL database performance.
How to add Custom Error Handler Middleware to your Node.js/Express API.
Nodemailer + Gmail: How to Send Emails from Node.js API using Gmail.
Complete JWT Authentication and Authorization System for MySQL/Node.js API.
How to Build a Complete API for User Login and Authentication using MySQL and Node.js.
How to store Session in MySQL Database using express-mysql-session.
How to Get Current Date and Time in JavaScript.
How to interact with MySQL database using async/await promises in node.js ?
How to use Sequelize async/await to interact with MySQL database in Node.js.
MANY-TO-MANY Association in MYSQL Database using Sequelize async/await with Node.js.
ONE-TO-ONE Association in MYSQL Database using Sequelize async/await with Node.js
ONE-TO-ONE Association in MYSQL Database using Sequelize async/await with Node.js.
How to add Routes to insert data into MySQL database-related tables in Node.js API?
Example How to use initialize() Function in Node.js/Express API .
Why is Connection Pooling better than Single Connection?.
How to create MySQL database using node.js.
Automating Your MySQL Database with Triggers.
How to use ON DELETE CASCADE in MySQL.
MySQL + Event Scheduler: How to delete data from MYSQL table using Event Scheduler.