Registration System With Otp

» Project Overview

  1. For project overview please, watch the demo section on youtube video provided above.

» Starting With Project Initialization

  1. Open the terminal and in here, run the command,
pnpm create vite
  1. Name the project, whatever you’d like to call.

  2. Then after that select javascript as our language and the navigate to the project and open it into the vs code.

  3. And in VS code, we run the command, to intsall of the dependencies vite application needs. 

pnpm intsall
  1. Then we also install obe more dependencides that we will be needing, and that is axios. So let’s install them by running the command:
pnpm i axios

» Create backend

  1. Then, the next step is to create the backend or our server side of the application, create a folder named server into the root directory and inside this folder let’s initialize this as a nodejs backend, naviagte to the server folder and the run the command:
npm init -y 
  1. Let’s first add a start script in package.json file, to start the server
"start": "nodemon index.js",
  1. Then in the terminal run the command, to install all of these dependencies that we need for this application.
npm install cors dotenv express mongoose nodemailer nodemon require 
  1. After that’s done, let’s now create all of the files and folders that we need into this server folder, so that it can be easy to work on with the project. 

folder structure

  1. First create a src folder and inside it create another three folders name models, routes and utils. (or refer to GitHub Repo link)
- react-nodejs-otp(main project folder)
 - server
 - node_modules
 - src
    - models
        auth-controller.js
        auth-routes.js
        auth-service.js
        user.js
    - routes
        routes.js
    - utils
        nodemailer.js
        responses.js
        otp-generator.js
  .env
  index.js
  package-lock.json
  package.json
  1. To make things easy, I’ll provide the code for every file first in detail and then explain them.

  2. In the .env file we first create several variables:

APP_PORT=4000
BASE_URL=http://localhost:4000

DB_HOST=localhost
DB_NAME=db-name
DB_USER=db-username
DB_PASSWORD=db-password
DB_PORT=5432
DB_DIALECT=mongodb


MONGO_URI=your-mongo-uri


MAIL_USER=your-gmail-account
MAIL_PASSWORD=your-gmail-password
MAIL_PORT=465
MAIL_HOST=smtp.gmail.com
  1. Next, in the response.js file we will create a function named response that will help us send HTTP responses from the server with a specific structure.
// response.js file
const response = (res, status, result = '') => {
    let desc = ''

    switch (status) {
        case 200:
            desc = 'Ok'
            break
        case 201:
            desc = 'Created'
            break
        case 400:
            desc = 'Bad Request'
            break
        case 401:
            desc = 'Unauthorized'
            break
        case 404:
            desc = 'Not Found'
            break
        case 500:
            desc = 'Internal Server Error'
            break
        default:
            desc = ''
    }

    const isObject = (data) => {
        return !!data && data.constructor === Object
    }

    const results = {
        status: status,
        description: desc,
        result: isObject(result) ? [result] : result
    }

    res.status(status).json(results)
}

module.exports = response
  1. index.js is the entry point. This file essentially sets up our server and database connection, and starts the server once the database connection is made.
// index.js
require('dotenv').config()
const express = require('express')
const cors = require('cors')
const server = express()
const response = require('./src/utils/responses')
const port = process.env.APP_PORT
const mongoose = require('mongoose')
const router = require('./src/routes/routes')

server.use(cors())
server.use(express.json())
server.use(express.urlencoded({ extended: true }))
server.use(router)

server.all('*', (req, res, next) => {
    response(res, 404, 'Page not found.')
})

mongoose.connect(process.env.MONGO_URI)
    .then(() => {
        server.listen(port, () => {
            console.log('DB connected')
            console.log(`Server is running on ${process.env.BASE_URL}\n`)
        })
    }).catch((error) => {
        console.log(error.message)
    })

» Database connection.

  1. Head over to MongoDB Website.

  2. Sign in and follow the steps:

  3. Create a project and name it: Create a project

Name a Project

  1. Choose a plan

Name a Project

  1. Create a deployment Name a Project

  2. Get the connection String

Name a Project

  1. Connect database in VS code with connection string, paste the string and hit enter

Name a Project

  1. Now we will start off by defining a MongoDB schema for a user and creates a corresponding model using Mongoose.

  2. So in the models folder we have a file named user.js open that up and paste the code:

// user.js
const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
    username: {
        type: String,
        required: true
    },
    email: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true
    },
    otp_code: {
        type: String,
        required: false
    },
    otp_expiration: {
        type: Date,
        required: false
    },
    is_verified: {
        type: Boolean,
        default: false
    },
});

const Users = mongoose.model('Users', UserSchema);

module.exports = Users;
  1. This schema defines the structure of our user details in MongoDB. In this we will have fields such as ‘username’, ‘email’, ‘password’, ‘otp_code’, ‘otp_expiration’, and ‘is_verified’." .

» Create API endpoints

  1. Perfect now let’s create our Registration, Verification, and Resend function in auth-service file.
// auth-service.js
const User = require("../models/user");

const Register = async ({ username, email, password }) => {
  try {
    const newUser = await User.create({
      username,
      email,
      password,
    });

    return newUser;
  } catch (error) {
    throw error;
  }
};


const ResendOTP = async ({ email, otp_code, otp_expiration }) => {
    try {
        const user = await User.findOneAndUpdate(
            { email: email },
            { otp_code: otp_code, otp_expiration: otp_expiration },
            { new: true }
        );

        if (!user) return null

        return user
    } catch (error) {
        throw error
    }
}

const EmailExists = async ({ email }) => {
  try {
    const data = await User.find({
      email: email,
    });

    return data;
  } catch (error) {
    throw error;
  }
};

const VerifyEmail = async ({email, otp_code})=>{
    try {
        const user = await User.findOne({
                email: email,
                otp_code: otp_code
        })

        if (!user) return null

        const OTPExpired = new Date() > new Date(user.otp_expiration)
        if (OTPExpired) throw new Error('OTP is expired')

        const alreadyVerified = user.is_verified === true
        if (alreadyVerified) throw new Error('Email has been verified')

        await User.updateOne(
            { email: email },
            { is_verified: true }
        )

        const updatedData = await User.findOne({
            email: email
        })

        return updatedData
    } catch (error) {
        throw error
    }
}

module.exports = { Register, ResendOTP, EmailExists, VerifyEmail};

  1. Now we will Create a Registration, Verification, and Resend function in auth-controller.js file so that it can handle requests and responses.
// auth-controller.js
const service = require('./auth-service')
const response = require('../utils/responses')
const User = require('../models/user');
const generateOTP = require('../utils/otp-generator')
const { sendEmail } = require('../utils/nodemailer')

const Register = async (req, res) => {
    try {
        const { username, email, password } = req.body
        const otp = generateOTP()

        if (!username) {
            return response(res, 400, { message: `Username can't be empty` })
        } else if (!email || !password) {
            return response(res, 400, { message: `Email or password can't be empty` })
        }

        const queries = {
            username: username,
            email: email,
            password: password,
            otp_code: otp,
            otp_expiration: new Date(Date.now() + 10 * 60 * 1000) // OTP expires after 10 minutes
        };

        const newUser = new User(queries);

        const savedUser = await newUser.save();

        if (!savedUser) {
            return response(res, 500, { message: 'There was a problem registering the user.' });
        }

        // Send the OTP to the user's email
        sendEmail(email, 'Your OTP', `Your OTP is: ${otp}`);

        return response(res, 200, { message: 'User registered successfully', user: savedUser });
    } catch (error) {
        return response(res, 500, { message: error.message });
    }
}

const ResendOTP = async (req, res) => {
    const { email } = req.body;
    const otp_code = generateOTP();
    const otp_expiration = new Date(Date.now() + 10 * 60 * 1000);

    console.log(`Email: ${email}`); // Log the email

    try {
        const user = await User.findOneAndUpdate(
            { email: email },
            { otp_code: otp_code, otp_expiration: otp_expiration },
            { new: true }
        );

        console.log(`User: ${JSON.stringify(user)}`); // Log the user

        if (!user) { 
            return res.status(404).json({ message: 'User not found' });
        }

        // Send the OTP to the user's email
        sendEmail(email, 'Resent OTP', `Resent OTP is: ${otp_code}`);

        return res.status(200).json({ message: 'OTP resent', user });
    } catch (error) {
        return res.status(500).json({ message: error.message });
    }
}

const VerifyEmail = async (req, res)=>{
    try {
        console.log(req.body)
        const { email, otp_code } = req.body
        const emailExists = await service.EmailExists({ email })

        console.log('emailExists:', emailExists)

        if (emailExists.length <= 0) return response(res, 404, { message: 'User not found' })

        try {
            const user = await service.VerifyEmail({ email, otp_code })
            console.log('user:', user)
            if (!user) return response(res, 400, {message: 'user not found'})

            const result = user

            return response(res, 200, {
                message: 'Email is verified',
                result: result,
            })
        } catch (error) {
            console.log('VerifyEmail error:', error)
            if (error.message === 'OTP is expired') return response(res, 400, 
            { message: 'OTP is expired' })
            if (error.message === 'Email has been verified') return response
            (res, 401, {message: 'Email has been verified'})
        }
    } catch (error) {
        console.log('Outer error:', error)
        return response(res, 500, error.message)
    }
} 

module.exports = { Register, ResendOTP, VerifyEmail}

» Creating Routes

  1. In the auth-routes.js file we will create the router that will handle all our authentication-related routes.

  2. We will have an HTTP POST endpoint at /register, /resend-otp, /verify. So When a user submits their registration details, this route will handle it. And When a user ask to resend the otp, /resend-otp route will handle it. And When a user wants to verify the account, /verify route will handle it

// auth-routes.js
const express = require('express')
const authRouter = express.Router()
const ctrl = require('./auth-controllers')

authRouter.post('/register', ctrl.Register)
authRouter.post('/resend-otp', ctrl.ResendOTP)
authRouter.post('/verify', ctrl.VerifyEmail)


module.exports = authRouter
  1. Next, we import our authentication routes from the auth-routes.js file (located in the ../modules directory) into the routes.js file.

  2. Routes.js file will use the authRouter for any routes that start with /auth to avoid conflicts with other routes in the future.

//routes.js
const express = require('express')
const router = express.Router()

const authRouter = require('../models/auth-routes')

router.use('/auth', authRouter) 

module.exports = router
  1. The authRouter will take care of validating the input, creating a new user, saving it to the database, and sending an OTP to the user’s email.

» GMAIL Account Password

Now that we got the OTP in the postman, the next step is to Integrating OTP Generation and Delivery through node mailer.

Before all of this we need to configure node mailer so we will start by that. So let’s do some basic configuration. Open the .env file and add these four variables

MAIL_USER=use your own email
MAIL_PASSWORD=this password isn't your account 
password, but your application password from your 
gmail account.
MAIL_PORT=465
MAIL_HOST=smtp.gmail.com
  1. Go into Manage your Gmail account

  2. Click on Security button in the sidebar and scroll down until you see “2-Step_verification”

Name a Project

  1. Click on 2-Step-Verification and scroll down until you see “App password”.

Name a Project

  1. Now fill the form “App name” whatever you want. For example “iOS”.

Name a Project

  1. Click on “Create” button to generate the App Password, and there you go! And paste the password in .env file

» Nodemailer

Next up we will set up node mailer so for that open the file node mailer.js

const nodemailer = require('nodemailer')
require('dotenv').config()

function sendEmail(email, subject, message) {
    const transporter = nodemailer.createTransport({
        host: process.env.MAIL_HOST,
        port: process.env.MAIL_PORT,
        auth: {
            user: process.env.MAIL_USER,
            pass: process.env.MAIL_PASSWORD,
        },
    })

    const mailOptions = {
        from: process.env.MAIL_USER,
        to: email,
        subject,
        text: message,
    }

    transporter.sendMail(mailOptions)
}

module.exports = { sendEmail }

» OTP Generator

1.Next up we want to create a otp generator that will send the OTP to the user. So open the otp-generator file.

const generateOTP = () => {
    const length = 6
    const characters = '0123456789'

    let otp = ''
    for (let o = 0; o < length; o++) {
        const getRandomIndex = Math.floor(Math.random() 
        * characters.length)
        otp += characters[getRandomIndex]
    }

    return otp
}

module.exports = generateOTP

» Test endpoints on postman

  1. So open the postman app and in here let’s make a POST request to our endpoint that is /auth/register, /auth/resend-otp, /auth/verify for the respnses. Look for the results in picture given below:

  2. Register Endpoint Name a Project

  3. Resend Endpoint Name a Project

  4. Verify Endpoint Name a Project

» Creating Frontend

  1. We will create two jsx file in the src folder present in the root directory. Name these files as RegisterForm.jsx and VerifyEmailForm.jsx

Before moving further, let’s first start the frontend part of our application, so open the terminal and run the command npm start. And it will start on the localhost:whatever.

  1. Register Form:
import React, { useState } from 'react';
import axios from 'axios';
import './App.css';

function RegistrationForm() {
    const [username, setUsername] = useState('');
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [message, setMessage] = useState('');

    const handleSubmit = async (event) => {
        event.preventDefault();

        try {
            const response = await axios.post('http://localhost:4000/auth/
            register', { username, email, password });
            setMessage(response.data.message);
        } catch (error) {
            setMessage(error.response.data.message);
        }
    };

    return (
        <form onSubmit={handleSubmit} className='App'>
            <label>
                Username:
                <input type="name" value={username} onChange={(e) => 
                setUsername(e.target.value)} required />
            </label>
            <label>
                Email:
                <input type="email" value={email} onChange={(e) => 
                setEmail(e.target.value)} required />
            </label>
            <label>
                Password:
                <input type="password" value={password} onChange={(e) => 
                setPassword(e.target.value)} required />
            </label>
            <button type="submit">Register</button>
            {message && <p>{message}</p>}
        </form>
    );
}

export default RegistrationForm;
  1. Verify Form
import React, { useState } from 'react';
import axios from 'axios';
import './App.css';

function VerifyEmailForm() {
    const [email, setEmail] = useState('');
    const [otp, setOtp] = useState('');
    const [message, setMessage] = useState('');

    const handleSubmit = async (event) => {
        event.preventDefault();

        try {
            const response = await axios.post('http://localhost:4000/auth/
            verify', { email, otp_code: otp });
            console.log(response);
            if (response.data.result[0].message === 'Email is verified') {
                window.alert('Email verification successful');
            }
        } catch (error) {
            console.log(error.response);
        }
    };

    return (
        <form onSubmit={handleSubmit} className='App'>
            <label>
                Email:
                <input type="email" value={email} onChange={(e) => 
                setEmail(e.target.value)} required />
            </label>
            <label>
                OTP:
                <input type="text" value={otp} onChange={(e) => 
                setOtp(e.target.value)} required />
            </label>
            <button type="submit">Verify Email</button>
            {message && <p>{message}</p>}
        </form>
    );
}

export default VerifyEmailForm;
  1. App.jsx file
import React from 'react';
import VerifyEmailForm from './VerifyEmailForm';
import RegistrationForm from './RegistrationForm';
export default function App() {
  return (
    <div>
      <RegistrationForm />
      <VerifyEmailForm />
    </div>
  );
}

Now test the app on localhost.


And that’s all for this blog, I hope you found it useful. So I’ll see you in the next one, till then bye bye and take care.