Sending verification OTP to user-email with mailgun in a React, Node and Mongo App.

I will implement a feature to an existing simple MERN stack app, to send an email to the visitor of a site containing a randomly generated unique code which the visitor will need to verify to view a document on the site.

Source Code of the project in Github. Note, in this Github repo the branch containing this functionality is named “sending-otp-with-mailgun”. The master branch of this same repo has the same app but without the OTP sending feature.

(Note the repo will ONLY work after proper AWS and mailgun credentials (API key, Secret Key etc) are are set up in the .env file and the .env.override file).

Often we see in many site, that to get a download link or viewing a file, I first need to fill a form with my email and name etc and only then either the download link will be sent to that email, or an unique code will be sent first to that email, and then I have to put that code back to the site, and only then the required document will be rendered.

In this simple implementation, I have a React, Node, Mongo App running, where the user uploads file, which is saved in AWS-S3 and then user can view/download or delete the file or can edit the description text of the file. See my previous blog on how to build this project.

Here in this post, starting with the above app, I will implement this feature. When an user wants to view or download a file, he/she first needs to be verified by sending an OTP to their email > then he puts that OTP back to the App > only if the OTP matches to what was sent to his email the App render the link for the file.

To implement the above, in my app, the flow of this process is as follows

When in the main page, the user clicks on the View/Download a Material-UI Dialog will open > User types in the email and click Submit > this onSubmit event will generate a random string (using “shortid” npm package) and will send an email to the one that user just has typed in the modal — at the same time node backend code will also save that OTP in mongo database > and then it will open the second Material-UI Dialog for the user to input the unique string that he has received in his email > after he inputs the same, and clicks on the submit button > my node backend code will check if this code submitted is the one that was saved in Mongo when user clicked on the first Dialog > and if its same the document link is rendered.

As the first step, to build this functionality, create your own account in mailgun. They have free scheme under which I guess you can send 10,000 free emails per month. During the signup process putting Credit Card credentials is only optional. And if you dont want to put your Credit Card credentials, then after creating the account, manually set up Authorized Recipients (i.e. a list of emails) who can receive emails from your mailgun credentials. But if you put your Credit Card credentials you dont have to setup this Authorized Recipients. Note, I have install mailgun-js npm package to be used along with mailgun.

And then set up your .env file in the project root with your own AWS and mailgun credentials with the below format.

Then in the backend create the MongoDB schema for saving the OTP

Now in my backend Express code in fileUploadRoutes.js, for the two routes to “/sendotptovisitor" (will be hit when user clicks on the first Dialog to view a file) and “/visitor/:id” (will be hit when user clicks on the second Dialog to input the OTP) are as below

In the above the reason I need the wrapper function findLatestOTP — is because of Node’s asynchronous nature of execution. If I directly do the mongdb query and assign the latest OTP of the user inside the mongoose .find() function without using the wrapper like below

Then the latestOtp will not be assigned the correct value and will retain its empty array value for the next piece of code.

To check that, I put a <console.log(latestOtp) > after the above code (i.e. the code without the wrapper function), and it will print the initial value of latestOtp, which is an empty array.

This is because, in Node.js all I/O operations are performed asynchronously. Since the database query requires a disk read and doesn’t require any CPU time, node goes ahead and uses this time to execute the next bit of code while it waits for the result of the database query to run.
In this case the next bit that gets executed is that console.log, and it takes the initial value of latestOtp. So the way to deal with this situation is to write a seperate function (findLatestOTP) and the second argument of this function is the one.

Code in the mailSender.js file (below image) takes care of actually sending the email and mailCompose.js (along with mailgen npm package) file composes the email, meaning what will be the content of the email . Refer to mailgun’s API for understanding this better.

And in the above within the “/sendotptovisitor” route, I send the email with the line mailSender.sendOTPToVisitor(visitorEmail, newGeneratedOTP)

In the above mailSender.js file, I am using the async package (one of the popular and most widely used tools in the Node.js community) and async.waterfall method. Lets understand it in more detail.

In waterfall, we pass array of functions to async.waterfall(). All the functions are executed in series and output of each function is passed to the next function. In case if any error is passed to the function’s callback, then main callback function will be invoked by skipping rest of functions in the queue. That is, In case if we pass error object to any function callback as shown below, we get main callback executed.

The waterfall method takes two parameters:

async.waterfall([], function (err) {});
  • The first argument is an array and the second argument is a function.
  • Using the first argument (i.e. the array), you can specify what you want to run in order.
  • Using the second argument (i.e. the function), you can catch any errors that happens in any of the steps.
function firstStep(done) {
done(null, 'Value from step 1');
function secondStep(previousResult, done) {
function (err) {});

Every step function takes two arguments, except the first one. The first step function only takes one argument. Using the arguments, you can access the result of the previous step and also invoke the next step.

Notice, in firstSetp() function I am calling the done function with two arguments: the first argument is any error that we want to pass to the next step, and the second argument is the actual result or value that we want to pass to the next step. I have set error values to null, because for now, we don't really care about the errors. Hopefully, it should make more sense why the first step function takes one parameter. It's because nothing has been executed before the first function, so there are no results to be passed onto the first function.

To understand lets take a look at the below example code

And the output I will get from above is

Program Start
First Step →
Second Step → 1 2
Third Step → 3
Main Callback → final result
Program End

So, in my case here, withing the async.waterfall method in mailSender.js file, first generateTemplate() function gets executed and its output from its callback() is passed to the next function sendMail().

Now done with backend, and only thing left to do is the React front end. Most of the jobs about the taking user’s email and sending him the email are performed by the VisitorModal.js component. Here, I am using Material-UI Dialog to open two sequential Dialogs, first one for capturing the user email and the second one of capturing OTP sent to that email.

DataScience | ML | 2x Kaggle Expert. Ex Fullstack Engineer and Ex International Financial Analyst.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store