Ali
Ali

Reputation: 5121

Adonis.js RESTFUL API Workaround

I recently started developing an application on adonisjs framework. I had an option to use expressjs but I preferred adonisjs because I love the way it is structured (mostly the laravel style).

I am currently trying to build a RESTFUL API but unable to figure out the basic routing / middleware / apiController (my custom controller to handle all api requests) scenario.

Here's what I've done so far:

routes.js

Route.post('api/v1/login', 'ApiController.login')
Route.post('api/v1/register', 'ApiController.register')

// API Routes
Route.group('api', function() {

  Route.get('users', 'ApiController.getUsers')

}).prefix('/api/v1').middlewares(['auth:api'])

ApiController.js

'use strict'

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

const FAIL = 0
const SUCCESS = 1

class ApiController {

  * login (request, response) {

    let jsonResponse = {}

    const email = request.input('email')
    const password = request.input('password')

    // validate form input
    const rules = {
      email: 'required|email',
      password: 'required'
    }

    const messages = {
      'email.required': 'Email field is required.',
      'password.required': 'Password field is required.'
    }

    const validation = yield Validator.validateAll(request.all(), rules, messages)

    if (validation.fails()) {

      jsonResponse.status = FAIL
      jsonResponse.response = {}
      jsonResponse.response.message = validation.messages()[0].message

    } else {

      try {

        yield request.auth.attempt(email, password)

        const user = yield User.findBy('email', email)

        const token = yield request.auth.generate(user)

        jsonResponse.status = SUCCESS
        jsonResponse.response = {}
        jsonResponse.response.message = "Logged In Successfully"
        jsonResponse.response.user = user
        jsonResponse.response.token = token

      } catch (e) {

        jsonResponse.status = FAIL
        jsonResponse.response = {}
        jsonResponse.response.message = e.message

      }

    }

    return response.json(jsonResponse)

  }

}

module.exports = ApiController

config/auth.js

'use strict'

const Config = use('Config')

module.exports = {

  /*
  |--------------------------------------------------------------------------
  | Authenticator
  |--------------------------------------------------------------------------
  |
  | Authenticator is a combination of HTTP Authentication scheme and the
  | serializer to be used for retrieving users. Below is the default
  | authenticator to be used for every request.
  |
  | Available Schemes - basic, session, jwt, api
  | Available Serializers - Lucid, Database
  |
  */
  authenticator: 'session',

  /*
  |--------------------------------------------------------------------------
  | Session Authenticator
  |--------------------------------------------------------------------------
  |
  | Session authenticator will make use of sessions to maintain the login
  | state for a given user.
  |
  */
  session: {
    serializer: 'Lucid',
    model: 'App/Model/User',
    scheme: 'session',
    uid: 'email',
    password: 'password'
  },

  /*
  |--------------------------------------------------------------------------
  | Basic Auth Authenticator
  |--------------------------------------------------------------------------
  |
  | Basic Authentication works on Http Basic auth header.
  |
  */
  basic: {
    serializer: 'Lucid',
    model: 'App/Model/User',
    scheme: 'basic',
    uid: 'email',
    password: 'password'
  },

  /*
  |--------------------------------------------------------------------------
  | JWT Authenticator
  |--------------------------------------------------------------------------
  |
  | Jwt authentication works with a payload sent with every request under
  | Http Authorization header.
  |
  */
  jwt: {
    serializer: 'Lucid',
    model: 'App/Model/User',
    scheme: 'jwt',
    uid: 'email',
    password: 'password',
    secret: Config.get('app.appKey')
  },

  /*
  |--------------------------------------------------------------------------
  | API Authenticator
  |--------------------------------------------------------------------------
  |
  | Api authenticator authenticates are requests based on Authorization
  | header.
  |
  | Make sure to define relationships on User and Token model as defined
  | in documentation
  |
  */
  api: {
    serializer: 'Lucid',
    model: 'App/Model/Token',
    scheme: 'api'
  }

}

config/shield.js

'use strict'

module.exports = {
  /*
  |--------------------------------------------------------------------------
  | Content Security Policy
  |--------------------------------------------------------------------------
  |
  | Content security policy filters out the origins not allowed to execute
  | and load resources like scripts, styles and fonts. There are wide
  | variety of options to choose from.
  | @examples
  | directives: {
  |   defaultSrc: ['self', '@nonce', 'cdnjs.cloudflare.com']
  | }
  */
  csp: {
    directives: {
    },
    reportOnly: false,
    setAllHeaders: false,
    disableAndroid: true
  },

  /*
  |--------------------------------------------------------------------------
  | X-XSS-Protection
  |--------------------------------------------------------------------------
  |
  | X-XSS Protection saves from applications from XSS attacks. It is adopted
  | by IE and later followed by some other browsers.
  |
  */
  xss: {
    enabled: true,
    enableOnOldIE: false
  },

  /*
  |--------------------------------------------------------------------------
  | Iframe Options
  |--------------------------------------------------------------------------
  |
  | xframe defines whether or not your website can be embedded inside an
  | iframe. Choose from one of the following options.
  | @available options
  | DENY, SAMEORIGIN, ALLOW-FROM http://example.com
  */
  xframe: 'DENY',

  /*
  |--------------------------------------------------------------------------
  | No Sniff
  |--------------------------------------------------------------------------
  |
  | Browsers have a habit of sniffing content-type of a response. Which means
  | files with .txt extension containing Javascript code will be executed as
  | Javascript. You can disable this behavior by setting nosniff to false.
  |
  */
  nosniff: true,

  /*
  |--------------------------------------------------------------------------
  | No Open
  |--------------------------------------------------------------------------
  |
  | IE users can execute webpages in the context of your website, which is
  | a serious security risk. Below options will manage this for you.
  |
  */
  noopen: true,

  /*
  |--------------------------------------------------------------------------
  | CSRF Protection
  |--------------------------------------------------------------------------
  |
  | CSRF Protection adds another layer of security by making sure, actionable
  | routes does have a valid token to execute an action.
  |
  */
  csrf: {
    enable: true,
    methods: ['POST', 'PUT', 'DELETE'],
    filterUris: ['/api/v1/login', '/api/v1/register'],
    compareHostAndOrigin: true
  }

}

Now when i hit login web service (using postman). It validates the user but throws an exception at const token = request.auth.generate(user) and says request.auth.generate is not a function.

I don't know what is going on. Please help.

Thanks

Upvotes: 1

Views: 3112

Answers (1)

Qadir Hussain
Qadir Hussain

Reputation: 8856

You need to generate a JWT token (when user calls a login api call) and send it back so that the app which requested the login service can store it and use it to make future requests (Using "Authorization" header with value "Bearer [JWT Token String]"). When app sends another request i.e. get list of business categories (with JWT token in its header), we will validate that request in api middleware. Once the request is validated, we'll serve the request and send back the data in json format.

Here's what your header looks like:

image

And this is what you actually need to do in your code:

// ROUTES.JS

// API Routes
Route.post('/api/v1/register', 'ApiController.register')
Route.post('/api/v1/login', 'ApiController.login')

Route.group('api', function() {

  Route.get('/business_categories', 'ApiController.business_categories')

}).prefix('/api/v1').middlewares(['api'])

// API.JS (Middleware)

'use strict'

class Api {

  * handle (request, response, next) {

    // here goes your middleware logic
    const authenticator = request.auth.authenticator('jwt')
    const isLoggedIn = yield authenticator.check()

    if (!isLoggedIn) {
      return response.json({
        status: 0,
        response: {
          message: 'API Authentication Failed.'
        }
      })
    }

    // yield next to pass the request to next middleware or controller
    yield next

  }

}

module.exports = Api

// APICONTROLLER.JS

'use strict'

// Dependencies
const Env = use('Env')
const Validator = use('Validator')
const Config = use('Config')
const Database = use('Database')
const Helpers = use('Helpers')
const RandomString = use('randomstring')
const Email = use('emailjs')
const View = use('View')

// Models
const User = use('App/Model/User')
const UserProfile = use('App/Model/UserProfile')
const DesignCenter = use('App/Model/DesignCenter')
const Settings = use('App/Model/Setting')

// Properties
const FAIL      = 0
const SUCCESS   = 1
const SITE_URL  = "http://"+Env.get('HOST')+":"+Env.get('PORT')

// Messages
const MSG_API_AUTH_FAILED             = 'Api Authentication Failed.'
const MSG_REGISTERED_SUCCESS          = 'Registered Successfully.'
const MSG_LOGGED_IN_SUCCESS           = 'Logged In Successfully.'
const MSG_LOGGED_IN_CHECK             = 'You Are Logged In.'
const MSG_LOGGED_IN_FAIL              = 'Invalid Credentials.'
const MSG_FORGOT_PASS_EMAIL_SUCCESS   = 'Your password reset email has been sent. Please check your inbox to continue.'

class ApiController {

  * register (request, response) {

    let jsonResponse = {}

    // validate form input
    const validation = yield Validator.validateAll(request.all(), Config.get('validation.api.register.rules'), Config.get('validation.api.register.messages'))

    // show error messages upon validation fail
    if (validation.fails()) {

      jsonResponse.status = FAIL
      jsonResponse.response = {}
      jsonResponse.response.message = validation.messages()[0].message

    } else {

      // handle card image
      let card_image = null

      if ( request.file('card_image') ) {

        const image = request.file('card_image', {
          allowedExtensions: ['jpg', 'png', 'jpeg']
        })

        if (image.clientSize() > 0) {
          const filename = RandomString.generate({length: 30, capitalization: 'lowercase'}) + '.' + image.extension()
          yield image.move(Helpers.publicPath(Config.get('constants.user_card_img_upload_path')), filename)

          if (!image.moved()) {
            jsonResponse.status = FAIL
            jsonResponse.response = {}
            jsonResponse.response.message = image.errors()
            return response.json(jsonResponse)
          }

          // set value for DB
          card_image = filename
        }

      }

      // create user
      const user = yield User.create({
        username: new Date().getTime(),
        email: request.input('email'),
        password: request.input('password')
      })

      // create user profile
      const user_profile = yield UserProfile.create({
        user_id: user.id,
        user_type_id: 3, // designer
        first_name: request.input('first_name'),
        last_name: request.input('last_name'),
        business_name: request.input('business_name'),
        business_category_id: request.input('business_category'),
        card_image: card_image,
        phone: request.input('mobile_no'),
        is_active: 1
      })

      jsonResponse.status = SUCCESS
      jsonResponse.response = {}
      jsonResponse.response.message = MSG_REGISTERED_SUCCESS
      jsonResponse.response.user = {
        'id': user.id,
        'first_name': user_profile.first_name,
        'last_name': user_profile.last_name,
        'business_name': user_profile.business_name,
        'business_category_id': user_profile.business_category_id,
        'card_image': user_profile.card_image == null ? "" : SITE_URL + "/" + Config.get('constants.user_card_img_upload_path') + "/" + user_profile.card_image,
        'mobile_no': user_profile.phone
      }

    }

    return response.json(jsonResponse)

  }

  * login (request, response) {

    let jsonResponse = {}

    const email = request.input('email')
    const password = request.input('password')

    // validate form input
    const validation = yield Validator.validateAll(request.all(), Config.get('validation.api.login.rules'), Config.get('validation.api.login.messages'))

    if (validation.fails()) {

      jsonResponse.status = FAIL
      jsonResponse.response = {}
      jsonResponse.response.message = validation.messages()[0].message

    } else {

      try {
        const jwt = request.auth.authenticator('jwt')
        const token = yield jwt.attempt(email, password)
        const user = yield User.findBy('email', email)
        const user_profile = yield UserProfile.findBy('user_id', user.id)

        // check if user type is designer
        if ( user_profile.user_type_id == 3 ) {

          jsonResponse.status = SUCCESS
          jsonResponse.response = {}
          jsonResponse.response.message = MSG_LOGGED_IN_SUCCESS

          let card_image = null
          if (user_profile.card_image) {
            card_image = SITE_URL + "/" + Config.get('constants.user_card_img_upload_path') + "/" + user_profile.card_image
          }

          jsonResponse.response.user = {
            'id': user.id,
            'first_name': user_profile.first_name,
            'last_name': user_profile.last_name,
            'business_name': user_profile.business_name,
            'business_category_id': user_profile.business_category_id,
            'card_image': card_image,
            'mobile_no': user_profile.phone
          }
          jsonResponse.response.token = token

        } else {

          jsonResponse.status = FAIL
          jsonResponse.response = {}
          jsonResponse.response.message = MSG_LOGGED_IN_FAIL

        }
      } catch (e) {
        jsonResponse.status = FAIL
        jsonResponse.response = {}
        jsonResponse.response.message = e.message
      }

    }

    return response.json(jsonResponse)

  }

  * business_categories (request, response) {

    let jsonResponse = {}

    try {

      jsonResponse.status = SUCCESS
      const dbRecords = yield Database.select('id', 'name').from('business_categories')
      let records = []

      dbRecords.forEach(function(row) {
        records.push({
          id: row.id,
          name: row.name
        })
      })

      jsonResponse.response = records

    } catch (e) {

      jsonResponse.status = FAIL
      jsonResponse.response = {}
      jsonResponse.response.message = e.message

    }

    response.json(jsonResponse)

}

module.exports = ApiController

// CONFIG / AUTH.JS

Since JWT tokens remain valid unless they are expired or deleted (from app, when forcefully logging a user out). We can also set an expiry period as follows:

jwt: {
    serializer: 'Lucid',
    model: 'App/Model/User',
    scheme: 'jwt',
    uid: 'email',
    password: 'password',
    secret: Config.get('app.appKey'),
    options: {
        // Options to be used while generating token
        expiresIn: Ms('3m') // 3 months
    }
}

// CONFIG / SHIELD.JS

Since most of the api services are not able to send CSRF tokens while sending a POST request, you can exclude those api paths not to be checked for CSRF tokens here in this file as follow:

csrf: {
    enable: true,
    methods: ['POST', 'PUT', 'DELETE'],
    filterUris: [
        '/api/v1/login',
        '/api/v1/register'
    ],
    compareHostAndOrigin: true
}

Hope this helps :)

Upvotes: 2

Related Questions