server/server.ts (84 lines of code) (raw):

import * as Sentry from '@sentry/node'; import { raw } from 'body-parser'; import cookieParser from 'cookie-parser'; import type { NextFunction, Request, RequestHandler, Response } from 'express'; import { default as express } from 'express'; import helmet from 'helmet'; import { MAX_FILE_ATTACHMENT_SIZE_KB } from '../shared/fileUploadUtils'; import { conf } from './config'; import { log } from './log'; import { getConfig } from './oktaConfig'; import * as routes from './routes'; const port = 9233; const server = express(); const oktaConfig = await getConfig(); declare let WEBPACK_BUILD: string; if (conf.SERVER_DSN) { Sentry.init({ dsn: conf.SERVER_DSN, release: WEBPACK_BUILD || 'local', environment: conf.DOMAIN, }); server.use( Sentry.Handlers.requestHandler({ ip: false, user: false, request: ['method', 'query_string', 'url'], // this list is explicit, to avoid sending cookies }), ); } if (conf.DOMAIN === 'thegulocal.com') { // tslint:disable-next-line:no-object-mutation process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } server.use(helmet()); export const createCsp = (hashes: string[]) => { const prefixedHashes = hashes.map((hash) => `'sha256-${hash}'`); const csp = [ `script-src ${prefixedHashes.join(' ')} 'strict-dynamic'`, `style-src 'unsafe-inline'`, `object-src 'none'`, ]; return csp.join('; '); }; server.use(function (_: Request, res: Response, next: NextFunction) { /* * This sets a default csp header, this is overriden in: * - mmaFrontend.ts * - helpcentreFrontend.ts * Where a more specific policy with script hashes can be added */ res.set({ 'Report-To': '{ "group": "csp-endpoint", "endpoints": [ { "url": "/api/csp-audit-report-endpoint" } ] }', 'Content-Security-Policy-Report-Only': createCsp([]), }); next(); }); const serveStaticAssets: RequestHandler = express.static(__dirname + '/static'); /** static asses are cached by fastly */ server.use('/static', serveStaticAssets); /** * WARNING: Because manage-fronted manages personal data make sure to prevent caching * on both CDN (Fastly) and browsers. This Cache-Control header below is VERY IMPORTANT * so mind removing or it! Without it personal data would likely be served to wrong user. * In particular mind the endpoints that proxy to APIs such as IDAPI or MDAPI. * * This middleware has no mount path so it executes for all routes that follow this call. * To enable caching for a route that requires it register it above this middleware, see /static * route as example, or override headers via Response arguments for a particular route later one. * https://stackoverflow.com/a/31661931 * * There exists an additional safety net as a VCL condition in Fastly which should force a * PASS (do not cache) on sensitive routes. See https://github.com/guardian/manage-frontend/wiki/Fastly-&-Caching */ const disableCache = (_: Request, res: Response, next: NextFunction) => { res.header( 'Cache-Control', 'private, no-cache, no-store, must-revalidate, max-age=0', ); res.header('Access-Control-Allow-Origin', '*.' + conf.DOMAIN); next(); }; server.use(disableCache); /** * Cookies and body parsing * ------------------------ * We don't parse the body of JSON requests that are coming into the server from * the client using express.json() because MMA is essentially a proxy and is * usually not interested in the body of the request. One of the exceptions is * POST /aapi/avatar, which extracts file data from a JSON payload and reformats * it. This is handled separately directly in the route handler. * * WARNING: A lot of the routes _rely_ on the JSON body not being parsed at the * server level, so they don't stringify the body before sending it to * downstream APIs. For this reason, think and test carefully before adding a * global body parser to the server! */ server.use(cookieParser(oktaConfig.cookieSecret)); server.use( raw({ type: '*/*', limit: `${MAX_FILE_ATTACHMENT_SIZE_KB + 100}kb`, }), ); // parses all bodys to a raw 'Buffer' server.use(routes.core); server.use('/oauth', routes.oauth); server.use('/profile/', routes.profile); server.use('/api/', routes.api); server.use('/newspaperArchive', routes.newspaperArchive); server.use('/idapi', routes.idapi); server.use('/mpapi', routes.mpapi); server.use('/aapi', routes.aapi); server.use(routes.productsProvider('/api/')); // Help Centre server.use('/help-centre', routes.helpcentre); // ALL OTHER ENDPOINTS CAN BE HANDLED BY MMA CLIENT SIDE REACT ROUTING server.use(routes.frontend); if (conf.SERVER_DSN) { server.use(Sentry.Handlers.errorHandler()); } server.listen(port); log.info(`Serving at http://localhost:${port}`);