diff --git a/.gitignore b/.gitignore index ff2e9f4..71f12ca 100644 --- a/.gitignore +++ b/.gitignore @@ -97,4 +97,5 @@ typings/ *.swo -dist \ No newline at end of file +dist +.vscode diff --git a/jest.setup.js b/jest.setup.js index 03aa587..7a115c6 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,12 +1,14 @@ /* global server */ -import mockdate from 'mockdate' -mockdate.set(0) +import mockdate from 'mockdate'; +mockdate.set(0); import { mockDB } from '@utils/testUtils'; import { ONE_USER_DATA } from '@utils/constants'; import { init } from './lib/testServer'; require('jest-extended'); +process.env.ENVIRONMENT_NAME = 'local'; + mockDB(); beforeEach(async () => { diff --git a/lib/routes/routes.js b/lib/routes/routes.js index 32f1583..83c907a 100644 --- a/lib/routes/routes.js +++ b/lib/routes/routes.js @@ -1,8 +1,10 @@ +import withCors from '@utils/cors'; + const { logger } = require('@utils'); // Note: Unfortunately, wurst does not work well with the ES6 default export syntax. export default [ - { + withCors({ method: 'GET', path: '/', handler: (request, h) => { @@ -15,5 +17,5 @@ export default [ auth: false, tags: ['api', 'health-check'], }, - }, + }), ]; diff --git a/package.json b/package.json index 2b82f02..a38431c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "esm": "^3.2.25", "glob-promise": "^5.0.0", "hapi-auth-bearer-token": "^8.0.0", - "hapi-cors": "^1.0.3", "hapi-pagination": "^4.0.0", "hapi-rate-limit": "^5.0.0", "hapi-swaggerui": "https://github.com/wednesday-solutions/hapi-swaggerui/#master", diff --git a/server.js b/server.js index 1922fc5..3d092ec 100644 --- a/server.js +++ b/server.js @@ -13,7 +13,6 @@ import vision from '@hapi/vision'; import rateLimiter from 'hapi-rate-limit'; import rTracer from 'cls-rtracer'; -import cors from 'hapi-cors'; import serverConfig from '@config/server'; import dbConfig from '@config/db'; import hapiPaginationOptions from '@utils/paginationConstants'; @@ -126,14 +125,6 @@ const initServer = async () => { await cachedUser(server); - // Register cors plugin - await server.register({ - plugin: cors, - options: { - origins: ['http://localhost:3000'], - }, - }); - // Register rate limiter plugin await server.register({ plugin: rateLimiter, @@ -230,5 +221,4 @@ if (!isTestEnv() && !isLocalEnv() && cluster.isMaster) { logger().error(error, 'Server startup failed...'); } ); - } diff --git a/utils/cors.js b/utils/cors.js new file mode 100644 index 0000000..f0bff69 --- /dev/null +++ b/utils/cors.js @@ -0,0 +1,70 @@ +import isEmpty from 'lodash/isEmpty'; +import guardAndGet from './env'; + +/** + * + * @param {*} route + * @returns {*} route configuration with cors options + * + * @example + * + * withCORS({ + * method: 'GET', + * path: '/user', + * handler: (){...}, + * options: { + * ..., + * cors: { + * origin: ['https://example.com'] + * } + * } + * }) + * + * Use options.cors to add more CORS options + * @link + * https://hapi.dev/api/?v=20.2.2#-routeoptionscors + * + */ +const withCORS = (route) => { + if (!route || isEmpty(route) || typeof route !== 'object') { + throw new Error('Invalid route config'); + } + + /** + * @description Add the origins that are to be whitelisted for each environment. + * + */ + const ALLOWED_ORIGINS = { + local: ['http://localhost:3000'], + development: [], + production: [], + get getForEnvironment() { + return this[guardAndGet.ENVIRONMENT_NAME]; + }, + }; + + const { options = {} } = route; + const { cors = {} } = options; + + return { + ...route, + options: { + ...options, + /** + * + * @default maxAge = 86400 // 1 day + * @default headers = ['Accept', 'Authorization', 'Content-Type', 'If-None-Match'] + * @default exposedHeaders = ['WWW-Authenticate', 'Server-Authorization'] + * + * + * @description Define cors rules for a route + */ + cors: { + origin: ALLOWED_ORIGINS.getForEnvironment, + ...cors, + }, + }, + }; +}; + +export default withCORS; diff --git a/utils/env.js b/utils/env.js new file mode 100644 index 0000000..49d2ce3 --- /dev/null +++ b/utils/env.js @@ -0,0 +1,19 @@ +const guardAgainst = { + environmentName: process.env.ENVIRONMENT_NAME, + + get ENVIRONMENT_NAME() { + const environment = this.environmentName; + if (!environment) { + throw new Error('ENVIRONMENT_NAME is not set'); + } + + const ValidEnvironmentNames = new Set(['local', 'development', 'prod']); + if (ValidEnvironmentNames.has(environment)) { + return environment; + } + + throw new Error(`Invalid value ENVIRONMENT_NAME = ${environment}`); + }, +}; + +export default guardAgainst; diff --git a/utils/tests/cors.test.js b/utils/tests/cors.test.js new file mode 100644 index 0000000..696c8f6 --- /dev/null +++ b/utils/tests/cors.test.js @@ -0,0 +1,27 @@ +import withCORS from '@utils/cors'; + +beforeEach(() => {}); + +describe('cors', () => { + it('should throw for nullish route configs', () => { + expect(() => withCORS({})).toThrow(); + expect(() => withCORS(1)).toThrow(); + expect(() => withCORS([])).toThrow(); + expect(() => withCORS(null)).toThrow(); + expect(() => withCORS(undefined)).toThrow(); + }); + + it('should use default origin', () => { + expect(withCORS({ options: {} })).toEqual({ + options: { cors: { origin: ['http://localhost:3000'] } }, + }); + }); + + it('should support overriding defaults', () => { + expect( + withCORS({ options: { cors: { origin: ['https://example.com'] } } }) + ).toEqual({ + options: { cors: { origin: ['https://example.com'] } }, + }); + }); +}); diff --git a/utils/tests/env.test.js b/utils/tests/env.test.js new file mode 100644 index 0000000..1ba3980 --- /dev/null +++ b/utils/tests/env.test.js @@ -0,0 +1,22 @@ +afterAll(() => { + process.env.ENVIRONMENT_NAME = 'local'; +}); + +beforeEach(() => { + process.env.ENVIRONMENT_NAME = 'local'; +}); + +describe('env', () => { + it('should get ENVIRONMENT_NAME if it is valid', () => { + process.env.ENVIRONMENT_NAME = 'prod'; + + let guardAgainst; + jest.isolateModules(() => { + guardAgainst = require('../env').default; + }); + expect(() => { + console.log(guardAgainst.ENVIRONMENT_NAME); + }).not.toThrow(); + expect(guardAgainst.ENVIRONMENT_NAME).toBe('prod'); + }); +}); diff --git a/yarn.lock b/yarn.lock index aa785b9..98f9742 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5068,13 +5068,6 @@ hapi-auth-bearer-token@^8.0.0: dependencies: "@hapi/hoek" "^9.0.0" -hapi-cors@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/hapi-cors/-/hapi-cors-1.0.3.tgz#47db547d9d6b3ae52bbeec34b2b6cfc82c5a8892" - integrity sha512-45fkvy13d+Awp25OXuMj8imQSoD3x5SJ99D+P/WBEwruHArpoHdj+zMlrXOoixgo/O281/lls6rAZBnkBtNOpg== - dependencies: - joi "^7.0.1" - hapi-pagination@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/hapi-pagination/-/hapi-pagination-4.0.0.tgz#714c01b8e078da0e7955fc7ccf6101ac84369a35" @@ -5194,16 +5187,6 @@ hbs@^4.1.2: handlebars "4.7.7" walk "2.3.15" -hoek@3.x.x: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-3.0.4.tgz#268adff66bb6695c69b4789a88b1e0847c3f3123" - integrity sha512-VIMFzySNWnvVqBZIWJSHzun/dvtgYYxv0DypA8Mr9ue+kjXyf1mkq4/EOU/a33cIoW+fFyk9+t8W6ZSqucKYpA== - -hoek@4.x.x: - version "4.2.1" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" - integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== - home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -5730,11 +5713,6 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== -isemail@2.x.x: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isemail/-/isemail-2.2.1.tgz#0353d3d9a62951080c262c2aa0a42b8ea8e9e2a6" - integrity sha512-LPjFxaTatluwGAJlGe4FtRdzg0a9KlXrahHoHAR4HwRNf90Ttwi6sOQ9zj+EoCPmk9yyK+WFUqkm0imUo8UJbw== - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -6347,16 +6325,6 @@ joi@^17.6.1: "@sideway/formula" "^3.0.0" "@sideway/pinpoint" "^2.0.0" -joi@^7.0.1: - version "7.3.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-7.3.0.tgz#4d9c9f181830444083665b5b6cd5b8ca6779a5e9" - integrity sha512-7ysLFfGtSg5L1MWnIkJGvJLAsdZPLZK2+qAi+0D9QdsnlPVSk+dBZxEl1ezveJuhEcvZKLK+AZSkmnXeATcB0A== - dependencies: - hoek "3.x.x" - isemail "2.x.x" - moment "2.x.x" - topo "2.x.x" - js-beautify@^1.14.0: version "1.14.5" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.5.tgz#d544e3ac94371af3a4eadec0dea9fc1cd15743b9" @@ -8702,13 +8670,6 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -topo@2.x.x: - version "2.0.2" - resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" - integrity sha512-QMfJ9TC5lKcmLZImOZ/BTSWJeVbay7XK2nlzvFALW3BA5OkvBnbs0poku4EsRpDMndDVnM58EU/8D3ZcoVehWg== - dependencies: - hoek "4.x.x" - toposort-class@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toposort-class/-/toposort-class-1.0.1.tgz#7ffd1f78c8be28c3ba45cd4e1a3f5ee193bd9988"