'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/googlejwt
* @function
* @memberof module:app
* @inner
*/
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/googleidtoken
* @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;