DEV Community

loading...

How to implement reset password functionality with Node/PostgreSQL/Knex

anthonyamaro15 profile image anthony amaro ・4 min read

Have you ever wondered how to implement a reset password functionality on the server with NodeJS and PostgreSQL?

I found many articles and tutorials on how to do it with Node and MongoDB, here I'm going to tell you how I've done it with PostgreSQL.

First, we need to find an email service, I've tried Nodemailer at first and it works perfectly in my local environment, however, in production I was getting some security issues with my Gmail account. That's why I decided to go with SendGrid.

Create a SendGrid account, go to Email API, Integration Guide, choose Web API and just follow instructions to generate an API Key

Alt Text

Install dependencies

npm install --save @sendgrid/mail
npm install express bcryptjs jsonwebtoken knex
Enter fullscreen mode Exit fullscreen mode

Now that you have created the API key, create a .env file to store your key.

Let's see how our user table looks
I'm using the Knex library to interact with the database

table.increments();
table.string("email", 128).unique().notNullable();
table.string("password", 128).notNullable();
table.string("resetLink", 255);
Enter fullscreen mode Exit fullscreen mode

As you see we define "resetLink" to be optional.

Now let's create the routes

const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const sgMail = require('@sendgrid/mail');

const route = express.Router();
// bring env variables into file
const sendGridKey = process.env.SENDGRID_KEY;
const resetSecret = process.env.RESET_SECRET;

route.patch('/forgot-password', async (req, res) => {
  const { email } = req.body;

  try {
    // look for email in database
    const [user] = await filterBy({ email });
    // if there is no user send back an error
    if(!user) {
      res.status(404).json({ error: "Invalid email" });
    } else {
      // otherwise we need to create a temporary token that expires in 10 mins
      const resetLink = jwt.sign({ user: user.email }, 
      resetSecret, { expiresIn: '10m' });
      // update resetLink property to be the temporary token and then send email
      await update(user.id, { resetLink });
      // we'll define this function below
      sendEmail(user, resetLink);
      res.status(200).json({ message: "Check your email"} );
    }
  } catch(error) {
    res.status(500).json({ message: error.message });
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's define the new PATCH route to update the new password


route.patch('/reset-password/:token', async (req, res) => {
  // Get the token from params
  const resetLink = req.params.token;
  const newPassword = req.body;

  // if there is a token we need to decoded and check for no errors
  if(resetLink) {
    jwt.verify(resetLink, resetPassword, (error, decodedToken) => {
         if(error) {
           res.status().json({ message: 'Incorrect token or expired' })
         }
    })
  }

  try {
    // find user by the temporary token we stored earlier
    const [user] = await filterBy({ resetLink });

    // if there is no user, send back an error
    if(!user) {
      res.status(400).json({ message: 'We could not find a match for this link' });
    }

    // otherwise we need to hash the new password  before saving it in the database
    const hashPassword = bcrypt.hashSync(newPassword.password, 8);
    newPassword.password = hashPassword;

    // update user credentials and remove the temporary link from database before saving
    const updatedCredentials = {
      password: newPassword.password,
      resetLink: null
    }

    await update(user.id, updatedCredentials);
    res.status(200).json({ message: 'Password updated' });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
})
Enter fullscreen mode Exit fullscreen mode

Let's also define the functions we are using to interact with the database in this case is
findBy() and update()

// I'm using knex to interact with the database here
// but all is doing is looking into 'users' table and filtering the data by the filter parameter
function filterBy(filter) {
  return db('users').where(filter);
}

// same here, looking into 'users' table by 'id' and then updating the values
function update(id, changes) {
  return db('users').where({ id }).update(changes);
}
Enter fullscreen mode Exit fullscreen mode

Let's define sendEmail function

function sendEmail(user, token) {
  sgMail.setApiKey(sendGridKey);
  const msg = {
    to: user.email,
    from: "", // your email
    subject: "Reset password requested",
    html: `
     <a href="${clientURL}/reset-password/${token}">${token}</a>
   `
   // I'm only going to use an (a tag) to make this easier to
   // understand but feel free to add any email templates 
   // in the `html` property
  };

  sgMail.send(msg)
    .then(() => {
      console.log("Email sent");
  }).catch((error) => {
      console.error(error);
  })
}
Enter fullscreen mode Exit fullscreen mode

Now that we've created the routes I'm going to use Postman to test it, we need to make a PATCH request
Alt Text

You should get a 200 status if everything is okay, check your email and copy the token from the email
Alt Text

On Postman paste the token url/auth/reset-password/:token
see example below

Alt Text

You should get a 200 status code with a Password update message

Here is a link to one of my repos with full example code repo

I hope you find this helpful! If you have any questions let me know in the comments.
I'm planning to add the same functionality but using Redis to store the temporary token instead of the database.

Discussion (0)

pic
Editor guide