Build A Support Ticket Application With AdonisJs - Part 1

Chimezie Enyinnaya

AdonisJs is a MVC Framework for Node that is greatly inspired by the Laravel framework and borrows some of it concepts. AdonisJs follows the software paradigm of conventions over configuration which allows you focus on building your application rather than waste time on configurations. AdonisJs uses ES2015 Generators which removes the unnecessary callbacks from your code.

To explore more about AdonisJs, checkout this tutorial by the creator of Adonis: Meet AdonisJs! A Laravel-style MVC Framework for Node.js.

In this tutorial, we'll to be building a support ticket application. Some times ago, I did a tutorial on how to build a support ticket with Laravel. We're basically going to rebuild that application, but this time we'll build it with AdonisJs.

A support ticket application provides a medium for customers to lodge issues they face using a particular organization's service/product by opening a support ticket with the organization's help desk.

What we will be building will be minimal, but it will have the major features of support ticket application.

Requirements And Flow

  • Only authenticated user can open support tickets.
  • Upon opening the tickets, an email will be sent to the user along with the details of the support ticket opened.
  • Subsequently, mails will be sent to the user as the customer support staff or admin response to the ticket.
  • The user can also respond to the ticket he/she opened by commenting on the ticket.
  • The admin or the customer support staff can also mark a ticket as resolved.
  • Once a ticket is marked as closed, the user who opened the ticket will be notified by email on the status of the ticket.

Below is an image of the final output of what we'll be building.

Let's Get Started

We'll start by installing the Adonis-CLI which is a command line tool to install stable and dev releases of AdonisJs with all required dependencies.

npm install -g adonis-cli

Note: you might need to use sudo with the above command.

With the adonis-cli installed, let's create a new project:

adonis new adonis-support-ticket

This will create a new project called adonis-support-ticket and will also install all the required dependencies from NPM.

Note: If you are using Node.JS < 6.0, make sure to make following changes. Replace the scripts inside package.json file with following:

// package.json

"scripts": {
  "serve:dev": "nodemon --watch app --watch bootstrap --watch config --watch .env -x \"node --harmony_proxies\" server.js",
  "serve": "node --harmony_proxies server.js"
}

Lastly replace the first line of the ace file with following .ace:

// .ace

#!/usr/bin/env node --harmony_proxies

You can test to make sure the installation was successful by running:

cd adonis-support-ticket
npm run serve:dev

Now open http://localhost:3333 to see the welcome page.

The rest of this tutorial assumes you are already in the project directory which is adonis-support-ticket.

Database Setup

We'll be using MySQL for our database. First, we need to install a Node.JS driver for MySQL:

npm install mysql --save

Now we need a database to hold our data. Open the .env file and fill it with your database details.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_USER=root
DB_PASSWORD=
DB_DATABASE=adonis-support

Update the details above with your own database details.

User Authentication

Only registered users can make use of our Support Ticket Application, so we need an authentication system which will allow users to register and login to the application. AdonisJs can get us up to speed with that. With AdonisJs Authentication Provider, we can generate required migrations using an ace command:

./ace auth:setup

This will create the required migrations and models for authentication. AdonisJs by default, makes use of the session authenticator (which we'll stick to in this tutorial) to authenticate requests. Since we'll be using the session based login system, we are concerned with the User model and users table migration. But before we run the migrations, let's modify the users migration to suit our application. Update the up() of users table migration with:

// database/migrations/...create_users_table.js

// users table migration showing only the up() schemas with our modifications
this.create('users', table => {
    table.increments()
    table.string('username', 80).notNullable().unique()
    table.string('email', 254).notNullable().unique()
    table.string('password', 60).notNullable()
    table.integer('is_admin').unsigned().default(0);
    table.timestamps()
})

We add an is_admin column which will help us track if a particular user is an admin or not. Go ahead and run the migrations:

./ace migration:run

Now we can start building the authentication system. Let's start by creating a HTTP request controller called AuthController which will handle all the authentication logic:

./ace make:controller Auth

This will create AuthController.js within the app/Http/Controllers directory.

Before we start writing the authentication logic, let's setup Validator which we'll use for form validation since we'll need to validate user submitted data.

Validator is not the part of the base installation and hence we are going to install and register it manually:

npm install adonis-validation-provider --save

Once installed, we need to add it to the providers array in bootstrap/app.js

// bootstrap/app.js

const providers = [
    // ...
    'adonis-validation-provider/providers/ValidatorProvider'
    // ...
]

Also, we need to add it to the aliases object in bootstrap/app.js

// bootstrap/app.js

const aliases = {
    // ...
    Validator: 'Adonis/Addons/Validator'
    // ...
}

Now we can start using the Validator. Let's define some rules for our users registration. Open app/Model/User.js and add the code below to it:

// app/Model/User.js

/**
 * Define validation rules
 */
static get rules() {
    return {
        username: 'required|unique:users',
        email: 'required|email|unique:users',
        password: 'required|confirmed|min:6'
    }
}

Yeah, just some basic vaildation rules. We'll be using these rules shortly.

Now open app/Http/Controllers/AuthController.js and add the following to it:

// app/Http/Controllers/AuthController.js

// remember to add these to the top of file after 'use strict'
const User = use('App/Model/User')
const Validator = use('Validator')

/**
 * Show register page
 */
* showRegisterPage(request, response) {
    yield response.sendView('auth.register')
}

/**
 * Handle user registration
 */
* register(request, response) {
    // validate form input
    const validation = yield Validator.validateAll(request.all(), User.rules)

    // show error messages upon validation fail
    if (validation.fails()) {
        yield request
            .withAll()
            .andWith({ errors: validation.messages() })
            .flash()

        return response.redirect('back')
    }

    // persist to database
    const user = yield User.create({
        username: request.input('username'),
        email: request.input('email'),
        password: request.input('password')
    })

    // login the user
    yield request.auth.login(user)

    // redirect to homepage
    response.redirect('/')
}

The showRegisterPage() will simply render the user registration page. The register() will handle the user registration. It first validates the user submitted form inputs against the rules we defined above. If the validation fails, we return back to the registration page with the form request and display the appropriate error message(s) as flash message. If the validation passes, we persist user data to the database using Lucid, which is AdonisJs ORM to make secure SQL queries. Next, we log the registered user in and lastly redirect them to the homepage.

User Registrarion Routes

Next, we need to define our user registration routes. Open app/Http/routes.js and paste the code below into it:

// app/Http/routes.js

Route.get('register', 'AuthController.showRegisterPage')
Route.post('register', 'AuthController.register')

We defined two register routes that will handle GET and POST request respectively. The first route will trigger the showRegisterPage() on AuthController while the second will trigger the register() on AuthController.

User Registration View

AdonisJs view engine is built on top of nunjucks. We'll start by creating a master layout which we would use across the entire application. Within the views directory, there is a master.njk view file, we are going to move this file to a new directory called layouts:

cd resources/views
mkdir layouts
mv master.njk layouts

With master.njk in its new directory, let's open it up and replace the code in it with the following:

<!-- resources/views/layouts/master.njk -->

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

    <title>Adonis Support - {% block title %}{% endblock %}</title>

    <link rel="icon" href="/assets/favicon.png" type="image/x-icon">

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
    <nav class="navbar navbar-default">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse" aria-expanded="false">
            <span class="sr-only">Menu</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="/">Adonis Support</a>
        </div>

        <div class="collapse navbar-collapse" id="navbar-collapse">
          <ul class="nav navbar-nav navbar-right">
            {% ifAsync currentUser %}
              <li><a href="/new_ticket">Open Ticket</a></li>
              <li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">{{ currentUser.username }} <span class="caret"></span></a>
                <ul class="dropdown-menu">
                  <li><a href="/my_tickets">My Tickets</a></li>
                  <li role="separator" class="divider"></li>
                  <li><a href="/logout">Logout</a></li>
                </ul>
              </li>

            {% else %}
              <li><a href="/login">Login</a></li>
              <li><a href="/register">Register</a></li>
            {% endif %}
          </ul>
        </div>
      </div>
    </nav>

    <div class="container">
      {% block content %}{% endblock %}
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
</body>
</html>

A basic Bootstrap layout, the navbar shows links for users to either login or register if they are not already logged in. Otherwise it shows a link to open a new ticket and a dropdown menu with a link to display all tickets created my the logged in user and lastly, a logout link. The main page of the layout contains a content block which will be replaced by the actual content of the page extending this master layout.

With the master layout structured, let's move on to creating the user registration view. Create a new directory called auth within the resources/views directory, this will house all the application's authentication view files. Within the auth directory, create a new file named register.njk and paste the code below to it:

<!-- resources/views/auth/register.njk -->

{% extends 'layouts.master' %}

{% block title %}
    Register
{% endblock %}

{% block content %}
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Register</div>
                    <div class="panel-body">

                        {% include 'includes.errors' %}

                        {{ form.open({ url: '/register', method: 'POST', class: 'form-horizontal' }) }}
                            {{ csrfField }}

                            <div class="form-group">
                                {{ form.label('username', 'Username', { class: 'col-md-4 control-label' }) }}

                                <div class="col-md-6">
                                    {{ form.text('username', old('username'), { class: 'form-control' }) }}
                                </div>
                            </div>

                            <div class="form-group">
                                {{ form.label('email', 'Email', { class: 'col-md-4 control-label' }) }}

                                <div class="col-md-6">
                                    {{ form.email('email', old('email'), { class: 'form-control' }) }}
                                </div>
                            </div>

                            <div class="form-group">
                                {{ form.label('password', 'Password', { class: 'col-md-4 control-label' }) }}

                                <div class="col-md-6">
                                    {{ form.password('password', null, { class: 'form-control' }) }}
                                </div>
                            </div>

                            <div class="form-group">
                                {{ form.label('password_confirmation', 'Confirm Password', { class: 'col-md-4 control-label' }) }}

                                <div class="col-md-6">
                                    {{ form.password('password_confirmation', null, { class: 'form-control' }) }}
                                </div>
                            </div>

                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    {{ form.button('Register', null, { class: 'btn btn-primary' }) }}
                                </div>
                            </div>
                        {{ form.close() }}
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

As you can see, the register.njk is extending the master we created above. Noticed we included a errors partial (which we'll create shortly), this partial will display error messages encountered during form validation we discussed earlier on. We are using AdonisJs form builder which is pretty straightforward. Because AdonisJs protects us from CSRF, we need to add CSRF token to our form. Once the form is submitted, a POST request will be made to the register route.

Now create the errors partial we talked about. Create a new directory called includes within the resources/views directory. Within the includes directory, create a new file named errors.njk and paste the code below into it:

<!-- resources/views/includes/errors.njk -->

{% if old('errors') %}
    <div class="alert alert-danger">
        <ul>
            {% for error in old('errors') %}
                <li>{{ error.message }}</li>
            {% endfor %}
        </ul>
    </div>
{% endif %}

If there are errors in the flash messages, we loop through each of them and display them upon failed form validation.

When we visit http://localhost:3333/register in our browser, we should get a screen like below:

Go on and register. Now that we have registered, we need a way to login into the application. Okay, let's tackle that. Open app/Http/Controllers/AuthController.js and add the following code to it:

// app/Http/Controllers/AuthController.js

/**
 * Show login page
 */
* showLoginPage(request, response) {
    yield response.sendView('auth.login')
}

/**
 * Handle user authentication
 */
* login(request, response) {
    const email = request.input('email')
    const password = request.input('password')

    try {
        yield request.auth.attempt(email, password)

        // redirect to homepage
        response.redirect('/')
    } catch (e) {
        yield request.with({ error: 'Invalid credentials' }).flash()

        // redirect back with error
        response.redirect('back')
    }
}

The showLoginPage() will render the login page, while login() will handle the actual user authentication. We attempt to log the user in with the credentials (email and password) submitted and redirect them to the homepage. If either of the credential they submitted does not correspond with what we have in our database, we simple return them to the login page with the error message "Invalid credentials".

User Login Routes

Next, we need to define the login route. Open app/Http/routes.js and paste the code below into it:

// app/Http/routes.js

Route.get('login', 'AuthController.showLoginPage')
Route.post('login', 'AuthController.login')

We defined two login routes that will handle GET and POST request respectively. The first route will trigger the showLoginPage() on AuthController while the second will trigger the login() on AuthController.

User Login View

Create a new file named login.njk within the resources/views/auth directory and paste the code below to it:

<!-- resources/views/auth/login.njk -->

{% extends 'layouts.master' %}

{% block title %}
    Login
{% endblock %}

{% block content %}
  <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Login</div>
                    <div class="panel-body">

                        {% if old('error') %}
                            <div class="alert alert-danger">
                                {{ old('error') }}
                            </div>
                        {% endif %}

                        {{ form.open({ url: '/login', method: 'POST', class: 'form-horizontal' }) }}
                            {{ csrfField }}

                            <div class="form-group">
                                {{ form.label('email', 'Email', { class: 'col-md-4 control-label' }) }}

                                <div class="col-md-6">
                                    {{ form.email('email', null, { class: 'form-control' }) }}
                                </div>
                            </div>

                            <div class="form-group">
                                {{ form.label('password', 'Password', { class: 'col-md-4 control-label' }) }}

                                <div class="col-md-6">
                                    {{ form.password('password', null, { class: 'form-control' }) }}
                                </div>
                            </div>

                            <div class="form-group">
                                <div class="col-md-6 col-md-offset-4">
                                    {{ form.button('Login', null, { class: 'btn btn-primary' }) }}
                                </div>
                            </div>
                        {{ form.close() }}
                    </div>
                </div>
            </div>
        </div>
    </div>
{% endblock %}

Again, a simple login form with email and password fields. There is also a section that will display login failure error. Just as with the register form, we add CSRF token to the form. Once the form is submitted, a POST request will be made to the login route.

When we visit http://localhost:3333/login in our browser, we should get a screen like below:

We should be able to login now.

User Logout

Let's allow logged in user to be able to logout of the application. Open app/Http/Controllers/AuthController.js and add the following code to it:

// app/Http/Controllers/AuthController.js

/**
 * Logout authenticated user
 */
* logout(request, response) {
    // logouts the user
    yield request.auth.logout()

    // redirect to login page
    response.redirect('/login')
}

The logout() simply logs the authenticated user out and redirect to the login page.

User Logout Route

// app/Http/routes.js

Route.get('logout', 'AuthController.logout')

Ticket And Category Model With Their Migrations

Having taken care of users authentication, we move on to allow users create new tickets. First we need a tickets table to hold the tickets that users will be creating and also a Ticket model for them.

./ace make:model Ticket --migration

The above command will create a Ticket model and tickets table migration. Open the tickets table migration file and update it with:

// database/migrations/...create_ticket_table.js

// tickets table migration showing only the up() schemas with our modifications
this.create('tickets', (table) => {
    table.increments()
    table.integer('user_id').unsigned()
    table.integer('category_id').unsigned()
    table.string('ticket_id').unique()
    table.string('title')
    table.string('priority')
    table.text('message')
    table.string('status')
    table.timestamps()
})

The migration is straightforward, we have a user_id column that will hold the ID of the user that created the ticket, a category_id column to indicate the category the ticket belongs, ticket_id column which hold a unique random string that will be used to refer to the ticket, a title column, a priority column, a message column and a status column which indicate whether a ticket is open or closed.

We also need a categories table to hold our various categories:

./ace make:model Category --migration

As above, this will create a Category model and categories table migration. Open the categories table migration file and update it with:

// database/migrations/...create_category_table.js

// categories table migration showing only the up() schemas with our modifications
this.create('categories', (table) => {
    table.increments()
    table.string('name')
    table.timestamps()
})

We can now run the migrations:

./ace migration:run

Ticket To Category Relationship

A ticket can belong to a category, while a category can have many tickets. This is a one to many relationship and we'll use Lucid to setup the relationship.

Open the Ticket model and add the following code to it:

// app/Model/Ticket.js

/**
 * A ticket belongs to a category
 */
category() {
    return this.belongsTo('App/Model/Category')
}

Next, we need to create the inverse relationship, open the Category model and add the following code to it:

// app/Model/Category.js

/**
 * A category can have many tickets
 */
tickets () {
    return this.hasMany('App/Model/Ticket')
}

Adding Categories

For now, let's manually populate the categories table with some data. AdonisJs provides Seeds and Factories which we can use to generate fake data. For the purpose of this tutorial, we won't be using Factories, we'll only use Seeds and Query Builder. Open app/database/seeds/Database.js and update the run():

// app/database/seeds/Database.js

// remember to add these to the top of file
const Database = use('Database')

* run () {
    yield Database.table('categories').insert([
        {
            name: 'Technical',
            created_at: '2017-03-07 00:00:00',
            updated_at: '2017-03-07 00:00:00'
        },
        {
            name: 'Sales',
            created_at: '2017-03-07 00:00:00',
            updated_at: '2017-03-07 00:00:00'
        }
])

Using AdnonisJs query builder, we perform database insertion which insert two categories in the categories table of our database. To run the database seeder, we make use of an ace command:

./ace db:seed

Now we should have two categories in our categories table.

Tickets Controller

We need a controller that will contain the logic for opening a ticket. Run the command below to create an HTTP request controller:

./ace make:controller Tickets

This will create a TicketsController. Open app/Http/Controllers/TicketsController.js and add the code below to it:

// app/Http/Controllers/TicketsController.js

// remember to add these to the top of file
const Category = use('App/Model/Category')

/**
 * Show the form for opening a new ticket.
 */
* create(request, response) {
    const categories = yield Category.pair('id', 'name')

    yield response.sendView('tickets.create', {categories: categories})
}

The create() will get all the categories created and pass them along to a view file. Since we'll be displaying our categories in a select dropdown on the ticket opening form, we use the pair() which will return a flat object with a key/value pair of LHS and RHS key. Before we move further, let's create the routes that will handle opening a new ticket.

// app/Http/routes.js

Route.get('new_ticket', 'TicketsController.create')
Route.post('new_ticket', 'TicketsController.store')

The first route new_ticket will show the form to open a new ticket while the second route will call the store() on TicketsController which will do the actual storing of the ticket in the database.

The create() from above will render a view file tickets.create which we are yet to create. Let's go on and create the file.

Create a new folder named tickets in the resources/views directory and inside the tickets directory, create a new file named create.njk and add the following code to:

<!-- resources/views/tickets/create.njk -->

{% extends 'layouts.master' %}

{% block title %} Open Ticket {% endblock %}

{% block content %}
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">Open New Ticket</div>

                <div class="panel-body">
                    {% include 'includes.status' %}
                    {% include 'includes.errors' %}

                    {{ form.open({ url: '/new_ticket', method: 'POST', class: 'form-horizontal' }) }}
                        {{ csrfField }}

                        <div class="form-group">
                            {{ form.label('title', 'Title', { class: 'col-md-4 control-label' }) }}

                            <div class="col-md-6">
                                {{ form.text('title', old('title'), { class: 'form-control' }) }}
                            </div>
                        </div>

                        <div class="form-group">
                            {{ form.label('category', 'Category', { class: 'col-md-4 control-label' }) }}

                            <div class="col-md-6">
                                {{ form.select('category', categories, null, 'Select Category', { class: 'form-control' }) }}
                            </div>
                        </div>

                        <div class="form-group">
                            {{ form.label('priority', 'Priority', { class: 'col-md-4 control-label' }) }}

                            <div class="col-md-6">
                                {{ form.select('priority', { low: 'Low', medium: 'Medium', high: 'High' }, null, 'Select Priority', { class: 'form-control' }) }}
                            </div>
                        </div>

                        <div class="form-group">
                            {{ form.label('message', 'Message', { class: 'col-md-4 control-label' }) }}

                            <div class="col-md-6">
                                {{ form.textarea('message', null, { class: 'form-control', rows: 10 }) }}
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                {{ form.button('Open Ticket', null, { class: 'btn btn-primary' }) }}
                            </div>
                        </div>
                    {{ form.close() }}
                </div>
            </div>
        </div>
    </div>
{% endblock %}

Again we are using the AdonisJs form builder. Notice we included a status and errors partials. The status partial will display a flash message upon successful opening of ticket. The errors partial is the same we created earlier.

Status Flash Message

Create a file named status.njk in the resource/views/includes folder inside the views folder. Paste the code snippets below into it:

<!-- resources/views/includes/status.njk -->

{% if old('status') %}
    <div class="alert alert-success">
        {{ old('status') }}
    </div>
{% endif %}

When we visit http://localhost:3333/new_ticket in our browser, we should get a screen like below:

To handle the actual saving of the ticket to the database, open TicketsController and add the store() to it.

// app/Http/Controllers/TicketsController.js

// remember to add these to the top of file
const Mail = use('Mail')
const Validator = use('Validator')
const Ticket = use('App/Model/Ticket')
const RandomString = use('randomstring')
const Category = use('App/Model/Category')

/**
 * Store a newly created ticket in database.
 */
* store(request, response) {
    const user = request.currentUser

    // validate form input
    const validation = yield Validator.validateAll(request.all(), {
        title: 'required',
        category: 'required',
        priority: 'required',
        message: 'required'
    })

    // show error messages upon validation fail
    if (validation.fails()) {
        yield request
            .withAll()
            .andWith({ errors: validation.messages() })
            .flash()

        return response.redirect('back')
    }

    // persist ticket to database
    const ticket = yield Ticket.create({
        title: request.input('title'),
        user_id: user.id,
        ticket_id: RandomString.generate({ length: 10, capitalization: 'uppercase' }),
        category_id: request.input('category'),
        priority: request.input('priority'),
        message: request.input('message'),
        status: "Open",
    })

    // send mail notification
    yield Mail.send('emails.ticket_info', { user, ticket }, (message) => {
        message.to(user.email, user.username)
        message.from('support@adonissupport.dev')
        message.subject(`[Ticket ID: ${ticket.ticket_id}] ${ticket.title}`)
    })        

    yield request.with({ status: `A ticket with ID: #${ticket.ticket_id} has been opened.` }).flash()
    response.redirect('back')
}

We'll use an NPM package called randomstring to generate random string for our ticket IDs. So let's install the package:

npm install randomstring --save

Note: the randomstring package is require at the top of TicketsController.

Within the store() we get the currently authenticated user. Next, we set some form validation rules that must be met before moving forward. If the validation fail, we redirect the user back to the ticket opening page with appropriate error messages. Otherwise, a new the ticket is created and an email containing the ticket details is sent to the user (more on this below) and finally the user is redirected back with a success message.

Sending Ticket Information Mail

AdonisJs official Mail Provider makes it so easy and intuitive to send emails using one of the available drivers. Mail provider is not part of the base installation, and we have to pull the package from NPM and register the provider:

npm install adonis-mail-provider --save

Next, we need to register the provider in bootstrap/app.js:

// bootstrap/app.js

const providers = [
    ...,
    'adonis-mail-provider/providers/MailProvider'
]

and setup an alias:

// bootstrap/app.js

const aliases = {
    Mail: 'Adonis/Addons/Mail'
}

Next, we need a configuration file that tell AdonisJs which Mail driver we are using along with the driver's details. Create a mail.js file within the config directory and paste the following code into it:

// config/mail.js
'use strict'

const Helpers = use('Helpers')
const Env = use('Env')

module.exports = {
  /*
  |--------------------------------------------------------------------------
  | Driver
  |--------------------------------------------------------------------------
  |
  | driver defines the default driver to be used for sending emails. Adonis
  | has support for 'mandrill', 'mailgun', 'smtp', 'ses' and 'log' driver.
  |
  */
  driver: Env.get('MAIL_DRIVER', 'smtp'),

  /*
  |--------------------------------------------------------------------------
  | SMTP
  |--------------------------------------------------------------------------
  |
  | Here we define configuration for sending emails via SMTP.
  |
  */
  smtp: {
    pool: true,
    port: 2525,
    host: '',
    secure: false,
    auth: {
      user: Env.get('MAIL_USERNAME'),
      pass: Env.get('MAIL_PASSWORD')
    },
    maxConnections: 5,
    maxMessages: 100,
    rateLimit: 10
  },

  /*
  |--------------------------------------------------------------------------
  | Mandrill
  |--------------------------------------------------------------------------
  |
  | Here we define api options for mandrill. Mail provider makes use of
  | mandrill raw api, which means you cannot set email body specific
  | options like template, tracking_domain etc.
  |
  */
  mandrill: {
    apiKey: Env.get('MANDRILL_APIKEY'),
    async: false,
    ip_pool: 'Main Pool'
  },

  /*
  |--------------------------------------------------------------------------
  | Amazon SES
  |--------------------------------------------------------------------------
  |
  | Here we define api credentials for Amazon SES account. Make sure you have
  | verified your domain and email address, before you can send emails.
  |
  */
  ses: {
    accessKeyId: Env.get('SES_KEY'),
    secretAccessKey: Env.get('SES_SECRET'),
    region: 'us-east-1',
    rateLimit: 10
  },

  /*
  |--------------------------------------------------------------------------
  | MailGun
  |--------------------------------------------------------------------------
  |
  | Here we define api credentials for Amazon SES account. Make sure you have
  | verified your domain and email address, before you can send emails.
  |
  */
  mailgun: {
    domain: Env.get('MAILGUN_DOMAIN'),
    apiKey: Env.get('MAILGUN_APIKEY')
  },

  /*
  |--------------------------------------------------------------------------
  | Log
  |--------------------------------------------------------------------------
  |
  | Log driver is mainly for testing your emails expectations. Emails are
  | written inside a log file, which can be used for inspection.
  |
  */
  log: {
    toPath: Helpers.storagePath('logs/mail.eml')
  }
}

Let's take a closer look at the mail sending snippet from the save() above.

// app/Http/Controllers/TicketsController.js

// send mail notification
yield Mail.send('emails.ticket_info', { user, ticket }, (message) => {
    message.to(user.email, user.username)
    message.from('support@adonissupport.dev')
    message.subject(`[Ticket ID: ${ticket.ticket_id}] ${ticket.title}`)
})

The Mail's send() accepts a path to a view file email/ticket_info.njk, an object of data (user and ticket) we want to pass to the view file and lastly a callback. The view file will be inside the emails directory (which does not exist yet) and will be used to compose the mail we want to send. The message argument passed to the callback is an instance of message builder which is used to build the mail by specifying who we want to send the mail to, who is sending the mail and the subject of the mail.

Now let's create the email view file. Create a new ticket_info.njk file within the resources/views/emails directory and add:

<!-- resources/views/emails/ticket_info.njk -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Suppor Ticket Information</title>
</head>
<body>
    <p>
        Thank you {{ user.username }} for contacting our support team. A support ticket has been opened for you. You will be notified when a response is made by email. The details of your ticket are shown below:
    </p>

    <p>Title: {{ ticket.title }}</p>
    <p>Priority: {{ ticket.priority }}</p>
    <p>Status: {{ ticket.status }}</p>

    <p>
        You can view the ticket at any time at http://localhost:3333/tickets/{{ ticket.ticket_id }}
    </p>

</body>
</html>

This is the email that will be sent to the user once a ticket is created.

Before testing this out in the browser, remember to configure your Mail settings. For the purpose of this tutorial, we won't be sending actual emails. Instead we will just log them. So open .env file and update it as below:

MAIL_DRIVER=log

Now hit the browser and try opening a new ticket, you should see the mail content logged in the storage/logs/mail.eml file once the ticket is created.

Conclusion

So far, we have been able to setup our application and created our first ticket. You can see how simple and straightforward it is to develop an app with AdonisJs. We have been able to utilize AdonisJs Query Builder to define our table structures without writing a single SQL command, layout our views with its built in View Engine without doing extra setup as with other Node.JS frameworks (Express, Hapi.js etc). We also saw Lucid ORM in action which makes it seamless to interact with our database irrelevant of the databse type. Lastly, we saw Ace, a powerful interactive shell which we used to scaffold and perform some operations from within a terminal.

In the next post we'll cover commenting on ticket and marking ticket as closed. If you have any questions about the tutorial, let me know in the comments below.

Chimezie Enyinnaya

7 posts

Web Developer [PHP Laravel JavaScript NodeJS VueJS|AngularJS] | movie lover | run http://openlaravel.com | blogs at http://itoocode.com