August 24, 2018

Build a Task List with Authentication Using SQL, Node.js and Adonis (Part 2)

by Connor Leech

Build a Task List with Authentication Using SQL, Node.js and Adonis

This is the second part of a two-part series on building a task application, complete with authentication, where guests can view all the tasks and click in to view an individual task.

In the first part, we went through the initial setup, as well as creating a local database, adding authentication and seeding data. In this second part, we will go over defining controllers, handling authentication and adding the task views.

Protect Your Node.js App

Define controller

At this stage, our route endpoints are defined, but if we fire up the dev server and head to localhost:3333/tasks we see an error that "Cannot find module TaskController". To create a task controller, run:

$ ./ace make:controller Task --resource 

This command generates a file in app/Http/Controllers/TaskController.js that stubs out the various methods we defined above. The methods will be blank after we generate the command. Take the liberty of filling them out. We're going to use Lucid, which is Adonis' implementation of ActiveRecord. Essentially, it is an ORM so we don't have to write raw SQL queries to fetch our data.

'use strict'

class TaskController {

  static get inject () { 
    return ['App/Model/Task', 'App/Model/User']  
  }

  constructor (Task, User) { 
    this.Task = Task
    this.User = User
  }

  * index(request, response) {
    const tasks = yield this.Task.all() 
    yield response.sendView('tasks.index', { tasks: tasks.toJSON() })
  }

  * create(request, response) {
    const isLoggedIn = yield request.auth.check()

    if (!isLoggedIn) {
      response.redirect('/login')
    }

    yield response.sendView('tasks.create')
  }

  * store(request, response) {
    const isLoggedIn = yield request.auth.check()

    if (!isLoggedIn) {
      response.redirect('/login')
    }

    let task = request.only('title', 'description');

    const newTask = new this.Task({
      title: task.title,
      description: task.description,
      user_id: request.currentUser.id
    })

    yield newTask.save()

    response.redirect('/tasks')
  }

  * show(request, response) {
    const task = yield this.Task.find(request.param('id'))

    const owner = yield this.User.find(task.user_id)

    if (task) {
      yield response.sendView('tasks.show', { task: task.toJSON(), owner })
      return
    }

    response.send('Sorry, cannot find the selected found')
  }

  * destroy(request, response) {
    const isLoggedIn = yield request.auth.check()

    if (!isLoggedIn) {
      response.redirect('/login')
    }

    const task = yield this.Task.findBy('id', request.param('id'))
    yield task.delete()
  }

}

module.exports = TaskController  

This task controller contains all the logic for creating, reading and deleting tasks. We also include some logic for access control using authorization. For instance, we only want authenticated users to be able to create tasks. We ran some commands that generated a user model and authentication logic, but we have not fully set it up. We'll do that in the next section.

Authentication continued

Please note that there is an Adonis blueprint that sets up authentication scaffolding out of the box.

Additionally, Prosper's Auth0 blog post covers more details on Adonis authentication

First, let's define some routes for registering, logging in, showing the forms and logging out:

Route.get('/login', 'AuthController.showLogin')  
Route.post('/login', 'AuthController.login')

Route.get('/register', 'AuthController.showRegister')  
Route.post('register', 'AuthController.register')

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

Next, we need to set up a controller to handle these routes:

$ ./ace make:controller Auth 

Then, we need to define the controller to render views and take database actions:

'use strict'

const User = use('App/Model/User')  
const Hash = use('Hash')

class AuthController {

  * showLogin(request, response) {
    yield response.sendView('auth.login')
  }

  * login(request, response) {
    const email = request.input('email')
        const password = request.input('password')

        const attemptUser = yield User.findByOrFail('email', email)

        // Attempt to login with email and password
        const authCheck = yield request.auth.login(attemptUser)
        if (authCheck) {
            return response.redirect('/')
        }

        yield response.sendView('auth.login')
  }

  * showRegister(request, response) {
    yield response.sendView('auth.register')
  }

  * register(request, response) {
    const user = new User()
        user.username = request.input('username')
        user.email = request.input('email')
        user.password = yield Hash.make(request.input('password'))

        yield user.save()

        yield response.sendView('auth.register')

  }

    * logout(request, response) {
        yield request.auth.logout()

        return response.redirect('/')
    }
}

module.exports = AuthController  

Now, we can define the views for registering and logging in users:

resources/views/auth/login.njk

{% extends 'master' %}

{% block content %}
  <h1>Login</h1>

    <form method="POST" action="/login">

      {{ csrfField }}


      <input type='email' placeholder='Email' name='email' class='input'>

      <input type='password' placeholder='Password' name='password' class='input'>

      {% if error %}
      <div class="notification is-danger">
      {{ error }}
    </div>
    {% endif %}

      <input type='submit' value='Submit'>

  </form>
{% endblock %}

Adonis provides a special Cross-Site Request Forgery (CSRF) field for submitting forms so that malicious attackers cannot hijack requests. This helper checks that the request comes from the right people in the right places. To learn more about CSRF, please consult the documentation.

resources/views/auth/register.njk

{% extends 'master' %}

{% block content %}
  <h1>Register</h1>

  <form method="POST" action="/register">

    {{ csrfField }}
    <input type='text' placeholder='Username' name='username' class='input'>

    <input type='email' placeholder='Email' name='email' class='input'>

    <input type='password' placeholder='Password' name='password' class='input'>

    <input type='submit' value='Submit'>

  </form>
{% endblock %}

Task views

Now that we have all of our routes, controllers, and authentication set up, we can add the views for showcasing tasks:

resources/views/tasks/create.njk

{% extends 'master' %}

{% block content %}
  <div class="content">
    <h1>Create task</h1>

    <form method='POST' action='/tasks'>
      {{ csrfField }}
      <input type='text' name='title' placeholder='Title of task' class='input'>

      <textarea name='description' placeholder='Description of task' class='input'></textarea>

      <input type='submit' value='Submit'>
    </form>

  </div>
{% endblock %}

resources/views/tasks/index.njk

{% extends 'master' %}

{% block content %}
  <div>
    <h1 class='title'>Tasks</h1>

    {% for task in tasks %} 
      <div class="content">
        <h2>
          {{ linkTo('tasks.show', task.title, { id: task.id }, '_blank') }}
        </h2> 
        <p> {{ task.description }} </p>
      </div>
    {% endfor %}
  </div>
{% endblock %}

resources/views/tasks/show.njk

{% extends 'master' %}

{% block content %}
  <div class="content">
    <h1>{{ task.title }}</h1>

    <p> {{ task.description }} </p>

    <p>task created by: <b>{{ owner.username }}</b></p>
  </div>
{% endblock %}

Conclusion

In this tutorial, we've set up a Node.js server that talks to a MySQL database. We can create, delete and showcase tasks. Additionally, we can register and authorize users and define access control based on the current user's authentication status.

Protect Your Node.js App

Adonis.js is a powerful framework for building Node.js applications. While it is not as popular as other Node.js web servers like Koa and Express, Adonis provides more power within the confines of an opinionated tech stack.

Check out the source code on GitHub and definitely consider Adonis for future Node projects!