From 840172611630ba2932a8cad94c03bfd8d4fb24cb Mon Sep 17 00:00:00 2001 From: Dave Bauman Date: Tue, 11 Jan 2022 13:09:08 -0500 Subject: [PATCH] feat: Migrate to Apollo Server 3.x; use Apollo Sandbox Replaces GraphQL Playground with Apollo Sandbox --- packages/backend/env/.env | 2 +- packages/backend/env/.env.development | 2 +- .../backend/src/controllers/graphql.v1.ts | 43 +++++---- packages/backend/src/index.ts | 3 +- packages/backend/src/middleware/security.ts | 6 +- packages/backend/src/router.ts | 67 +++++++------- packages/backend/src/server.ts | 92 ++++++++++--------- .../main-page/components/footer/footer.tsx | 10 +- 8 files changed, 121 insertions(+), 104 deletions(-) diff --git a/packages/backend/env/.env b/packages/backend/env/.env index ac5a629cc..8af1d11df 100644 --- a/packages/backend/env/.env +++ b/packages/backend/env/.env @@ -8,7 +8,7 @@ LOG_TIMESTAMPS=true PORT=3001 ENCRYPTION_KEY= PUBLIC_URL= -GRAPHQL_PLAYGROUND=true +APOLLO_SANDBOX=true # GITHUB GITHUB_URL= diff --git a/packages/backend/env/.env.development b/packages/backend/env/.env.development index 2bdd44051..4e457e4b9 100644 --- a/packages/backend/env/.env.development +++ b/packages/backend/env/.env.development @@ -4,7 +4,7 @@ LOG_LEVEL=silly # Server HOST=localhost -GRAPHQL_PLAYGROUND=true +APOLLO_SANDBOX=true # Activities ACTIVITIES_IGNORE_LOGIN=true diff --git a/packages/backend/src/controllers/graphql.v1.ts b/packages/backend/src/controllers/graphql.v1.ts index 97cf850e8..162587ba2 100644 --- a/packages/backend/src/controllers/graphql.v1.ts +++ b/packages/backend/src/controllers/graphql.v1.ts @@ -15,12 +15,16 @@ */ import logger from '@iex/shared/logger'; -import { ApolloServer } from 'apollo-server-express'; +import { + ApolloServerPluginLandingPageLocalDefault, + ApolloServerPluginLandingPageProductionDefault +} from 'apollo-server-core'; +import { ApolloServer, ApolloServerExpressConfig } from 'apollo-server-express'; import { Container } from 'typedi'; import { schema } from '../lib/graphql'; -const server = new ApolloServer({ +const apolloConfig: ApolloServerExpressConfig = { schema, context: ({ req }) => { const requestId = req.id; @@ -41,20 +45,25 @@ const server = new ApolloServer({ return context; }, - // Enables a web UI at http://localhost:3001/api/v1/graphql - playground: process.env.GRAPHQL_PLAYGROUND === 'true', plugins: [ { - requestDidStart: () => ({ - willSendResponse(requestContext) { - // Remove the request's scoped container - logger.silly('[GRAPHQL.V1] Terminating Container ' + requestContext.context.requestId); - Container.reset(requestContext.context.requestId); - } - }) - } - ], - uploads: false -}); - -export default server; + async requestDidStart() { + return { + async willSendResponse(requestContext) { + // Remove the request's scoped container + logger.silly('[GRAPHQL.V1] Terminating Container ' + requestContext.context.requestId); + Container.reset(requestContext.context.requestId); + } + }; + } + }, + process.env.APOLLO_SANDBOX === 'true' + ? // Enables a web UI at http://localhost:3001/api/v1/graphql + ApolloServerPluginLandingPageLocalDefault({ footer: false }) + : ApolloServerPluginLandingPageProductionDefault({ footer: false }) + ] +}; + +const graphQLServer = new ApolloServer(apolloConfig); + +export { graphQLServer }; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 948a71d08..75562d580 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -33,7 +33,7 @@ import type { Server } from 'http'; import { bootstrap, defaultKnex } from './lib/db'; import { deployMappings } from './lib/elasticsearch'; -import app from './server'; +import { createServer } from './server'; import logger from '@iex/shared/logger'; // Safeguard to prevent the application from crashing. @@ -59,6 +59,7 @@ const startup = async (): Promise => { // Start Express server logger.debug('[INDEX] Starting Express server'); + const app = await createServer(); const server = app.listen(app.get('port'), () => { logger.info(`IEX Server started in ${app.get('env')} mode on port: ${app.get('port')}`); }); diff --git a/packages/backend/src/middleware/security.ts b/packages/backend/src/middleware/security.ts index d607a5527..3334305f6 100644 --- a/packages/backend/src/middleware/security.ts +++ b/packages/backend/src/middleware/security.ts @@ -21,7 +21,6 @@ import helmet from 'helmet'; */ export const security = helmet({ contentSecurityPolicy: { - reportOnly: false, directives: { 'default-src': ["'self'"], 'base-uri': ["'self'"], @@ -52,13 +51,12 @@ export const security = helmet({ 'object-src': ["'self'", '*'], // Unsafe-inline and unsafe-eval are required by React/webpack or something - // cdn.jsdelivr.net is used by GraphQL Playground 'script-src': [ "'self'", "'unsafe-inline'", "'unsafe-eval'", 'https://*.google-analytics.com', - 'https://cdn.jsdelivr.net', + 'https://*.cdn.apollographql.com', '*' ], 'script-src-attr': ["'none'"], @@ -66,5 +64,7 @@ export const security = helmet({ 'style-src': ["'self'", 'https:', "'unsafe-inline'"] } }, + crossOriginEmbedderPolicy: false, + crossOriginResourcePolicy: false, frameguard: false }); diff --git a/packages/backend/src/router.ts b/packages/backend/src/router.ts index f37df5285..4dd3f558f 100644 --- a/packages/backend/src/router.ts +++ b/packages/backend/src/router.ts @@ -21,7 +21,7 @@ import { graphqlUploadExpress } from 'graphql-upload'; import * as assetController from './controllers/assets.v1'; import * as avatarController from './controllers/avatars.v1'; import * as draftsController from './controllers/drafts.v1'; -import graphQlController from './controllers/graphql.v1'; +import { graphQLServer } from './controllers/graphql.v1'; import * as homeController from './controllers/home.v1'; import * as importController from './controllers/import.v1'; import * as insightsController from './controllers/insights.v1'; @@ -30,41 +30,46 @@ import { oktaAuthenticator } from './middleware/okta-authenticator'; import { requestId } from './middleware/request-id'; import { requireAuth } from './middleware/require-auth'; -const router = Router(); -const v1Router = Router(); +export async function createRouter(): Promise { + const router = Router(); + const v1Router = Router(); -/* - * API routes - */ + // Wait for the graphql server to be ready + await graphQLServer.start(); -router.get('/api/', homeController.getIndex); -router.use(requestId); -router.use('/api/v1', v1Router); + /* + * API routes + */ -/* - * API /v1/ Routes - */ + router.get('/api/', homeController.getIndex); + router.use(requestId); + router.use('/api/v1', v1Router); + + /* + * API /v1/ Routes + */ -v1Router.use( - '/graphql', - oktaAuthenticator, - graphqlUploadExpress({ maxFileSize: 104_857_600, maxFiles: 50 }), - graphQlController.getMiddleware({ path: '/' }) -); -v1Router.all('/webhook', webhookController.hook); + v1Router.use( + '/graphql', + oktaAuthenticator, + graphqlUploadExpress({ maxFileSize: 104_857_600, maxFiles: 50 }), + graphQLServer.getMiddleware({ path: '/' }) + ); + v1Router.all('/webhook', webhookController.hook); -// These routes require authentication -v1Router.get('/insights/search', oktaAuthenticator, requireAuth, insightsController.search); -v1Router.get('/insights/:namespace/:name', oktaAuthenticator, requireAuth, insightsController.getInsight); + // These routes require authentication + v1Router.get('/insights/search', oktaAuthenticator, requireAuth, insightsController.search); + v1Router.get('/insights/:namespace/:name', oktaAuthenticator, requireAuth, insightsController.getInsight); -// These routes do not require authentication -v1Router.get('/avatars/:key', avatarController.getAvatar); -v1Router.head('/insights/:namespace/:name/assets/:filepath*', insightsController.headInsightFile); -v1Router.get('/insights/:namespace/:name/assets/:filepath*', insightsController.getInsightFile); -v1Router.get('/drafts/:draftKey/assets/:attachmentKey', draftsController.getDraftAttachment); + // These routes do not require authentication + v1Router.get('/avatars/:key', avatarController.getAvatar); + v1Router.head('/insights/:namespace/:name/assets/:filepath*', insightsController.headInsightFile); + v1Router.get('/insights/:namespace/:name/assets/:filepath*', insightsController.getInsightFile); + v1Router.get('/drafts/:draftKey/assets/:attachmentKey', draftsController.getDraftAttachment); -v1Router.get('/changelog', assetController.getChangelog); -v1Router.get('/markdown', assetController.getMarkdown); -v1Router.all('/import', cors({ origin: true }), importController.importToDraft); + v1Router.get('/changelog', assetController.getChangelog); + v1Router.get('/markdown', assetController.getMarkdown); + v1Router.all('/import', cors({ origin: true }), importController.importToDraft); -export default router; + return router; +} diff --git a/packages/backend/src/server.ts b/packages/backend/src/server.ts index dba39a1fb..4658c6c86 100644 --- a/packages/backend/src/server.ts +++ b/packages/backend/src/server.ts @@ -26,59 +26,61 @@ import { StatusCodes } from 'http-status-codes'; import morgan from 'morgan'; import { security } from './middleware/security'; -import router from './router'; +import { createRouter } from './router'; -// Init express -const app = express(); +export async function createServer(): Promise { + // Init express + const app = express(); -app.set('port', Number(process.env.PORT || 3000)); -app.set('env', process.env.NODE_ENV); + app.set('port', Number(process.env.PORT || 3000)); + app.set('env', process.env.NODE_ENV); -// Show routes called in console during development -switch (process.env.NODE_ENV) { - case 'development': - logger.info('Loading development middleware'); - app.use(security); - break; - case 'production': - logger.info('Loading production middleware'); - app.use(security); - break; - default: - logger.info('Actually ' + process.env.NODE_ENV); -} + // Show routes called in console during development + switch (process.env.NODE_ENV) { + case 'development': + logger.info('Loading development middleware'); + app.use(security); + break; + case 'production': + logger.info('Loading production middleware'); + app.use(security); + break; + default: + logger.info('Actually ' + process.env.NODE_ENV); + } -app.use(compression()); -app.use(express.json({ limit: '20mb' })); -app.use(express.urlencoded({ extended: true })); + app.use(compression()); + app.use(express.json({ limit: '20mb' })); + app.use(express.urlencoded({ extended: true })); -if (process.env.LOG_REQUESTS === 'true') { - app.use(morgan('dev')); -} + if (process.env.LOG_REQUESTS === 'true') { + app.use(morgan('dev')); + } -// Add APIs -app.use('/', router); + // Add APIs + const router = await createRouter(); + app.use('/', router); -// Print API errors -// eslint-disable-next-line @typescript-eslint/no-unused-vars -app.use((err: Error, req: Request, res: Response, next: NextFunction) => { - logger.error(err.message, err); - return res.status(StatusCodes.BAD_REQUEST).json({ - error: err.message + // Print API errors + // eslint-disable-next-line @typescript-eslint/no-unused-vars + app.use((err: Error, req: Request, res: Response, next: NextFunction) => { + logger.error(err.message, err); + return res.status(StatusCodes.BAD_REQUEST).json({ + error: err.message + }); }); -}); -// Serve front-end -const frontendDir = path.join(__dirname, '../../../frontend/build'); + // Serve front-end + const frontendDir = path.join(__dirname, '../../../frontend/build'); -app.use( - express.static(frontendDir, { - redirect: false - }) -); -app.get(/^(?!\/api).+/, (req: Request, res: Response) => { - res.sendFile('index.html', { root: frontendDir }); -}); + app.use( + express.static(frontendDir, { + redirect: false + }) + ); + app.get(/^(?!\/api).+/, (req: Request, res: Response) => { + res.sendFile('index.html', { root: frontendDir }); + }); -// Export express instance -export default app; + return app; +} diff --git a/packages/frontend/src/pages/main-page/components/footer/footer.tsx b/packages/frontend/src/pages/main-page/components/footer/footer.tsx index a02e754cb..226809168 100644 --- a/packages/frontend/src/pages/main-page/components/footer/footer.tsx +++ b/packages/frontend/src/pages/main-page/components/footer/footer.tsx @@ -32,11 +32,11 @@ export const Footer = (props) => { const bgColor = useColorModeValue('unset', 'gray.700'); const color = useColorModeValue('gray.700', 'gray.200'); - let graphQlPlaygroundLink; + let apolloSandboxLink; if (window.location.origin === 'http://localhost:3000') { - graphQlPlaygroundLink = 'http://localhost:3001/api/v1/graphql'; + apolloSandboxLink = 'http://localhost:3001/api/v1/graphql'; } else { - graphQlPlaygroundLink = '/api/v1/graphql'; + apolloSandboxLink = '/api/v1/graphql'; } return ( @@ -64,10 +64,10 @@ export const Footer = (props) => { )} - + - GraphQL Playground + Apollo Sandbox