trailsjs/trailpack-express

View on GitHub
lib/server.js

Summary

Maintainability
D
1 day
Test Coverage
/* eslint no-console: [0] */
'use strict'

const _ = require('lodash')
const path = require('path')
const session = require('express-session')
const consolidate = require('consolidate')
const expressBoom = require('express-boom')
const cors = require('cors')
const http = require('http')
const https = require('https')
const Joi = require('joi')
const errors = require('./errors')
const utils = require('./utils')

module.exports = {
  port: null,
  host: null,
  ssl: null,
  redirectToHttps: false,
  nativeServer: null,
  serverRoutes: {},
  serverPolicies: {},
  serverHandlers: {},
  webConfig: {},

  createServer(app) {
    const main = app.config.get('main')
    const sess = app.config.get('session')
    const express = app.config.get('web.express')

    const server = express()
    this.webConfig = _.cloneDeep(app.config.get('web'))
    this.port = this.webConfig.port
    this.portHttp = this.webConfig.portHttp
    this.host = this.webConfig.host
    this.ssl = this.webConfig.ssl
    this.externalConfig = this.webConfig.externalConfig
    this.cors = this.webConfig.cors
    this.redirectToHttps = this.webConfig.redirectToHttps || false

    if (main.paths && main.paths.www) {
      this.webConfig.middlewares.www = express.static(main.paths.www, {
        maxAge: this.webConfig.cache
      })
    }
    else {
      app.log.info('config.paths.www: No www directory is set, www middleware will not be loaded')
    }

    if (sess && sess.secret) {
      this.webConfig.middlewares.session = session(_.defaults({
        secret: sess.secret,
        store: sess.store,
        resave: true,
        saveUninitialized: false
      }, sess.options))
    }
    else {
      app.log.info('config.session.secret: No secret given so session are disabled')
    }

    if (!this.webConfig.middlewares.addMethods) {
      this.webConfig.middlewares.addMethods = (req, res, next) => {
        req.log = app.log
        req.trailsApp = app
        const accept = req.get('accept') || ''
        req.wantsJSON = accept.indexOf('json') !== -1
        res.serverError = err => {
          this.webConfig.middlewares['500'](err, req, res, next)
        }
        res.notFound = () => {
          this.webConfig.middlewares['404'](req, res, next)
        }
        res.forbidden = (msg) => {
          res.serverError({
            statusCode: 403,
            code: 'E_FORBIDDEN',
            message: msg || 'forbidden'
          })
        }

        next()
      }
    }

    return server
  },
  /**
   * Register middlewares
   * @param server express server
   * @param app Trails app
   */
  registerMiddlewares(app, server) {
    server.use(expressBoom())
    if (this.cors) {
      server.use(cors(this.cors === true ? {} : this.cors))
    }
    for (const index in this.webConfig.middlewares.order) {
      const middlewareName = this.webConfig.middlewares.order[index]
      const middleware = this.webConfig.middlewares[middlewareName]
      if (!middleware && middlewareName !== 'router') continue

      if (_.isArray(middleware)) {
        if (_.isString(middleware[0])) {
          server.use.apply(server, middleware)
        }
        else {
          for (const i in middleware) {
            const m = middleware[i]
            server.use(m)
          }
        }
      }
      else if (middlewareName === 'router') {
        this.registerRoutes(app, server)
      }
      else if (middleware) {
        server.use(middleware)
      }

    }
  },

  /**
   * Register template engines and views path
   * @param server express server
   * @param app Trails app
   */
  registerViews(app, server) {
    const viewEngine = app.config.get('views.engine') || null
    const viewEngines = app.config.get('web.views')

    if (!viewEngine && !viewEngines) {
      app.log.info('No view engine is set')
      return
    }

    if (viewEngines) {
      let defaultExt
      for (const ext in viewEngines.engines) {
        if (!defaultExt) {
          defaultExt = ext
        }
        server.engine(ext, consolidate[viewEngines.engines[ext]] ? consolidate[viewEngines.engines[ext]] : viewEngines.engines[ext])
      }

      if (defaultExt) {
        server.set('view engine', defaultExt)
      }

      server.set('views', path.join(process.cwd(), viewEngines.path))
    }
    else {
      server.engine('html', consolidate[viewEngine] ? consolidate[viewEngine] : viewEngine)
      server.set('view engine', 'html')
      server.set('views', path.join(process.cwd(), 'views'))
    }
  },

  /**
   * Register routes to express server
   * @param server express server
   * @param app Trails app
   */
  registerRoutes(app, server) {
    // Sort the routes so that they are always in the correct express order.
    const routes = app.routes.sort(utils.createSpecificityComparator({ order: 'asc' }))
    const express = app.config.get('web.express')
    const expressRouter = express.Router
    const router = expressRouter()
    utils.extendsExpressRouter(router)

    if (this.ssl && this.redirectToHttps) {
      router.all('*', (req, res, next) => {
        if (req.secure) {
          return next()
        }
        res.redirect(`https://${req.hostname}:${this.port + req.url}`)
      })
    }

    routes.forEach(route => {
      if (route.method === '*') route.method = 'ALL'

      if (route.method instanceof Array) {
        route.method.forEach(method => {
          this.serverRoutes[method.toLowerCase() + ' ' + route.path] = route
        })
      }
      else {
        this.serverRoutes[route.method.toLowerCase() + ' ' + route.path] = route
      }
    })

    _.each(this.serverRoutes, (route, path) => {

      const parts = path.split(' ')

      let methods = []

      // Handler is object configuration
      if (_.isPlainObject(route.handler)) {
        if (route.handler.directory && route.handler.directory.path) {
          router.use(parts[1], express.static(route.handler.directory.path))
        }
        else {
          app.log.warn(`${route.path} will be ignored because it doesn't have a correct handler configuration`)
        }
      }
      else {
        if (route.config) {
          if (route.config.validate && Object.keys(route.config.validate).length > 0) {

            // add validation
            const validation = utils.createJoiValidationRules(route)

            methods = methods.concat((req, res, next) => {
              // validate request
              // the request is sequentially validate the headers, params, query, and oby
              Joi.validate({
                headers: req.headers,
                params: req.params,
                query: req.query,
                body: req.body
              }, validation, (err, result) => {
                if (err) {
                  // return the first error
                  return next(new errors.ValidationError(err))
                }
                else {
                  req.headers = result.headers
                  req.params = result.params
                  Object.defineProperty(req, 'query', { value: result.query })
                  req.body = result.body
                  next()
                }
              })
            })
          }
          if (route.config.cors) {
            methods.push(cors(route.config.cors === true ? {} : route.config.cors))
          }

          if (route.config.pre && route.config.pre.length > 0) {
            methods = methods.concat(route.config.pre)
          }
        }

        // Push the handler
        methods.push(route.handler)

        // Set route config as params to keep it on handlers
        methods.unshift(route.config)

        //Format route to express protocol, maybe `trailpack-router` will do this in the future
        methods.unshift(route.path.replace(/{/g, ':').replace(/}/g, ''))

        // Filter out undefined
        methods = methods.filter(m => m)

        console.log('methods', methods)
        router[parts[0]].apply(router, methods)
      }
    })
    server.use(router)
  },

  /**
   * Start express server
   * @param server express server
   * @param app Trails application
   */
  start(app, server) {
    const init = app.config.get('web.init')
    if (!init) {
      const err = new Error('Init is not define and express can not start')
      return Promise.reject(err)
    }

    init(app, server)
    return new Promise((resolve, reject) => {
      if (this.externalConfig) {
        this.externalConfig(app, server).then(servers => {
          this.nativeServer = servers
          resolve()
        }).catch(reject)
      }
      else if (this.ssl) {
        this._createHttpsServer(this.ssl, server).then(resolve).catch(reject)
      }
      else {
        this.nativeServer = http.createServer(server).listen(this.port, this.host, (err) => {
          if (err) return reject(err)
          resolve()
        })
      }
    })
  },
  _createHttpsServer(sslConfig, app) {
    return new Promise((resolve, reject) => {
      this.nativeServer = https.createServer(sslConfig, app)
        .listen(this.port, this.host, (err) => {
          if (err) return reject(err)
          if (this.redirectToHttps || this.portHttp) {
            const httpServer = http.createServer(app)
              .listen(this.portHttp, this.host, (err) => {
                if (err) return reject(err)
                this.nativeServer = [this.nativeServer, httpServer]
                resolve()
              })
          }
          else {
            resolve()
          }
        })
    })
  }
}