app.js

'use strict';
/**
 * Node.js Express application API Endpoints.
 * @module app
 * @requires bodyParser
 * @requires express
 * @requires @google-cloud/bigquery
 * @requires @google-cloud/firestore
 * @requires path
 */
const bodyParser = require('body-parser');
const createApplication = require('express');
const memoryCache = require('memory-cache');
const path = require('path');
const serveStatic = require('serve-static');
const common = require(path.join(__dirname, 'common'));
const {bynderWebhook, getProduct} = require(path.join(__dirname, 'express-handlers'));
const docsDirPath = path.join(__dirname, 'docs');
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');

/**
 * ES6 wrapper function which catches errors and passes them to next.
 * The async/await proposal behaves just like a promise generator,
 * but it can be used in more places (like class methods and arrow functions).
 *
 * @name wrap
 * @function
 * @memberof module:app
 * @inner
 * @param {function} fn
 * @return {function(...[*]): Promise<any>}
 */
const wrap =
  (fn) =>
  (...args) =>
    fn(...args).catch(args[2]);

/**
 * Product Enrichment Node.js Express application.
 * Backend REST API which serves product data on demand from Google Cloud Firestore.
 * @type {object}
 * @const
 * @namespace api
 */
const app = createApplication();
// configure express to understand the client IP address as the left-most entry in the X-Forwarded-For header.
// see: https://expressjs.com/en/guide/behind-proxies.html
app.set('trust proxy', false);

app.use(function (req, res, next) {
  if (req.get('x-amz-sns-message-type')) {
    req.headers['content-type'] = 'application/json';
  }
  next();
});

app.use(bodyParser.json());
app.use(serveStatic(docsDirPath));

/**
 * Returns request JWT authentication information.
 * A JWT is a JSON Web Token is an open standard access token format for use in
 * HTTP Authorization headers and URI query parameters.
 * @name get/auth/info
 * @function
 * @memberof module:app
 * @inner
 * @see [Introduction to JSON Web Tokens](https://jwt.io/introduction/)
 */
app.get(`${common.basePath}/auth/info/googlejwt`, common.authInfoHandler);

/**
 * Returns request Google id Token authentication information.
 * A Google id Token is a JSON Web Token (JWT) that contains the OpenID Connect fields
 * needed to identify a Google user account or service account, and that is signed by
 * Google's authentication service, https://accounts.google.com.
 * @name get/auth/info
 * @function
 * @memberof module:app
 * @inner
 */
app.get(`${common.basePath}/auth/info/googleidtoken`, common.authInfoHandler);

/**
 * Get Product Enrichment JavaScript API documentation.
 *
 * @name docs
 * @function
 * @memberof module:app
 * @inner
 * @param {string} path - The path for which the middleware function is invoked; can be any of:
 *  A string representing a path.
 *  A path match pattern.
 *  A regular expression pattern to match paths.
 *  An array of combinations of the above.
 *  @see [path-examples](https://expressjs.com/en/4x/api.html#path-examples)
 * @param {object} req - The req object represents the HTTP request and has properties for the request query string,
 *  parameters, body, HTTP headers, and so on. In this documentation and by convention, the object is always
 *  referred to as req (and the HTTP response is res) but its actual name is determined by the parameters to the
 *  callback function in which you are working.
 * @param {object} res - The res object represents the HTTP response that an Express app sends when it gets an
 *  HTTP request. In this documentation and by convention, the object is always referred to as res
 *  (and the HTTP request is req) but its actual name is determined by the parameters to the callback function in
 *  which you are working.
 *  The res object is an enhanced version of the Node response object and supports all
 *  @see [built-in fields and methods](https://nodejs.org/api/http.html#http_class_http_serverresponse).
 *  @async
 */
app.get(
  common.docsPath,
  wrap(async (req, res) => {
    res.sendFile(`${docsDirPath}/index.html`);
  })
);

/**
 * Health check endpoint.
 *
 * @name health
 * @function
 * @memberof module:app
 * @inner
 * @param {string} path - The path for which the middleware function is invoked; can be any of:
 *  A string representing a path.
 *  A path match pattern.
 *  A regular expression pattern to match paths.
 *  An array of combinations of the above.
 *  @see [path-examples](https://expressjs.com/en/4x/api.html#path-examples)
 * @param {object} req - The req object represents the HTTP request and has properties for the request query string,
 *  parameters, body, HTTP headers, and so on. In this documentation and by convention, the object is always
 *  referred to as req (and the HTTP response is res) but its actual name is determined by the parameters to the
 *  callback function in which you are working.
 * @param {object} res - The res object represents the HTTP response that an Express app sends when it gets an
 *  HTTP request. In this documentation and by convention, the object is always referred to as res
 *  (and the HTTP request is req) but its actual name is determined by the parameters to the callback function in
 *  which you are working.
 *  The res object is an enhanced version of the Node response object and supports all
 *  @see [built-in fields and methods](https://nodejs.org/api/http.html#http_class_http_serverresponse).
 *  @async
 */
app.get(
  '/health',
  wrap(async (req, res) => res.status(200).send('HEALTHY'))
);

const memCache = new memoryCache.Cache();

/**
 * Configure cache middleware which looks for a cached value using the request URL as the key.
 * When found the cached response is sent.
 * When not found the Express send function is wrapped to cache the response
 * before sending it to the client and then calling the next middleware.
 *
 * @name cache
 * @function
 * @memberof module:app
 * @inner
 * @param {number} duration - Time in ms (via setTimeout) after which the value will be removed from the cache.
 * @return {function}
 */
const cache = (duration) => {
  return (req, res, next) => {
    const key = `__express__${req.originalUrl}` || req.url;
    const cachedBody = memCache.get(key);
    if (cachedBody) {
      res.send(cachedBody);
    } else {
      res.sendResponse = res.send;
      res.send = (body) => {
        memCache.put(key, body, duration);
        res.sendResponse(body);
      };
      next();
    }
  };
};

/**
 * Get Product data from Google Cloud Firestore.
 *
 * Request, Response and Next are Callback functions.  You can provide multiple callback functions that behave
 * just like middleware, except that these callbacks can invoke next('route') to bypass the remaining route
 * callback(s). You can use this mechanism to impose pre-conditions on a route, then pass control to subsequent
 * routes if there is no reason to proceed with the current route.
 *
 * @name getProduct
 * @function
 * @memberof module:app
 * @inner
 * @param {string} path - The path for which the middleware function is invoked; can be any of:
 *  A string representing a path.
 *  A path match pattern.
 *  A regular expression pattern to match paths.
 *  An array of combinations of the above.
 *  Defaults to '/' (root path)
 *  @see [path-examples](https://expressjs.com/en/4x/api.html#path-examples)
 * @param {object} req - The req object represents the HTTP request and has properties for the request query string,
 *  parameters, body, HTTP headers, and so on. In this documentation and by convention, the object is always
 *  referred to as req (and the HTTP response is res) but its actual name is determined by the parameters to the
 *  callback function in which you are working.
 * @param {object} res - The res object represents the HTTP response that an Express app sends when it gets an
 *  HTTP request. In this documentation and by convention, the object is always referred to as res
 *  (and the HTTP request is req) but its actual name is determined by the parameters to the callback function in
 *  which you are working.
 *  The res object is an enhanced version of Node’s own response object and supports all
 *  @see [built-in fields and methods](https://nodejs.org/api/http.html#http_class_http_serverresponse).
 * @param {object} next - For errors returned from asynchronous functions invoked by route handlers and middleware,
 *  you must pass them to the next() function, where Express will catch and process them. Bypass the remaining route
 *  callbacks(s) and invoke the Google Error Reporting middleware handler.
 *  @async
 */
app.get(common.productPath, cache(common.cacheDuration), wrap(getProduct));

/**
 * Rate-limiting middleware for Express used to ensure we do not exceed the Bynder API request limit
 * of 4500 request per 5 minute time frame.
 *
 * Global rate-limiting is configured using the extensible service proxy IP in order to slow down all request before
 * the Bynder API request limit is reached.
 *
 * @name bynderApiLimiter
 * @function
 * @memberof module:app
 * @inner
 * @see https://www.npmjs.com/package/express-rate-limit
 * @type {function(...*)}
 */
const bynderApiLimiter = rateLimit({
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
  max: 4500, // Limit requests to 4500 per `window` (here, per five-minute time frame) from a single IP address.
  message: `Request IP has exceeded the Bynder API request limit of 4500 request per 5 minute time frame.
  Request IP is forbidden from submitting bynder-webhook request for the next 5 minutes.`,
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  windowMs: 5 * 60000, // 60000 ms = 1 minute
});

/**
 * Middleware for Express that slows down responses rather than blocking them outright.
 *
 * Global slow down is configured using the extensible service proxy IP in order to slow down all request before
 * the Bynder API request limit of 4500 request per 5 minute time frame is reached.
 *
 * Response rate will be slowed down after a requestor reaches 4000 request in a 5-minute time frame
 * by adding 500ms of delay per request above 4000.
 *
 * A req.slowDown property is added to all requests with the following fields:
 *  limit: The options.delayAfter value (defaults to 1)
 *  current: The number of requests in the current window
 *  remaining: The number of requests remaining before rate-limiting begins
 *  resetTime: When the window will reset and current will return to 0, and remaining will return to limit
 *    (in milliseconds since epoch - compare to Date.now()). Note: this field depends on store support.
 *    It will be undefined if the store does not provide the value.
 *  delay: Amount of delay imposed on current request (milliseconds)
 *
 * @name bynderApiSlowDown
 * @function
 * @memberof module:app
 * @inner
 * @see https://www.npmjs.com/package/express-slow-down
 * @type {function(...*)}
 */
const bynderApiSlowDown = slowDown({
  delayAfter: 4000, // allow 4000 requests per 5 minutes, then...
  delayMs: 500, // begin adding 500ms of delay per request above 4000
  // request # 4001 is delayed by  500ms
  // request # 4002 is delayed by 1000ms
  // request # 4003 is delayed by 1500ms ...
  maxDelayMs: 5 * 60000 + 500, // 60000 ms = 1 minute - maximum delayMs of 5 min 500 ms
  windowMs: 5 * 60000, // 60000 ms = 1 minute
});

/**
 * Bynder webhook.
 *
 * This endpoint is rate limited to 4500 request per 5 minute time frame.
 *
 * Response rate will be slowed down after a requestor reaches 4000 request in a 5-minute time frame
 * by adding 500ms of delay per request above 4000.
 *
 * Request, Response and Next are Callback functions.  You can provide multiple callback functions that behave
 * just like middleware, except that these callbacks can invoke next('route') to bypass the remaining route
 * callback(s). You can use this mechanism to impose pre-conditions on a route, then pass control to subsequent
 * routes if there is no reason to proceed with the current route.
 *
 * @name bynderWebhook
 * @function
 * @memberof module:app
 * @inner
 * @param {object} req - The req object represents the HTTP request and has properties for the request query string,
 *  parameters, body, HTTP headers, and so on. In this documentation and by convention, the object is always
 *  referred to as req (and the HTTP response is res) but its actual name is determined by the parameters to the
 *  callback function in which you are working.
 * @param {object} res - The res object represents the HTTP response that an Express app sends when it gets an
 *  HTTP request. In this documentation and by convention, the object is always referred to as res
 *  (and the HTTP request is req) but its actual name is determined by the parameters to the callback function in
 *  which you are working.
 *  The res object is an enhanced version of Node’s own response object and supports all
 *  @see [built-in fields and methods](https://nodejs.org/api/http.html#http_class_http_serverresponse).
 *  @async
 */
app.post(common.bynderWebhookPath, bynderApiSlowDown, bynderApiLimiter, wrap(bynderWebhook));

/**
 * Add Google Cloud Error Reporting express error handling middleware to the app router.
 * Should be attached after all the other routes and use() calls.
 * @see http://expressjs.com/en/guide/error-handling.html#error-handling.

 * @public
 */
app.use(common.errors.express);

/**
 * Application prototype.
 */
module.exports = app;