DEV Community

Cover image for AdonisJs - Events and Mailing Part 1
Ted Ngeene
Ted Ngeene

Posted on • Originally published at tngeene.com

AdonisJs - Events and Mailing Part 1

Welcome back to the fourth installment of the AdonisJs series!

A web framework's ability to handle events and mailing greatly add to its appeal. Luckily, Adonis provides these in an easily configurable way that will set you on your path to building robust APIs and web apps.

In this article, I'll show just how we go about these two crucial concepts in modern software development. In order to achieve our goal, we'll build upon the previous article, where we talked about user registration and login.

If you're not familiar with the concept of events in software development, worry not.

Events can be described as actions that are triggered by another action in a system, for example, you'd want your users to be emailed upon successful registration. In this case, the email sending is an event triggered by successful user registration.

What we'll be working on

For this tutorial, we're going to implement an event that emails our users an activation link for their accounts. The purpose of this is to enforce security in our application.
We definitely wouldn't have bots and fictitious accounts registering in our application.

We'll look at two approaches we can employ to achieve this, but before we do that, let's set up the mailer.

Installing Adonis Mailing package.

Since we'll need users to receive emails, we need a way to make our system be able to send them. Luckily, Adonis also has a mailer package for this. To install, run

 npm i @adonisjs/mail
Enter fullscreen mode Exit fullscreen mode

As usual, we also need to configure the package preferences,

node ace invoke @adonisjs/mail

For my configuration, I'll be using SMTP

tngeene adonis mail config

Open up the env.ts file and paste the following values.

SMTP_HOST: Env.schema.string({ format: 'host' }),
SMTP_PORT: Env.schema.number(),
SMTP_USERNAME: Env.schema.string(),
SMTP_PASSWORD: Env.schema.string(),
DEFAULT_FROM_EMAIL: Env.schema.string(),
Enter fullscreen mode Exit fullscreen mode

These values are the environment variables for our SMTP configuration. I recommend validating them in the env.ts file.
The DEFAULT_FROM_EMAIL is the email that will appear as the sender from our application.
The main environment variables will reside in the .env file. We'll get there in a minute.

Setting up our SMTP service

The Simple Mail Transfer Protocol(SMTP) is an internet standard communication protocol for electronic mail transmission. Mail servers and other message transfer agents use SMTP to send and receive mail messages.

There are a number of SMTP providers; including, google, mailgun, SendGrid...and so on. However, since we are not building a production-ready application yet, I'll be using mailtrap.

tngeene adonis mailtrap

Mailtrap is an email testing tool that Captures SMTP traffic from staging and dev environments. Simply put, it mocks real-world emails in a sandbox environment. Head over to their website, create an account, and navigate to the demo inbox section.

tngeene adonisjs mailtrap demo inbox

Next, open the .env file located in your project's root directory and paste these constants.

  SMTP_HOST=smtp.mailtrap.io
  SMTP_PORT=2525
  SMTP_USERNAME=<your_mailtrap_username>
  SMTP_PASSWORD=<your_mailtrap_password>
  DEFAULT_FROM_EMAIL=admin@fitit.com #varies
Enter fullscreen mode Exit fullscreen mode

The username and password values are found on the mailtrap dashboard, under the integrations section.

Now that we have all that setup, we'll get straight right into actual coding.

Approach 1 - Using a function in a model

This approach will involve us having a function inside our user model that will handle email sending. We'll call this function in our authController, right after a successful registration.

Open the app/Models/User.ts. We'll import several packages at the top,

import Mail from '@ioc:Adonis/Addons/Mail'
import Env from '@ioc:Adonis/Core/Env'
import Route from '@ioc:Adonis/Core/Route'
Enter fullscreen mode Exit fullscreen mode

Next, after all the model column definitions, we'll write a sendVerificationEmail function.


  public async sendVerificationEmail() {
    const appDomain = Env.get('APP_URL')
    const appName = Env.get('APP_NAME')
    const currentYear = new Date().getFullYear()
    const url = Route.builder()
      .params({ email: this.email })
      .prefixUrl(appDomain)
      .makeSigned('verifyEmail', { expiresIn: '24hours' })
    Mail.send((message) => {
      message
        .from(Env.get('DEFAULT_FROM_EMAIL')
        .to(this.email)
        .subject('Please verify your email')
        .htmlView('emails/auth/verify', { user: this, url, appName, appDomain, currentYear })
    })
  }
Enter fullscreen mode Exit fullscreen mode

The code above is contains several building blocks that helps to achieve the email sending functionality.


    const appDomain = Env.get('APP_URL')
    const appName = Env.get('APP_NAME')
Enter fullscreen mode Exit fullscreen mode

These are values which reside in our .env file. If you don't have them already, you can check out the env.example file of this project repo

My current values are

  APP_NAME=Fitit
  APP_URL=http://127.0.0.1:3333
Enter fullscreen mode Exit fullscreen mode
 const currentYear = new Date().getFullYear()
Enter fullscreen mode Exit fullscreen mode

we get the current year. We'll inject this as a variable in our html template for our email

const url = Route.builder()
      .params({ email: this.email })
      .prefixUrl(appDomain)
      .makeSigned('verifyEmail', { expiresIn: '24hours' })
Enter fullscreen mode Exit fullscreen mode

We're using the Route package to encode our user data into an activation token. The makeSigned() function is tied to a verifyEmail route that we'll build in the next part. Finally, we set an expiry period for the token. For this case, I set the period to 24 hours. That way, if a user fails to verify their account within that duration, their account won't be activated.

Mail.send((message) => {
      message
        .from(Env.get('DEFAULT_FROM_EMAIL')
        .to(this.email)
        .subject('Please verify your email')
        .htmlView('emails/auth/verify', { user: this, url, appName, appDomain, currentYear })
    })

Enter fullscreen mode Exit fullscreen mode

This section makes use of the adonis mailer package. We're giving instructions to the program to send an email to the created user's email. The email will appear as received from the value DEFAULT_FROM_EMAIL in our .env file.
The subject line will be, "Please verify your email".

The htmlView method contains the template our email will read from, that is, the html code that styles our verification email.
For this, create a folder named resources in the project root directory. The entry point for all Adonis html templates must be located within this folder.
Since we initialized this project as an API only, we'll install the package that enables us to have .edge templates

npm i @adonisjs/view

The first argument the function takes is the html template to read from.
Create a emails\auth\verify.edge file and paste the code from this github gist. The next argument it takes are the variables to be read in the html template. In our case,

{ user: this, url, appName, appDomain, currentYear }
Enter fullscreen mode Exit fullscreen mode

Writing the Verify Email controller

We're going to write an email verification controller that will be responsible for validating and activating a user's account.

node ace make:controller users/EmailVerificationsController
Enter fullscreen mode Exit fullscreen mode

Open the created file and copy this snippet.

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
import { DateTime } from 'luxon'

export default class EmailVerificationsController {
  public async confirm({ response, request, params }: HttpContextContract) {
    if (request.hasValidSignature()) {
      const user = await User.findByOrFail('email', params.email)
      if (!user.isActivated) {
        user.email_verified_at = DateTime.local()
        user.isActivated = true
        user.save()
        return response.status(202).send({ message: 'Account verified and activated' })
      } else {
        return response.status(409).send({ message: 'Account was already verified' })
      }
    } else {
      return response.status(403).send({ error: { message: 'Invalid token' } })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, copy this into the users.ts file under the routes directory

Route.get('/verify-email/:email', 'users/EmailVerificationsController.confirm').as('verifyEmail')
Enter fullscreen mode Exit fullscreen mode

We're mapping the controller to a route. In adonis, routes can be given custom names, by defining the named route to a function, as(). In our case, the route is called verifyEmail. If you're keen enough, you'll notice that we passed this named route as a parameter in the model function we defined in the User model.

    .makeSigned('verifyEmail', { expiresIn: '24hours' })
    })
Enter fullscreen mode Exit fullscreen mode

From the email verification controller code, we can see we have the confirm() function.
This block contains the user activation logic, that is, immediately the link is hit, we verify the user and activate his account. It also contains constraints that check whether the URL pattern matches the valid the signature, hence the makeSigned() and hasValidSignatureSections()

Finally, we modify the AuthController to send the email after a successful registration. Right after we save our user in the database,

const user = await User.create(data)
    // send verification email
    user?.sendVerificationEmail()

    return response
      .status(201)
      .send({ success: 'Registration successful, check your email inbox for a verification email' })
Enter fullscreen mode Exit fullscreen mode

Testing

For testing open up your postman and repeat the registration steps we used in the previous article. Create a new user, and head over to your mailtrap demo inbox.

If everything worked well, then the following images should be what you see.

tngeene events adonis
tngeene events adonis
tngeene events adonis

There's a lot of content to cover in events. The method covered in this piece is just one way to go about it. To keep the article short, I've decided to cover the next approach in the next piece.

If you have any comments, queries, don't hesitate to leave a comment or email.
Until then, stay Adonis. It is the way!

Top comments (1)

Collapse
 
aliif profile image
Aliif

you must run node ace configure @adonisjs/view after installing adonisjs view