Node.js + MySQL : Add Forgot/Reset Password to Login-Authentication System.

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:

  1. Set up Nodejs + MySQL Boilerplate API
  2. Install Nodemailer Module
  3. Add the forgotPassword middleware
  4. Add a forgot password link to the Login Page
  5. Define the sendEmail() function
  6. Define the sendPasswordResetEmail() function
  7. Add the forgotPassword Route to the Router
  8. Add the resetPassword middleware
  9. Define the validateResetToken middleware function
  10. Add the resetPassword Route to the Router
  11. Test forgotPassword route with postman
  12. Test resetPassword with Postman

Related Articles:

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:

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:

  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.

Using MySQL Event Scheduler: A Complete Guide.

Leave a Comment

Your email address will not be published. Required fields are marked *

Translate »