server/api.js

import _ from 'lodash'
import bodyParser from 'body-parser'
import { Router } from 'express'
import qs from 'query-string'

import anonymousApis from './anonymous'
import securedApis from './secured'

const routersConditions = {}
const routers = {}

const maybeApply = (name, fn) => {
  return (req, res, next) => {
    const router = req.originalUrl.match(/\/api\/(botpress-[^\/]+).*$/i)
    if (!router) {
      return fn(req, res, next)
    }

    if (!routersConditions[router[1]]) {
      return fn(req, res, next)
    }

    const condition = routersConditions[router[1]][name]
    if (condition === false) {
      next()
    } else if (typeof condition === 'function' && condition(req) === false) {
      next()
    } else {
      return fn(req, res, next)
    }
  }
}

module.exports = bp => {
  const _authenticationMiddleware = async (req, res, next) => {
    res.maybeSendRequireLogin = () => {
      if (!bp.botfile.login.enabled) {
        res.status(400).send({
          message: 'Login must be turned on for this API method'
        })

        return true
      } else {
        return false
      }
    }

    if (!bp.botfile.login.enabled) {
      return next()
    }

    const user = await bp.security.authenticate(req.headers.authorization)
    if (!!user) {
      req.user = user
      next()
    } else {
      res
        .status(401)
        .location('/login')
        .end()
    }
  }

  const installProtector = app => {
    // TODO: X/Cloud | Add Permissions
    app.secure = (operation, ressource) => {
      const wrap = method => (name, ...handlers) => {
        const secure = async (req, res, next) => {
          try {
            return next()
            // return res.sendStatus(403) // HTTP Forbidden
          } catch (err) {
            return res.status(500).send({ message: err.message })
          }
        }

        return app[method](name, secure, ...handlers)
      }

      return {
        get: wrap('get'),
        post: wrap('post'),
        put: wrap('put'),
        delete: wrap('delete')
      }
    }
  }

  const installRouter = app => {
    /**
     * Creates an HTTP [Express Router]{@link https://expressjs.com/} that is protected by authentication
     * The router routes are available at "http://bot_url/api/:name"
     * Where `name` is a string starting with `botpress-`
     * @func
     * @alias getRouter
     * @memberOf! Botpress
     * @param  {String} name       The name of the router. Must start with `botpress-`
     * @param  {object=} [conditions] See examples. Conditionally disables built-in Botpress middlewares.
     * @example
     * const securedRouter = bp.getRouter('botpress-custom')
     * const publicRouter = bp.getRouter('botpress-custom', { auth: false })
     *
     * // Conditions can also be used like below
     * const conditions = { 'auth': req => !/\/webhook/i.test(req.originalUrl) }
     * const conditionalAuthentication = bp.getRouter('botpress-custom', conditions)
     */
    bp.getRouter = (name, conditions) => {
      if (!/^botpress-/.test(name)) {
        throw new Error(`The name of a router must start with 'botpress-'. Received: ${name}`)
      }

      if (!routers[name]) {
        const router = Router()
        routers[name] = router
        app.use(`/api/${name}/`, router)
      }

      if (conditions) {
        routersConditions[name] = Object.assign(routersConditions[name] || {}, conditions)
      }

      installProtector(routers[name])
      return routers[name]
    }

    const links = {}

    /**
     * Creates a short link for a specific API route, making it easier to share, more verbose and elegant.
     * Short links are available as `http://bot_url/s/{name}`
     * @alias  createShortlink
     * @param  {String} name        Unique, url-friendly name of the short link
     * @param  {String} destination The original route to redirect to
     * @param  {Object} params      Query parameters to pass the route. Will be serialized.
     * @memberof! Botpress
     * @example
     *   const config = {
  botName: 'Superbot',
  botConvoDescription: "Tell me something!",
  backgroundColor: '#ffffff'
}

// Visiting "http://bot_url/s/chat" will display the webchat in fullscreen
bp.createShortlink('chat', '/lite', {
  m: 'channel-web',
  v: 'fullscreen',
  options: JSON.stringify({ config: config })
})
     */
    bp.createShortlink = (name, destination, params) => {
      name = name.toLowerCase()

      if (links[name]) {
        throw new Error(`There's already a shortlink named "${name}"`)
      }

      const q = params ? '?' + qs.stringify(params) : ''
      links[name] = `${destination}${q}`
    }

    app.get(`/s/:name`, (req, res) => {
      const name = req.params.name.toLowerCase()

      if (!links[name]) {
        return res.status(404).send({ error: `Shortlink "${name}" not registered` })
      }

      res.redirect(links[name])
    })
  }

  const installMaybeUse = app => {
    app.maybeUse = function() {
      if (arguments.length === 3) {
        app.use(arguments[0], maybeApply(arguments[1], arguments[2]))
      } else if (arguments.length === 2) {
        app.use(maybeApply(arguments[0], arguments[1]))
      }
    }
  }

  const install = async app => {
    installRouter(app)
    installProtector(app)
    installMaybeUse(app)

    app.maybeUse('bodyParser.json', bodyParser.json({ limit: _.get(bp.botfile, 'api.bodyMaxSize') || '1mb' }))
    app.maybeUse('bodyParser.urlencoded', bodyParser.urlencoded({ extended: true }))

    anonymousApis(bp, app)

    app.use('/api/*', maybeApply('auth', _authenticationMiddleware))

    securedApis(bp, app)
  }

  return { install }
}