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
As usual, we also need to configure the package preferences,
node ace invoke @adonisjs/mail
For my configuration, I'll be using SMTP
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(),
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.
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.
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
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'
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 })
})
}
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')
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
const currentYear = new Date().getFullYear()
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' })
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 })
})
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 }
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
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' } })
}
}
}
Next, copy this into the users.ts file under the routes directory
Route.get('/verify-email/:email', 'users/EmailVerificationsController.confirm').as('verifyEmail')
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' })
})
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' })
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.
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)
you must run
node ace configure @adonisjs/view
after installing adonisjs view