Introduction

Welcome back once again. Yesterday we wrote a lot of code while building our first API in ES5. Today we're going to finish it up then quickly move over to writing our alternate api in ES8. Let's get started.

Getting Started

First, let's open utility.js in our ES5 project and write it out.

var config = require('../config')
var jwt = require('jsonwebtoken')
var User = require('../model/Model').User

exports.generateJwt = function (userId) {
  var payload = {id: userId}
  var opts = {expiresIn: '1h'}
  return jwt.sign(payload, config.secret, opts)
}

function getRandomInt (min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min
}

exports.getRandomInt = getRandomInt

exports.generateJwt = function (payloadInfo) {
  var payload = payloadInfo
  var opts = {expiresIn: '1h'}
  return jwt.sign(payload, config.secret, opts)
}

exports.uid = function (len) {
  var buf = []
  var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
  var charlen = chars.length

  for (var i = 0; i < len; ++i) {
    buf.push(chars[getRandomInt(0, charlen - 1)])
  }

  return buf.join('')
}

Here, we have some utility functions written that we'll be seeing again shortly. Time is short so let's move on to clientAuth.js.

/*
* Passport configuration happens here.
*/
var passport = require('passport')
var ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy
var Client = require('../model/Model').Client

/*
* Authenticate a client using the Client/Password strategy and passing the
* login info in the request body.
*/
passport.use('clientPassword', new ClientPasswordStrategy(function (clientId, clientSecret, done) {
  // find the client by their id
  Client.findOne({clientId: clientId}, function (err, client) {
    // handle errors
    if (err) return done(err)
    if (!client) return done(null, false)
        // make sure the client is TRUSTED
    if (!client.trustedClient) return done(null, false)
        // if the clientSecrets match, return done
    if (client.clientSecret == clientSecret) return done(null, client)
    else return done(null, false)
  })
}))

Now we have our Client Password strategy defined for use with Passport. Time to write our last module, oauth.js.

/*
* OAuth2orize token exchanges are defined here.
* In this case, you are using the Password Exchange Flow
* and the Refresh Token Exchange Flow
*/

// load modules
var oauth2orize = require('oauth2orize')
var Client = require('../model/Model').Client
var User = require('../model/Model').User
var jwt = require('jsonwebtoken')
var crypto = require('crypto')
var fs = require('fs')
var passport = require('passport')
var bcrypt = require('bcrypt-nodejs')
var utility = require('../services/utility')
var config = require('../config')

/*
*   Config OAuth2orize
*/
// create OAuth 2.0 server
var server = oauth2orize.createServer()

// Password exchange flow
server.exchange(oauth2orize.exchange.password(function (client, username, password, scope) {
  // generate refresh token
  var refreshToken = utility.uid(256)
  // encrypt the refreshToken
  var refreshTokenHash = crypto.createHash('sha1').update(refreshToken).digest('hex')

  // find user by email
  User.findOne({username: username}, function (err, user) {
    if (!user) {
      return false
    } else {
      // password check
      bcrypt.compare(password, user.password, function(err, result) {
        if (err) {
          return false
        } else {
          var payload = {
            name: user.username,
            userId: user.id,
            sub: user.username,
            aud: 'todo-list',
            issuer: 'todo-list',
            role: user.role,
            clientId: client.clientId
          }
          // create jwt
          jwt.sign(payload, config.secret, {expiresIn: '1h'}, function (err, jwt) {
            if (err) {
              return false
            } else {
              return jwt
            }
          })
        }
      })
    }
  })
}))

// token endpoint
exports.token = [
  passport.authenticate(['clientBasic', 'clientPassword'], { session: false }),
  server.token(),
  server.errorHandler()
]

exports.server = server

Now we have set up our OAuth2orize server. Take particular note of the callback mess going on here. Let's be completely honest, if it weren't for the comments, this would be a nightmare to read and debug. Especially when we're tired after a long day or night, but don't worry... we'll be fixing this shortly.

So now we've finished our ES5 api. We could run this but for now, our focus is on the language, not the api. You can run the service in your own time. However, it's time to begin writing our ES8 alternative. So let's transition to the ES8 directory, open app.js, and begin writing.

// import core modules
const Promise = require('bluebird')
const koa = require('koa')
const mongoose = Promise.promisifyAll(require('mongoose'))
const config = require('./config')

// import authentication modules
const passport = require('koa-passport')
const jwt = require('koa-jwt')
require('./services/clientAuth')
const authServer = require('./services/oauth').server

// import middleware config
const bodyParser = require('koa-bodyParser')
const convert = require('koa-convert')
const res = require('koa-res')
const router = require('koa-simple-router')
const cors = require('kcors')

// import models
const Client = require('./model/Model').Client

// import controllers
const task = require('./controller/task')
const user = require('./controller/user')

// initialize koa server
const app = new koa()

/*
  function that checks if a Client exists in the database for the dashboard
*/
const clientCheck = async () => {
  const client = await Client.findOneAsync({name: 'ToDo Dashboard'})
  if (!client) {
    console.log('no ToDo Dashboard client found, creating new one')
    const newClient = await Client.create({
      name: 'ToDo Dashboard',
      clientId: 'my-awesome-clientid',
      clientSecret: 'my-awesome-clientSecret',
      trustedClient: true
    })
    if(!newClient) {
      console.log('error creating dashboard client!')
    } else {
      console.log('new ToDo Dashboard Client created')
    }
  }
}

/*
  Mongoose Config
*/

mongoose.Promise = require('bluebird')
mongoose
.connect(config.mongoUrl)
.then(response => {
  console.log('connected to mongo :-)')
  clientCheck()
})
.catch(err => {
  console.log("Error connecting to Mongo")
  console.log(err)
})

/*
  Server Config
*/
// error handling
app.use(async (ctx, next) => {
  try {
    await next()
  } catch (err) {
    ctx.status = err.status || 500
    ctx.body = err.message
    ctx.app.emit('error', err, ctx)
  }
})

// initialize passport
app.use(passport.initialize())

// format response as JSON
app.use(convert(res()))

// cors
app.use(convert(cors()))

// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}`)
})

// body parser
app.use(bodyParser())

// unprotected router
app.use(router(_ => {
  _.post('/token',
    passport.authenticate('clientPassword', { session: false }),
    authServer.token(),
    authServer.errorHandler()),
  // create new user
  _.post('/user/new', user.createUser)
}))

// // jwt config, any routes after this will require a JWT to be accessed
app.use(jwt({secret: config.secret}))

// protected router
app.use(router(_ => {
  // get user by username
  _.get('/user/:username', user.getUser),
  // delete user by username
  _.delete('/user', user.deleteUser),
  // get user tasks
  _.get('/task/:username', task.getUserTasks),
  // create new task
  _.post('/task', task.createTask),
  // edit task
  _.put('/task', task.editTask),
  // delete task
  _.delete('/task/:username/:taskId', task.deleteTask)
}))

app.listen(3000)

Ok, now we have an app.js that largely mirrors our ES5 app.js. However, let's first take note of the use of const all over the place. This is just providing a little hint of stability as well as clarifying for us which variables are supposed to remain constant and which ones will in fact be subject to changes. Next, take note of how we are now passing async function into our middleware. This is because Koa is built to accept async functions instead of standard callbacks. Next, pay attention to the use of arrow functions all over and how they make our code drastically cleaner to read. We should also note our alternate logger function which uses a string template literal. Also take note of our error handler function which eliminates our need to use try/catches in any future middleware or routes that we write. However, this is thanks more to a Koa feature than ES8.

Now, before we finish, let's go ahead and write our Model.js file right quick.

const mongoose = require('mongoose')
const Schema = mongoose.Schema
const relationship = require('mongoose-relationship')

// USER Model
const UserSchema = mongoose.Schema({
    username: {type: String, required: true, unique: true},
    password: {type: String, required: true, bcrypt: true},
    tasks: [{type: Schema.ObjectId, ref: 'Task'}]
})

// TASK Model
const TaskSchema = mongoose.Schema({
    name: String,
    user: {type: Schema.ObjectId, ref: 'User', childPath: 'tasks'}
})

// CLIENT Model
let ClientSchema = new mongoose.Schema({
  name: { type: String, unique: true, required: true },
  clientId: { type: String, required: true },
  clientSecret: { type: String, required: true },
  trustedClient: { type: Boolean, required: true }
})

// Plugins
UserSchema.plugin(require('mongoose-unique-validator'))
UserSchema.plugin(require('mongoose-bcrypt'))
TaskSchema.plugin(relationship, {relationshipPathName: 'user'})
ClientSchema.plugin(require('mongoose-unique-validator'))

// Exports
exports.Task = mongoose.model('Task', TaskSchema)
exports.User = mongoose.model('User', UserSchema)
exports.Client = mongoose.model('Client', ClientSchema)

As you can see here, this is largely the same as the Model file of before. However, don't worry, we'll be seeing the full effects of ES8 shortly.

Summary

So far, we've managed to finish writing our ES5 api and we have now started on our ES8 alternative. We haven't gotten to really feel the serious differenes between the two just yet. That being said, we will be seeing the stark differences tomorrow when we begin writing our controllers and services.

Resources

Franzé Jr

Software Engineer with experience working in multi-cultural teams, Franze wants to help people when he can, and he is passionate about programming and Computer Science. Founder of RemoteMeetup.com where he can meet people all over the World. When Franze is not coding, he is studying something about programming.

  1. Comments for Finish the ES5 API and start building an ES8 API

You must login to comment

You May Also Like