import bodyParser from 'body-parser'; import compression from 'compression'; import timeout from 'connect-timeout'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import Err from 'err'; import { Application } from 'express'; import { isObject } from 'lodash'; import { DateTime } from 'luxon'; import methodOverride from 'method-override'; import { ExpressControllersInterface, ExpressMiddlewareInterface } from '@/serviceManager'; import api from './api'; /* eslint-disable no-magic-numbers */ const REQ_TIMEOUT_SEC = 29 * 1000; /* eslint-enable no-magic-numbers */ const NON_JSON_ENDPOINTS = []; const RAW_BODY_ENDPOINTS = ['/billing/webhook']; const defaultContentTypeMiddleware = (req, res, next): void => { if (NON_JSON_ENDPOINTS.some((urlPath) => req?.url?.includes(urlPath))) { return next(); } req.headers['content-type'] = 'application/json'; return next(); }; // This is necessary for stripe, intercom, and slack signature testing const rawBodyVerify = (req, res, buf): void => { if (RAW_BODY_ENDPOINTS.some((urlPath) => req?.url?.includes(urlPath))) { req.rawBody = buf; } }; /** * @class */ export class ExpressMiddleware { /** * Attach express middleware to app * * @param {app} app * @param {object} middleware * @param {object} controllers * @returns {void} */ static attach(app: Application, middleware: ExpressMiddlewareInterface, controllers: ExpressControllersInterface): void { if (!isObject(app) || !isObject(middleware) || !isObject(controllers)) { throw new Err('must have app, middleware, and controllers'); } // app.use(logger); app.use(defaultContentTypeMiddleware); app.use(middleware.logger.log); app.use(compression()); app.use( bodyParser.json({ limit: '50mb', verify: rawBodyVerify, }) ); app.use(bodyParser.urlencoded({ extended: true, verify: rawBodyVerify })); app.use(methodOverride()); app.use(cookieParser()); app.use( cors({ exposedHeaders: ['demo-id', 'Date', 'ETag', 'timestamp', 'x-ratelimit-reset', 'x-ratelimit-remaining', 'x-ratelimit-limit', 'retry-after'], }) ); app.use((request, response, next) => { const token = request.get('authorization')?.replace(/^bearer(:)?\s+/i, ''); if (token) { (request as any).token = token; } next(); }); app.enable('trust proxy'); app.disable('x-powered-by'); const haltOnTimedout = (req, res, next): void => !req.timedout && next(); app.use(timeout(REQ_TIMEOUT_SEC)); app.use(haltOnTimedout); app.use(async (err, req, res, next) => middleware.exceptionHandler.handleJsonParseError(err, req, res, next)); // Set timestamp on response to calibrate client time app.use((request, response, next) => { response.set('timestamp', `${DateTime.utc().valueOf()}`); next(); }); // Add locals, so we don't have to check everywhere app.use((request, response, next) => { (request as any).locals = { ...(request as any).locals }; next(); }); // All valid routes handled here app.use(api(middleware, controllers)); // Handle errors that are otherwise unhandled for some reason app.use(async (err, req, res, next) => middleware.exceptionHandler.handleError(err, req, res, next)); // Everything else is a 404 app.use(async (req, res) => middleware.exceptionHandler.handleNotFound(req, res)); } }