DEV Community

Cover image for Forgot password & Password reset flow in node.js
CyberWolves
CyberWolves

Posted on • Edited on

Forgot password & Password reset flow in node.js

Hi guys today we gonna implement password reset via email in node.js. If you user forgot there password, we send an link to you user email account. From that link user can add there new password. If you just wanna know how this concept works, you can start from Model section .

So let's start coding...

Demo Video

Project Github Link

App Overview :
Project Structure
project_structure
Following table shows the overview of Rest APIs that exported

Methods Urls Actions
POST /users create user
POST /password-reset Send password reset link
POST /password-reset/:userId/:token Reset user password

create Node.js App

$ mkdir node-email-password-reset
$ cd node-email-password-reset
$ npm init --yes
$ npm install express mongoose dotenv nodemailer joi
Enter fullscreen mode Exit fullscreen mode

Express : Express is minimal and flexible Node.js web applicaton framework.
Mongoose : Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js.
Nodemailer : Nodemailer allow us to send email.
Joi : Joi is an object schema description language and validator for javascript objects.
Dotenv : It loads environment variables from a .env file.

package.json

{
  "name": "node-email-password-reset",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^9.0.2",
    "express": "^4.17.1",
    "joi": "^17.4.0",
    "mongoose": "^5.12.10",
    "nodemailer": "^6.6.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Setup Express Web Server
In the root folder, let's create index.js file :

require("dotenv").config();
const express = require("express");
const app = express();

app.use(express.json());

const port = process.env.PORT || 8080;
app.listen(port, () => console.log(`Listening on port ${port}...`));
Enter fullscreen mode Exit fullscreen mode

Configure Environment Variables
In the root folder, let's create .env file :

DB = // mongodb url
HOST = // email host
USER = // email id
PASS = // email password
SERVICE = // email service
BASE_URL = "http://localhost:8080/api"
Enter fullscreen mode Exit fullscreen mode

Configure MongoDB Database
In the root folder, let's create db.js file :

const mongoose = require("mongoose");

module.exports = connection = async () => {
    try {
        const connectionParams = {
            useNewUrlParser: true,
            useCreateIndex: true,
            useUnifiedTopology: true,
        };
        await mongoose.connect(process.env.DB, connectionParams);
        console.log("connected to database.");
    } catch (error) {
        console.log(error, "could not connect database.");
    }
};
Enter fullscreen mode Exit fullscreen mode

import db.js in index.js and call it :

//....
const connection = require("./db");
const express = require("express");
const app = express();

connection();

app.use(express.json());
//....
Enter fullscreen mode Exit fullscreen mode

Define The Models
In the root directory create models folder.
models/user.js like this :

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const Joi = require("joi");

const userSchema = new Schema({
    name: {
        type: String,
        required: true,
    },
    email: {
        type: String,
        required: true,
    },
    password: {
        type: String,
        required: true,
    },
});

const User = mongoose.model("user", userSchema);

const validate = (user) => {
    const schema = Joi.object({
        name: Joi.string().required(),
        email: Joi.string().email().required(),
        password: Joi.string().required(),
    });
    return schema.validate(user);
};

module.exports = { User, validate };
Enter fullscreen mode Exit fullscreen mode

models/token.js file like this :

const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const tokenSchema = new Schema({
    userId: {
        type: Schema.Types.ObjectId,
        required: true,
        ref: "user",
    },
    token: {
        type: String,
        required: true,
    },
    createdAt: {
        type: Date,
        default: Date.now,
        expires: 3600,
    },
});

module.exports = mongoose.model("token", tokenSchema);
Enter fullscreen mode Exit fullscreen mode

Configure The Email Transporter
In the root directory create utils folder.
utils/sendEmail.js file like this :

const nodemailer = require("nodemailer");

const sendEmail = async (email, subject, text) => {
    try {
        const transporter = nodemailer.createTransport({
            host: process.env.HOST,
            service: process.env.SERVICE,
            port: 587,
            secure: true,
            auth: {
                user: process.env.USER,
                pass: process.env.PASS,
            },
        });

        await transporter.sendMail({
            from: process.env.USER,
            to: email,
            subject: subject,
            text: text,
        });

        console.log("email sent sucessfully");
    } catch (error) {
        console.log(error, "email not sent");
    }
};

module.exports = sendEmail;
Enter fullscreen mode Exit fullscreen mode

Define The Routes
In the root directory create routes folder.
routes/users.js file like this :

const { User, validate } = require("../models/user");
const express = require("express");
const router = express.Router();

router.post("/", async (req, res) => {
    try {
        const { error } = validate(req.body);
        if (error) return res.status(400).send(error.details[0].message);

        const user = await new User(req.body).save();

        res.send(user);
    } catch (error) {
        res.send("An error occured");
        console.log(error);
    }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

routes/passwordReset.js file like this :

const { User } = require("../models/user");
const Token = require("../models/token");
const sendEmail = require("../utils/sendEmail");
const crypto = require("crypto");
const Joi = require("joi");
const express = require("express");
const router = express.Router();

router.post("/", async (req, res) => {
    try {
        const schema = Joi.object({ email: Joi.string().email().required() });
        const { error } = schema.validate(req.body);
        if (error) return res.status(400).send(error.details[0].message);

        const user = await User.findOne({ email: req.body.email });
        if (!user)
            return res.status(400).send("user with given email doesn't exist");

        let token = await Token.findOne({ userId: user._id });
        if (!token) {
            token = await new Token({
                userId: user._id,
                token: crypto.randomBytes(32).toString("hex"),
            }).save();
        }

        const link = `${process.env.BASE_URL}/password-reset/${user._id}/${token.token}`;
        await sendEmail(user.email, "Password reset", link);

        res.send("password reset link sent to your email account");
    } catch (error) {
        res.send("An error occured");
        console.log(error);
    }
});

router.post("/:userId/:token", async (req, res) => {
    try {
        const schema = Joi.object({ password: Joi.string().required() });
        const { error } = schema.validate(req.body);
        if (error) return res.status(400).send(error.details[0].message);

        const user = await User.findById(req.params.userId);
        if (!user) return res.status(400).send("invalid link or expired");

        const token = await Token.findOne({
            userId: user._id,
            token: req.params.token,
        });
        if (!token) return res.status(400).send("Invalid link or expired");

        user.password = req.body.password;
        await user.save();
        await token.delete();

        res.send("password reset sucessfully.");
    } catch (error) {
        res.send("An error occured");
        console.log(error);
    }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

If you are using this routes in front-end, you might need another route in passwordReset.js. We need to show password reset form only if the link is valid. So, we need a GET route which will validate the link and show password reset form.

Import routes in index.js

//...
const passwordReset = require("./routes/passwordReset");
const users = require("./routes/users");
const connection = require("./db");
//.....

app.use(express.json());

app.use("/api/users", users);
app.use("/api/password-reset", passwordReset);
//....
Enter fullscreen mode Exit fullscreen mode

That's it, test the APIs in postman, If you found any mistakes or making code better let me know in comment. For better understanding please watch Youtube video. Subscribe to my Youtube channel to get more knowledgeable content every week.

Arigato gozaimasu.. ๐Ÿ™‚

Top comments (22)

Collapse
 
mwafrika profile image
Mwafrika Josuรฉ

Thank you for this amazing tutorial, After watching it carefully I noticed a small bug that you can work on later. After reseting the password, The plain password is being kept in the database. I think you should concider encrypting the password before saving to the database after reseting it

Collapse
 
subhashoos profile image
subhash-oos

no you are wrong thats not a bug please try to see this schema we have no need to encrypt
const express =require("express")
const mongoose=require("mongoose")
const bcrypt = require('bcryptjs');
const schema=new mongoose.Schema({
firstName: {
type: String,
// required: true,
},
lastName: {
type: String,
// required: true,
},
image: {
type: String,
},
email:{
type:String,
required: true,
},
status: {
type: String,
enum: ['Pending', 'Active'],
default: 'Pending'
},
confirmationCode: {
type: String,
unique: true
},
password:{
type:String,
required: true,
},
phone:{
type:String,
// required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
phoneOtp:String
},
{ timestamps: true }
)
schema.pre('save', async function(next) {
try {
// check method of registration
const user = this;
if (!user.isModified('password')) next();
// generate salt
const salt = await bcrypt.genSalt(10);
// hash the password
const hashedPassword = await bcrypt.hash(this.password, salt);
// replace plain text password with hashed password
this.password = hashedPassword;
next();
} catch (error) {
return next(error);
}
});
schema.methods.matchPassword = async function (password) {
console.log(password)
try {
return await bcrypt.compare(password, this.password);
} catch (error) {
throw new Error(error);
}
};
const mens=new mongoose.model("APJ_Ecomusers",schema)
module.exports=mens

Collapse
 
stylespriley profile image
StylesPRiley

Getting this error message:
Error: connect ECONNREFUSED 127.0.0.1:587
at TCPConnectWrap.afterConnect as oncomplete {
errno: -4078,
code: 'ESOCKET',
syscall: 'connect',
address: '127.0.0.1',
port: 587,
command: 'CONN'
} email not sent

Collapse
 
tarun080 profile image
Tarun Purohit

How to solve this error.

Collapse
 
tarun080 profile image
Tarun Purohit

@cyberwolves can u show the solution for this error

Collapse
 
tarun080 profile image
Tarun Purohit

@stylespriley if u got the solution please share. Facing the same issue here

Collapse
 
mdirshaddev profile image
Md Irshad

Awesome Article I am new to this Web Development. Just by reading this i was able to understand the flow. Thank You. Keep writing such articles.

Collapse
 
cyberwolves profile image
CyberWolves

Thank you, subscribe to my Youtube channel youtube.com/channel/UCxyo2h1uAuMT1...

Collapse
 
adesoji1 profile image
Adesoji1

Thanks, i did something similar to this using react,postgres and express.js

Collapse
 
himanshupal0001 profile image
Himanshupal0001

Can you provide the music name that you used in your video.

Collapse
 
s0fy4n002 profile image
yayan

nice share gan

iam from indonesia

Collapse
 
mohit1607 profile image
mohit1607

Arigato gozaimasu.. senpai

Collapse
 
kyzsu profile image
kyzsu

thanks mate, works fine!

Collapse
 
chadrackkyungu profile image
Chadrack kyungu

Thank you so much your blogs are amazing

Collapse
 
zany49 profile image
abdul kalam

UnhandledPromiseRejectionWarning: CastError: Cast to ObjectId failed for value "undefined" (type string) at path "_id" for model "User"
please help me