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);
};