import { URL } from 'url'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import express from 'express'; import { graphqlUploadExpress } from 'graphql-upload'; import helmet from 'helmet'; import { setupApolloServer } from './config/apollo'; import { extractAuthDataFromRequest, setRefreshTokenCookie, } from './config/auth'; import logger from './logger'; import { setupRoutes } from './routes'; export const setupServer = async (): Promise => { const app = express(); app.use(function (req, res, next) { if (process.env.NODE_ENV !== 'test') { req.setTimeout(60000, function () { logger.warn(`Request to path ${req.path} timed out`); next(new Error('Request timeout')); }); res.setTimeout(60000, () => { logger.warn(`Response to path ${req.path} timed out`); next(new Error('Response timeout')); }); } next(); }); /** * Apply Middlewares */ // only necessary if this is running behind a proxy (e.g. nginx) app.set('trust proxy', process.env.NODE_ENV === 'development'); // parse cookies app.use(cookieParser()); app.use( cors({ origin: (origin, callback) => { // origin === undefined means it was the same origin if (origin === undefined) { return callback(null, true); } const whiteList = process.env.CORS_ORIGIN ? String(process.env.CORS_ORIGIN).split(',') : []; if (whiteList.includes(origin)) { return callback(null, true); } let hostname = origin; try { ({ hostname } = new URL(origin)); } catch (error) { logger.error( `Could not parse origin: ${origin}. Continuing anyways...` ); } if (whiteList.includes(hostname)) { return callback(null, true); } // check if the whiteList includes any hostname prefixed with the // protocols const acceptableProtocols = ['http', 'https']; if ( acceptableProtocols.some((protocol) => whiteList.includes(`${protocol}://${hostname}`) ) ) { return callback(null, true); } if (origin.startsWith('chrome-extension://')) { // HACK: should eventually know the id of the chrome extensions we // want to enable return callback(null, true); } // HACK: for staging we want to enable all our PR builds which end // with .vercel.app if ( process.env.NODE_ENV === 'staging' && origin && origin.endsWith('.vercel.app') ) { return callback(null, true); } logger.error( `Could not accept origin: ${origin} with ${process.env.CORS_ORIGIN}` ); callback(new Error('Not allowed.'), false); }, credentials: true, optionsSuccessStatus: 200, exposedHeaders: ['Set-Cookie'], }) ); app.use( helmet({ contentSecurityPolicy: process.env.NODE_ENV === 'production' ? undefined : false, }) ); app.use( express.json({ limit: '20mb', }) ); app.use( express.urlencoded({ extended: true, }) ); const oneMb = 1048576; app.use(graphqlUploadExpress({ maxFileSize: 25 * oneMb, maxFiles: 10 })); const apollo = await setupApolloServer(); apollo.applyMiddleware({ app, // handled elsewhere cors: false, path: '/api', }); app.get('/', (_, res) => { // healthcheck return res.send('ok'); }); // exposed for convenience to get a new access token using a refresh token app.post('/refresh_token', async (req, res) => { const authData = await extractAuthDataFromRequest(req, res); if (authData.authError || !authData.accessToken || !authData.user) { return res.send({ ok: false, accessToken: '', }); } setRefreshTokenCookie(authData.refreshToken, res); return res.send({ ok: true, accessToken: authData.accessToken, }); }); setupRoutes(app); return app; };