api_api.js

/**
## XYZ API

The XYX API module exports the api function which serves as the entry point for all XYZ API requests.

A node.js express app will require the api module and reference the exported api method for all request routes.

```js
const app = express()
const api = require('./api/api')
app.get(`/`, api)
```

@requires /view
@requires /query
@requires /fetch
@requires /sign
@requires /user/auth

@module /api
*/

const logger = require('../mod/utils/logger')

const login = require('../mod/user/login')

const register = require('../mod/user/register')

const auth = require('../mod/user/auth')

const saml = process.env.SAML_ENTITY_ID && require('../mod/user/saml')

const routes = {
  provider: require('../mod/provider/_provider'),
  sign: require('../mod/sign/_sign'),
  query: require('../mod/query'),
  fetch: require('../mod/fetch'),
  user: require('../mod/user/_user'),
  view: require('../mod/view'),
  workspace: require('../mod/workspace/_workspace'),
}

process.env.COOKIE_TTL ??= 36000

process.env.TITLE ??= 'GEOLYTIX | XYZ'

process.env.DIR ??= ''

/**
@global
@typedef {Object} req
The req object represents the HTTP request and has properties for the request query string, parameters, body, HTTP headers, and so on.
@property {Object} params HTTP request parameter.
@property {Object} [body] HTTP POST request body.
@property {Object} header HTTP request header.
*/

/**
@global
@typedef {Object} res
The res object represents the HTTP response that an [Express] app sends when it gets an HTTP request.
*/

/**
@function api
@async

@description
The XYZ api method will validate request parameter.

The API module method requires the user/auth module to authenticate private API requests.

Requests are passed to individual API modules from the api() method.

@param {req} req HTTP request.
@param {res} res HTTP response.
*/

module.exports = async function api(req, res) {

  // redirect if dir is missing in url path.
  if (process.env.DIR && req.url.length === 1) {
    res.setHeader('location', `${process.env.DIR}`)
    return res.status(302).send()
  }

  logger(req, 'req')

  logger(req.url, 'req_url')

  // Request will be short circuited to the saml module.
  if (req.url.match(/\/saml/)) {
    if (!saml) return;
    return saml(req, res)
  }

  // Merge request params and query params.
  req.params = Object.assign(req.params || {}, req.query || {})

  Object.keys(req.params).forEach(key => {

    // Set null from string.
    if (req.params[key]?.toLowerCase() === 'null') {
      req.params[key] = null
      return;
    }

    // Set boolean false from string.
    if (req.params[key]?.toLowerCase() === 'false') {
      req.params[key] = false
      return;
    }

    // Set boolean true from string.
    if (req.params[key]?.toLowerCase() === 'true') {
      req.params[key] = true
      return;
    }

    // Delete param keys with undefined values.
    if (req.params[key] === undefined) {
      delete req.params[key]
      return;
    }

    // Delete param keys with empty string value.
    if (req.params[key] === '') {
      delete req.params[key]
      return;
    }

    // Check whether param begins and ends with square braces.
    if (typeof req.params[key] === 'string' && req.params[key].match(/^\[.*\]$/)) {

      // Slice square brackets of string and split on comma.
      req.params[key] = req.params[key].slice(1, -1).split(',')
    }
  })

  // Url parameter keys must be white listed as letters and numbers only.
  if (Object.keys(req.params).some(key => !key.match(/^[A-Za-z0-9_-]*$/))) {

    return res.status(403).send('URL parameter key validation failed.')
  }

  // Language param will default to english [en] is not explicitly set.
  req.params.language ??= 'en'

  // Assign from _template if provided as path param.
  req.params.template ??= req.params._template

  // Short circuit login view or post request.
  if (req.params.login || req.body && req.body.login) return login(req, res)

  // Short circuit register view or post request.
  if (req.params.register || req.body && req.body.register) return register(req, res)

  // Short circuit logout request
  if (req.params.logout) {

    // Remove cookie.
    res.setHeader('Set-Cookie', `${process.env.TITLE}=null;HttpOnly;Max-Age=0;Path=${process.env.DIR || '/'}`)

    // Remove logout parameter.
    res.setHeader('location', (process.env.DIR || '/') + (req.params.msg && `?msg=${req.params.msg}` || ''))

    return res.status(302).send()
  }

  // Validate signature of either request token or cookie.
  const user = await auth(req, res)

  // Remove token from params object.
  delete req.params.token

  // The authentication method returns an error.
  if (user && user instanceof Error) {

    if (req.headers.authorization) {

      return res.status(401).send(user.message)
    }

    // Remove cookie.
    res.setHeader('Set-Cookie', `${process.env.TITLE}=null;HttpOnly;Max-Age=0;Path=${process.env.DIR || '/'};SameSite=Strict${!req.headers.host.includes('localhost') && ';Secure' || ''}`)

    req.params.msg = user.msg || user.message

    // Return login view with error message.
    return login(req, res)
  }

  // Set user as request parameter.
  req.params.user = user

  // User route
  if (req.url.match(/(?<=\/api\/user)/)) {

    // A msg will be returned if the user does not met the required priviliges.
    return routes.user(req, res)
  }

  // The login view will be returned for all PRIVATE requests without a valid user.
  if (!user && process.env.PRIVATE) {

    if (process.env.SAML_LOGIN) {
      res.setHeader('location', `${process.env.DIR}/saml/login`)
      return res.status(302).send()
    }

    return login(req, res)
  }

  // Provider route
  if (req.url.match(/(?<=\/api\/provider)/)) {
    return routes.provider(req, res)
  }

  // Signing route
  if (req.url.match(/(?<=\/api\/sign)/)) {
    return routes.sign(req, res)
  }

  // Location route
  if (req.url.match(/(?<=\/api\/location)/)) {

    // Set template and route to query mod.
    req.params.template = `location_${req.params.method}`
    return routes.query(req, res)
  }

  // Query route
  if (req.url.match(/(?<=\/api\/query)/)) {
    return routes.query(req, res)
  }

  // Fetch route
  if (req.url.match(/(?<=\/api\/fetch)/)) {
    return routes.fetch(req, res)
  }

  // Workspace route
  if (req.url.match(/(?<=\/api\/workspace)/)) {
    return routes.workspace(req, res)
  }

  // Return the View API on the root.
  routes.view(req, res)
}