Verification Email Useing Nodejs + Express + Postgress

Verfication Email Useing Nodejs + Express + Postgress

How to verify user email address in node.js ?

node.js, Express.js, Postgress

Every website should include an email verification feature. It will protect us against spammers. Because this is my first blog, I shall do my best. Let’s get started coding.

Source Code For TypeScripte

Source Code Github

Create Node.js Folders App

firstly strat create express server, use this command For that:

Create Node.js App

    $ mkdir verification-email
    $ cd verification-email

Next, we initialize the Node.js App with a package.json file:

  $ npm init -y

We need to install necessary modules.

  $ npm install express pg pg-hstore sequelize nodemailer joi env2 compression bcryptjs cors jsonwebtoken
  $ npm install nodemon -d

Express : Express is minimal and flexible Node.js web applicaton framework. postgress : postgress is an database. sequelize : sequelize is an database ORM. Nodemailer : Nodemailer allow us to send email. Joi : Joi is an object schema description language and validator for javascript objects. env2 : It loads environment variables from a .env file.

package json

The package.json file should look like this :

{
  "name": "verification-email",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon server"
  },
  "keywords": [
    "nodejs",
    "express",
    "email",
    "verification",
    "email-verification"
  ],
  "author": "Ahmed Qeshta",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "compression": "^1.7.4",
    "cors": "^2.8.5",
    "env2": "^2.2.2",
    "express": "^4.18.1",
    "joi": "^17.6.0",
    "jsonwebtoken": "^8.5.1",
    "nodemailer": "^6.7.5",
    "pg": "^8.7.3",
    "pg-hstore": "^2.3.4",
    "sequelize": "^6.20.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.16"
  }
}

Project Structure

Project Structure - part 1 Project Structure - part 2

Setup Express Web Server

In root Folder

server\index.js

const app = require('./app');

const port = app.get('port');

app.listen(port, () => {
  console.log(`server is running on http://localhost:${port}`);
});

server\app.js

require('env2')('.env');
const express = require('express');
const compression = require('compression');
const cors = require('cors');
const cookieParser = require('cookie-parser');

const { notFundError, serverError } = require('./error');

const routes = require('./routes');

const app = express();

app.disable('x-powered-by');

app.use([
  compression(),
  cors(),
  cookieParser(),
  express.json({ limit: '50mb' }),
  express.urlencoded({ extended: false }),
]);

app.set('port', process.env.PORT || 8080);

app.use('/api/v1/', routes);

app.use(notFundError);
app.use(serverError);

module.exports = app;

In Routes Folder

server\routes\index.js

const { Router } = require('express');
const auth = require('./auth');

const routes = Router();

// {domain-name}/api/v1/user
routes.use('/user', auth);

module.exports = routes;

server\routes\auth.js

const { Router } = require('express');
const { addUser, verifyUser, getUsers } = require('../controllers');

const user = Router();

// {domain-name}/api/v1/user
user.post('/', addUser);
user.get('/', getUsers);

// {domain-name}/api/v1/user/verify/:id/:token
user.get('/verify/:id/:token', verifyUser);

module.exports = user;

In Error Folder

server\error\index.js

const notFundError = require('./notFoundError');
const serverError = require('./serverError');

module.exports = { notFundError, serverError };

server\error\notFoundError.js

const notFoundError = (_, res) => {
  res.status(404).json({ status: 404, message: 'Not Found Page' });
};

module.exports = notFoundError;

server\error\serverError.js

const serverError = (error, _, res, next) => {
  if (error.status) {
    res.status(error.status).json({ status: error.status, message: error.message });
  } else {
    res.status(500).json({ status: 500, message: 'Server Error' });
  }
};

module.exports = serverError;

In Controllers Folder

server\controllers\index.js

const { addUser, verifyUser, getUsers } = require('./auth');

module.exports = { addUser, verifyUser, getUsers };

server\controllers\auth\index.js

const addUser = require('./addUser');
const getUsers = require('./getUsers');
const verifyUser = require('./verifyUser');

module.exports = { addUser, verifyUser, getUsers };

server\controllers\auth\addUser.js

const { CustomError, addUserSchema, generateToken } = require('../../utils');
const { User } = require('../../database');
const { hash } = require('bcryptjs');

const { verifyEmail } = require('../../utils/email/templates/verifyEmail');
const sendEmail = require('../../utils/email');

const addUser = async (req, res, next) => {
  try {
    const { email, password } = await addUserSchema.validateAsync(req.body, { abortEarly: false });

    // Check if the gym already exists
    const isExist = await User.findOne({
      where: { email },
    });
    // if is exist throw an error
    if (isExist) {
      throw CustomError('Sorry, This Email is already exist', 409);
    }

    const hashedPassword = await hash(password, 12);

    const { id } = await User.create({ email, password: hashedPassword });

    const payload = {
      id,
      email,
    };

    // Generate the token

    const token = await generateToken(payload, {
      expiresIn: '0.5h',
      algorithm: 'HS256',
    });

    const html = verifyEmail(`${process.env.BASE_URL}api/v1/user/verify/${id}/${token}`);
    sendEmail(email, 'Verify Your Email', html);

    res.status(201).json({
      message: 'An Email sent to your account please verify',
    });
  } catch (error) {
    console.log(error);
    if (error.name === 'ValidationError') {
      return next(CustomError(error.message, 400));
    }
    return next(error);
  }
};

module.exports = addUser;

server\controllers\auth\verifyUser.js

const { User } = require('../../database');
const { checkToken, CustomError, paramsValidation } = require('../../utils');

const verifyUser = async ({ params }, res, next) => {
  try {
    const { id, token } = await paramsValidation.validateAsync(params);

    // Check if the gym already exists
    const user = await User.findByPk(id);
    // if is exist throw an error
    if (!user) throw CustomError('Sorry, Invalid link', 409);

    const tokenChecked = await checkToken(token);

    if (!tokenChecked) throw CustomError('Sorry, Invalid link', 409);

    await user.update({ verified: true });

    res.json({
      message: 'email verified successfully',
    });
  } catch (error) {
    if (error.name === 'ValidationError') {
      return next(CustomError(error.message, 400));
    }
    if (error.name === 'TokenExpiredError') {
      return next(CustomError('Sorry This link was Invalid, try Again', 500));
    }
    return next(error);
  }
};

module.exports = verifyUser;

server\controllers\auth\getUsers.js

const { User } = require('../../database');

const getUsers = async (_, res, next) => {
  try {
    const user = await User.findAll();
    res.json({
      message: 'Get All User',
      user,
    });
  } catch (error) {
    return next(error);
  }
};

module.exports = getUsers;

Routes

Methods Route Urls Actions
GET /api/v1/user Get All User In DataBase
POST /api/v1/user Create new User In DataBase
GET /api/v1/user/verify/:id/:token verify Email link, was sent by email

Setup Database

First thing Create DataBase as verify_email_db, open your terminal or open sql shell

$ Psql -h localhost -p 5432 -U postgres

Enter Yor Password for user postgres:

CREATE DATABASE verify_email_db;

.env File

BASE_URL = http://localhost:{your port}/
NODE_ENV = development
DB_URL = postgres://postgres:[email protected]:5432/db_name
TEST_DB_URL = postgres://postgres:[email protected]:5432/db_name_test
DATABASE_URL = your-prodction-DATABASE_URL
JWT_SECRET = your-jwt_SECRET

MAIL_HOST = smtp.mailtrap.io
MAIL_USER = user_mail
MAIL_PASS = passwor_mail

In DataBase Folder

server\database\config\connection.js

const { Sequelize } = require('sequelize');

require('env2')('.env');

const { NODE_ENV, DB_URL, TEST_DB_URL, DATABASE_URL, DB_BUILD } = process.env;

let dbUrl = '';
let ssl = false;

switch (NODE_ENV) {
  case 'development':
    dbUrl = DB_URL;
    ssl = false;
    break;
  case 'test':
    dbUrl = TEST_DB_URL;
    ssl = false;
    break;
  case 'production':
    dbUrl = DATABASE_URL;
    ssl = { rejectUnauthorized: false };
    break;
  default:
    throw new Error('NODE_ENV is not set');
}

const sequelize = new Sequelize(dbUrl, {
  dialect: 'postgres',
  dialectOptions: { ssl, charset: 'utf8' },
  logging: false,
});

if (!DB_BUILD) {
  // sync sequelize when DB_BUILD equals false
  sequelize.sync();
}

module.exports = sequelize;

server\database\models\index.js

const User = require('./users');

module.exports = { User };

server\database\models\user.js

const { DataTypes } = require('sequelize');

const sequelize = require('../config/connection');

const User = sequelize.define('users', {
  id: {
    type: DataTypes.INTEGER,
    autoIncrement: true,
    primaryKey: true,
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  verified: {
    type: DataTypes.BOOLEAN,
    defaultValue: false,
  },
});

module.exports = User;

server\database\index.js

const sequelize = require('./config/connection');
const { User } = require('./models');

module.exports = {
  User,
  sequelize,
};

Setup The Email Transporter

In the utils folder, create email folder then create configration and create templete email.

server\utils\email\index.js

const nodemailer = require('nodemailer');

const { EMAIL_SENDER, MAIL_HOST, MAIL_USER, MAIL_PASS } = process.env;

const sendEmail = async (to, subject, html) => {
  try {
    const transport = nodemailer.createTransport({
      host: MAIL_HOST,
      port: 2525,
      // secure: true,
      auth: {
        user: MAIL_USER,
        pass: MAIL_PASS,
      },
    });

    await transport.sendMail({
      from: EMAIL_SENDER,
      to,
      subject,
      html,
    });
  } catch (error) {
    console.log(error);
  }
};

module.exports = sendEmail;

server\utils\email\templates\verifyEmail.js

const styles = `  <style type="text/css">
@media only screen and (min-width: 520px) {
.u-row {
width: 500px !important;
}
.u-row .u-col {
vertical-align: top;
}

.u-row .u-col-100 {
width: 500px !important;
}

}

@media (max-width: 520px) {
.u-row-container {
max-width: 100% !important;
padding-left: 0px !important;
padding-right: 0px !important;
}
.u-row .u-col {
min-width: 320px !important;
max-width: 100% !important;
display: block !important;
}
.u-row {
width: calc(100% - 40px) !important;
}
.u-col {
width: 100% !important;
}
.u-col > div {
margin: 0 auto;
}
}
body {
margin: 0;
padding: 0;
}

table,
tr,
td {
vertical-align: top;
border-collapse: collapse;
}

p {
margin: 0;
}

.ie-container table,
.mso-container table {
table-layout: fixed;
}

* {
line-height: inherit;
}

a[x-apple-data-detectors='true'] {
color: inherit !important;
text-decoration: none !important;
}

table, td { color: #000000; } a { color: #0000ee; text-decoration: underline; }
</style>
`;
const verifyEmail = (link) => {
  return `
  <!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
  <head>
  <!--[if gte mso 9]>
  <xml>
    <o:OfficeDocumentSettings>
      <o:AllowPNG/>
      <o:PixelsPerInch>96</o:PixelsPerInch>
    </o:OfficeDocumentSettings>
  </xml>
  <![endif]-->
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="x-apple-disable-message-reformatting">
    <!--[if !mso]><!--><meta http-equiv="X-UA-Compatible" content="IE=edge"><!--<![endif]-->
    <title></title>
    
    ${styles}
    
  
  </head>
  
  <body class="clean-body u_body" style="margin: 0;padding: 0;-webkit-text-size-adjust: 100%;background-color: #e7e7e7;color: #000000">
    <!--[if IE]><div class="ie-container"><![endif]-->
    <!--[if mso]><div class="mso-container"><![endif]-->
    <table style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;min-width: 320px;Margin: 0 auto;background-color: #e7e7e7;width:100%" cellpadding="0" cellspacing="0">
    <tbody>
    <tr style="vertical-align: top">
      <td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
      <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td align="center" style="background-color: #e7e7e7;"><![endif]-->
      
  
  <div class="u-row-container" style="padding: 0px;background-color: transparent">
    <div class="u-row" style="Margin: 0 auto;min-width: 320px;max-width: 500px;overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;background-color: transparent;">
      <div style="border-collapse: collapse;display: table;width: 100%;background-color: transparent;">
        <!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="padding: 0px;background-color: transparent;" align="center"><table cellpadding="0" cellspacing="0" border="0" style="width:500px;"><tr style="background-color: transparent;"><![endif]-->
        
  <!--[if (mso)|(IE)]><td align="center" width="500" style="width: 500px;padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;" valign="top"><![endif]-->
  <div class="u-col u-col-100" style="max-width: 320px;min-width: 500px;display: table-cell;vertical-align: top;">
    <div style="width: 100% !important;">
    <!--[if (!mso)&(!IE)]><!--><div style="padding: 0px;border-top: 0px solid transparent;border-left: 0px solid transparent;border-right: 0px solid transparent;border-bottom: 0px solid transparent;"><!--<![endif]-->
    
  <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
    <tbody>
      <tr>
        <td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
          
    <h1 style="margin: 0px; line-height: 140%; text-align: center; word-wrap: break-word; font-weight: normal; font-family: comic sans ms,sans-serif; font-size: 23px;">
      😀 We are Happy to see you 😀
    </h1>
  
        </td>
      </tr>
    </tbody>
  </table>
  
  <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
    <tbody>
      <tr>
        <td style="overflow-wrap:break-word;word-break:break-word;padding:2px;font-family:arial,helvetica,sans-serif;" align="left">
          
  <table width="100%" cellpadding="0" cellspacing="0" border="0">
    <tr>
      <td style="padding-right: 0px;padding-left: 0px;" align="center">
        
        <img align="center" border="0" src="https://user-images.githubusercontent.com/38624002/167153626-c10c301a-fd95-4aee-b7ed-993d44f2004f.jpeg" alt="" title="" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: inline-block !important;border: none;height: auto;float: none;width: 100%;max-width: 266px;" width="266"/>
        
      </td>
    </tr>
  </table>
  
        </td>
      </tr>
    </tbody>
  </table>
  
  <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
    <tbody>
      <tr>
        <td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
          
    <table height="0px" align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;border-top: 1px solid #BBBBBB;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
      <tbody>
        <tr style="vertical-align: top">
          <td style="word-break: break-word;border-collapse: collapse !important;vertical-align: top;font-size: 0px;line-height: 0px;mso-line-height-rule: exactly;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%">
            <span> </span>
          </td>
        </tr>
      </tbody>
    </table>
  
        </td>
      </tr>
    </tbody>
  </table>
  
  <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
    <tbody>
      <tr>
        <td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
          
  <div align="center">
    <div style="display: table;">
      <table align="left" border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 5px">
        <tbody><tr style="vertical-align: top"><td align="left" valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
          <a href="https://www.facebook.com/A7medQeshta/" title="Facebook" target="_blank">
            <img src="https://user-images.githubusercontent.com/38624002/167153619-24dd5572-276b-4ea5-a9ba-ab004372bf56.png" alt="Facebook" title="Facebook" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important">
          </a>
        </td></tr>
      </tbody></table>
    
    
      <table align="left" border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 5px">
        <tbody><tr style="vertical-align: top"><td align="left" valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
          <a href="https://twitter.com/ahmedqeshta0" title="Twitter" target="_blank">
            <img src="https://user-images.githubusercontent.com/38624002/167153615-a17c1671-b9e0-4efa-8f25-d2b35e2729c4.png" alt="Twitter" title="Twitter" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important">
          </a>
        </td></tr>
      </tbody></table>

      <table align="left" border="0" cellspacing="0" cellpadding="0" width="32" height="32" style="border-collapse: collapse;table-layout: fixed;border-spacing: 0;mso-table-lspace: 0pt;mso-table-rspace: 0pt;vertical-align: top;margin-right: 5px">
        <tbody><tr style="vertical-align: top"><td align="left" valign="middle" style="word-break: break-word;border-collapse: collapse !important;vertical-align: top">
          <a href="https://github.com/AhmedQeshta" title="Twitter" target="_blank">
            <img src="https://user-images.githubusercontent.com/38624002/167153630-cae46bcd-7db2-434f-a625-e345e6b3df58.png" alt="GitHub" title="GitHub" width="32" style="outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;clear: both;display: block !important;border: none;height: auto;float: none;max-width: 32px !important">
          </a>
        </td></tr>
      </tbody></table>
    </div>
    
      
  
  <table style="font-family:arial,helvetica,sans-serif;" role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
    <tbody>
      <tr>
        <td style="overflow-wrap:break-word;word-break:break-word;padding:10px;font-family:arial,helvetica,sans-serif;" align="left">
          
    <div style="line-height: 140%; text-align: center; word-wrap: break-word;">
      <a href="${link}">
           <button type="button" style="background: #34bbbc;
                    color: #ffffff;
                    padding: 1rem;
                    font-size: 14px;
                    line-height: 140%;
                    border: none;
                    cursor: pointer;
                    border-radius: 8px;">Verify Your Email</button>
                  </a>
                </div>
  
                </td>
              </tr>
            </tbody>
          </table>
          
        </div>
        </div>

      </div>
    </div>
  </div>
      </td>
    </tr>
    </tbody>
    </table>
  </body>
  
  </html>
  `;
};

module.exports = {
  verifyEmail,
};

In Folder utils

server\utils\jwt.js

const { sign, verify } = require('jsonwebtoken');

const { JWT_SECRET } = process.env;

const OPTIONS = {
  expiresIn: '30d',
  algorithm: 'HS256',
};

module.exports = {
  generateToken: (object, options = OPTIONS) =>
    new Promise((resolve, reject) => {
      sign(object, JWT_SECRET, options, (error, payload) => {
        if (error) return reject(error);
        return resolve(payload);
      });
    }),
  checkToken: (token) =>
    new Promise((resolve, reject) => {
      verify(token, JWT_SECRET, (error, payload) => {
        if (error) return reject(error);
        return resolve(payload);
      });
    }),
};

server\utils\CustomError.js

module.exports = {
  CustomError: (message, status, massages) => {
    const error = new Error(message);
    error.status = status;
    error.massages = massages;
    return error;
  },
};

server\utils\validation\index.js

const addUserSchema = require('./addUserSchema');
const paramsValidation = require('./paramsValidation');

module.exports = {
  addUserSchema,
  paramsValidation,
};

server\utils\validation\paramsValidation.js

const Joi = require('joi');

const paramsValidation = Joi.object({
  id: Joi.number().integer().positive().required(),
  token: Joi.string().required(),
});

module.exports = paramsValidation;

server\utils\validation\addUserSchema.js

const Joi = require('joi');

const addUserSchema = Joi.object({
  email: Joi.string()
    .email({
      minDomainSegments: 2,
      tlds: { allow: ['com', 'net'] },
    })
    .required(),
  password: Joi.string()
    .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[[email protected]#\$%\^&\*])(?=.{6,})/)
    .min(6)
    .required(),
});

module.exports = addUserSchema;

server\utils\index.js

const { CustomError } = require('./CustomError');
const { addUserSchema, paramsValidation } = require('./validation');
const { generateToken, checkToken } = require('./jwt');

module.exports = {
  CustomError,
  addUserSchema,
  generateToken,
  checkToken,
  paramsValidation,
};

Result

After Create new user then will sent email, Go to Mailtrap to test it,

Conclusion

in conclusion, as a user when registering in any way, must by default set Column ‘verified’ false, then after creating it in the database, the app will send an email with has verification link, this link contains the id of this user and token to check if he or not.

when clicked above this link will change the status of the user from false to true, to be verified

Source Code Github – Javascript

Source Code Github – TypeScripte

Powered By : Ahmed Qeshta

GitHub

View Github