DEV Community

Cover image for AdonisJs - Understanding User Registration and Authentication
Ted Ngeene
Ted Ngeene

Posted on

AdonisJs - Understanding User Registration and Authentication

In this third installment of the Everything, you need to know about AdonisJs series. we'll go over the basic setup of database models, using the user model. We'll also configure our registration and login controllers(authentication). Finally, I'll show you how to handle routing for endpoints.
This article will also briefly introduce you to basic lifecycle hooks in Adonis. Let's dive in.

Definitions

Authentication is the process of verifying who a user is, for example by making them enter a password.

If you're not familiar with the concept of database models, the following description succinctly defines it.

A database model is a type of data model that determines the logical structure of a database. It fundamentally determines in which manner data can be stored, organized and manipulated. The most popular example of a database model is the relational model, which uses a table-based format.

The model is essentially the data that will be manipulated in the system, it has attributes and relationships with other models.

Routes allow us to make HTTP requests to our application. The entry point for all Adonis routes is located in the start/routes.ts file. You can define all routes in this file or other files and import them into this file as we will do. For more detailed info on Adonis routes, head over to their official documentation.

HTTP methods

In most cases, you'll need your application to perform some business logic. This is where HTTP methods come in, these allow us to perform some actions on our models. Common HTTP methods include.

  • GET - Used to fetch data from a specified resource.
  • POST - Used to store new data or dispatch data to the server.
  • PUT/PATCH - Used to update existing data.
  • DELETE - Used to delete existing data.

Finally, controllers are files that all logic on the program that will be performed. A controller determines what response to send back to a user when a user makes a browser request. For example, we can have an authController that will handle all authentication logic.

Routes are tied down to controller functions. They are URL patterns that are tied down to a handler function, in this case, a function in a controller. Using the example above, we could have a login route mapping to a function in the auth controller.

From the above definitions, it's pretty clear that we're covering the MC in the MVC pattern, that is, the model and the controller.

Now we can actually get our hands dirty in setting up the user model.

Setting up the user model

A cool thing with Adonis is that it has a neat package called adonisjs/auth which handles authentication. It leverages the fully-fledged in-built authentication system of Adonis.
We'll begin by installing the package; npm i @adonisjs/auth

After successful installation, as earlier mentioned on the configuration of packages, we configure the package settings by running node ace configure @adonis/auth
This will lead the cli to prompt some questions. For my configuration, I followed the steps below.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7845wcwrog9lvgjxrwal.png

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yis97sht63p3e7sidwvu.png

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dz2ijdqow7b8o0g61pmu.png

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b6uwq9fotbwuzxupnv2p.png

If the configuration was successful, you'll notice that some new files will be added to the file tree of your application.

These are the user migrations and user model files. The package creates a basic user model which we can modify depending on the use case.
You'll also notice that for this particular configuration, since I decided to use API token guard, then a separate migration file for API tokens has also been created.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i2ivmm2jel4e5p60w0qu.png

The configuration for the auth package is stored inside the config/auth.ts file. Inside this file, you can define one or more guards to authenticate users. If you'd like to use a combination of API tokens and sessions, for example, all you'd need to invoke the node ace configure @adonisjs/auth command and add the guard you'd like to use. This will then append to the config/auth.ts file.

Modifying the user migration

Every application's user model is different. The basic model provided makes a general assumption of the common user attributes of most systems, however, to modify it to our use-case, we need to open the database/migrations/....users.ts file. Don't mind the digits.

For our application, the user table will need to look like this


import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class UsersSchema extends BaseSchema {
  protected tableName = 'users'

  public async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id').primary()
      table.string('email', 255).notNullable()
      table.string('username', 255).notNullable().unique()
      table.string('avatar').nullable()
      table.dateTime('email_verified_at').nullable()
      table.boolean('is_activated').notNullable().defaultTo(false)
      table.string('password', 180).notNullable()
      table.string('remember_me_token').nullable()

      /**
       * Uses timestampz for PostgreSQL and DATETIME2 for MSSQL
       */
      table.timestamp('created_at', { useTz: true }).notNullable()
      table.timestamp('updated_at', { useTz: true }).notNullable()

      table.index(['id', 'username'])
    })
  }

  public async down() {
    this.schema.dropTable(this.tableName)
  }
}

Enter fullscreen mode Exit fullscreen mode

From the configuration above, we can see which fields we'll need our users to have. Over and above their inputs, we'll require that users verify their accounts. This will prevent bots from using our system. The implementation for this will be covered in the next section.

We'll also need to index some fields, which adonis provides. All we have to do is indicate which fields we'd like to be indexed.
For those of you not familiar with the concept of database indexing, head over to this definition.

Finally, it's time to migrate the data

node ace migration:run

Enter fullscreen mode Exit fullscreen mode

If you got a successful migration, you'll see this on the command line.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mrt44o9feww1o0jl1n79.png

It's worth noting that you might experience an error in migration related to phc-argon not being installed as a dependency. If you come across this, install this library by running npm i phc-argon2, then try running the migration again.

Modify the user model

In most cases, we'll have separate model files for each table in our database. These model files describe the columns to lucid. They also contain relationship definitions, lifecycle hooks, computed properties, serialization behavior, and query scopes. We'll dig into this at a later time.

Under the app/models directory, open the User.ts. We'll adjust it to this format.


import { DateTime } from 'luxon'
import Hash from '@ioc:Adonis/Core/Hash'
import { column, beforeSave, BaseModel } from '@ioc:Adonis/Lucid/Orm'

export default class User extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column()
  public email: string

  @column()
  public username: string

  @column()
  public avatar: string

  @column()
  public isActivated: boolean = false

  @column.dateTime()
  public email_verified_at: DateTime

  @column({ serializeAs: null })
  public password: string

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime

  @beforeSave()
  public static async hashPassword(user: User) {
    if (user.$dirty.password) {
      user.password = await Hash.make(user.password)
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

The code above is quite self-explanatory; it defines all the fields that we'd need our user model to have. However, at this point, I'd like to mention on the last bit

@beforeSave()
  public static async hashPassword(user: User) {
    if (user.$dirty.password) {
      user.password = await Hash.make(user.password)
    }
  }

Enter fullscreen mode Exit fullscreen mode

This is a brief introduction to adonis lifecycle hooks. What this hook does is essentially encrypt user passwords using a hashing algorithm. This operation is performed right before a user is saved into the database, hence the beforeSave() function. We wouldn't want to store user passwords as raw texts. You can perform other lifecycle operations using any of these hooks in adonis

beforeSave(), beforeCreate(), beforeUpdate(), beforeDestroy(), beforeFind(), afterFind(),beforeFetch(), afterFetch(), beforePaginate(), afterPaginate()

Enter fullscreen mode Exit fullscreen mode

Creating our Auth Controller

For the next step, we will make a controller that will handle all user authentication. We do this by running

node ace make:controller Users/AuthController

Enter fullscreen mode Exit fullscreen mode

You'll notice that a new directory has been created under the app/Controllers/Http.
Open up the AuthController file and paste the following code.


import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
import { rules, schema } from '@ioc:Adonis/Core/Validator'

export default class AuthController {
  public async register({ request, response }: HttpContextContract) {
    // validate email
    const validations = await schema.create({
      email: schema.string({}, [rules.email(), rules.unique({ table: 'users', column: 'email' })]),
      password: schema.string({}, [rules.confirmed()]),
      username: schema.string({}, [rules.unique({ table: 'users', column: 'username' })]),
    })
    const data = await request.validate({ schema: validations })
    const user = await User.create(data)
    return response.created(user)
  }

  //   login function
  public async login({ request, response, auth }: HttpContextContract) {
    const password = await request.input('password')
    const email = await request.input('email')

    try {
      const token = await auth.use('api').attempt(email, password, {
        expiresIn: '24hours',
      })
      return token.toJSON()
    } catch {
      return response
        .status(400)
        .send({ error: { message: 'User with provided credentials could not be found' } })
    }
  }

  //   logout function
  public async logout({ auth, response }: HttpContextContract) {
    await auth.logout()
    return response.status(200)
  }
}

Enter fullscreen mode Exit fullscreen mode

So what does the code above do?

Registration

There are three functions within this controller;
The first one being the registration of users.

We have some validators that ensure that the data being input meets certain requirements, in our case, the email and username fields should be unique. The password field should also be entered twice, that is, have a password confirmation field that matches the password.
If the user input meets the set validations, then the system creates a record of the user in the database.

Login

The login functionality of our application will handle the authorization of users. We'll require that users enter an email and password. If the two match against a user in the database, then we return an API token that gives the user access to our system.
This token will validate all requests from the user and will only be valid for 24 hours.
In a case where the user enters the wrong credentials, the system will throw an error with an appropriate response message.

Logout

Finally, we will need users to also be able to log out when they need to. The logout() function helps us achieve this.

Defining User Routes

Next, navigate to the start directory and create a new directory called routes, under it make a file named users.ts . Therefore your start directory should be looking like start/routes/users.ts. Paste the following;


import Route from '@ioc:Adonis/Core/Route'

Route.group(() => {
  // registration logic
  Route.post('register', 'Users/AuthController.register').as('register')
  Route.post('login', 'Users/AuthController.login').as('login')
  Route.post('logout', 'Users/AuthController.logout').as('logout')
}).prefix('api/v1/users/')

Enter fullscreen mode Exit fullscreen mode

The above defines the user-related URLs that our application will be having.

The prefix keyword means that all URLs within the Route group will be prepended with the api/v1/users pattern.

For now, all the routes use POST requests, but not to worry, we'll see how to use other HTTP methods in upcoming articles.

I'll take a dive into its functionality, but before then we need to inject the user routes into the entry point of all routes for our application. This is the start/routes.ts file.

Open the file and modify it such that its contents are like this;


import HealthCheck from '@ioc:Adonis/Core/HealthCheck'
import Route from '@ioc:Adonis/Core/Route'

import './routes/users.ts'

// check db connection
Route.get('health', async ({ response }) => {
  const report = await HealthCheck.getReport()

  return report.healthy ? response.ok(report) : response.badRequest(report)
})

Enter fullscreen mode Exit fullscreen mode

Testing

We will be using postman for testing, for my setup, I've made a collection and added a global URL variable called BASE_DEV_API_URL, which is basically, http://localhost:3333/api
Next, I've added the three requests we've just created above and tested them out. We'll cover different scenarios, using different inputs.

  1. User registration

Successful registration

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iavos50wjt1h5wtt78dh.png

Unique fail for email and username

This error will occur when an email and username fail to meet the uniqueness validator.

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dh5lb5aiiaoi3e1h43br.png

Password Confirmation Missing

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2nnxwod7w2gdxshu2vt.png

  1. Login

Successful Login

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ook3f26vzcoua3z9mjz4.png

Wrong login credentials

https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zebwtk1c8513e9st9xy8.png

  1. Logout For the logout functionality, copy the bearer token of a logged in user and pass it as an authorization type of Bearer token under the authorization tab in Postman. Hit the http:/localhost:3333/api/v1/users/logout url. The result, if successful should be a status 200.

tngeene adonis logout

Conclusion.

Congratulations! You've made it to the end of the tutorial. I hope you're following along just fine. In this article, we've learnt to setup an authentication scheme in Adonis, gotten introduced to controllers and validators and finally, done some basic HTTP routing.

In case of any query, feel free to shoot a DM or comment on the post below.

All the source code of the above application can be found here

For the next part of the series, we'll cover Relationships, by setting up more models. See you on the next piece!

Discussion (6)

Collapse
olarcher profile image
Olga • Edited

Thank you so much for this series of articles on Adonis! Today I made my first authentication :)

The only thing that didn't work for me. Command "node ace configure @ adonis/auth" returned error "Cannot invoke instructions. Missing package "@ adonis/auth". Although the package had been installed. Instead configure command I used "node ace invoke @ adonis/auth" and after this everything worked the way you described.

Looking forward to reading your next articles!

Collapse
adonis profile image
Adis Durakovic

lmao, you just tagged me

Collapse
olarcher profile image
Olga

Sorry for disturbing! Your username happened to be adonis. After publishing the comment I've edited it by adding spaces, but Dev's notification system works really fast. Sorry :)

Thread Thread
adonis profile image
Adis Durakovic

it's all good 😅👍

Collapse
tngeene profile image
Ted Ngeene Author

Thanks for the feedback Olga 😄.
I'm definitely working on the next post. Is there something specific you'd like me to cover in my next piece?

Collapse
olarcher profile image
Olga

Right now no. Just keep going according to your plan. I'll ask if anything comes up along the way. Thank you once again!