diff --git a/src/lib/app.ts b/src/lib/app.ts index eb6353e257..6a31ba5223 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -1,4 +1,5 @@ import { publicFolder } from 'unleash-frontend'; +import fs from 'fs'; import EventEmitter from 'events'; import express from 'express'; import cors from 'cors'; @@ -22,6 +23,7 @@ import demoAuthentication from './middleware/demo-authentication'; import ossAuthentication from './middleware/oss-authentication'; import noAuthentication from './middleware/no-authentication'; import secureHeaders from './middleware/secure-headers'; +import { rewriteHTML } from './util/rewriteHTML'; export default function getApp( config: IUnleashConfig, @@ -33,6 +35,12 @@ export default function getApp( const baseUriPath = config.server.baseUriPath || ''; + let indexHTML = fs + .readFileSync(path.join(publicFolder, 'index.html')) + .toString(); + + indexHTML = rewriteHTML(indexHTML, baseUriPath); + app.set('trust proxy', true); app.disable('x-powered-by'); app.set('port', config.server.port); @@ -57,7 +65,8 @@ export default function getApp( app.use(secureHeaders(config)); app.use(express.urlencoded({ extended: true })); app.use(favicon(path.join(publicFolder, 'favicon.ico'))); - app.use(baseUriPath, express.static(publicFolder)); + + app.use(baseUriPath, express.static(publicFolder, { index: false })); if (config.enableOAS) { app.use(`${baseUriPath}/oas`, express.static('docs/api/oas')); @@ -103,7 +112,7 @@ export default function getApp( ); if (typeof config.preRouterHook === 'function') { - config.preRouterHook(app); + config.preRouterHook(app, config, services, stores); } // Setup API routes @@ -113,6 +122,18 @@ export default function getApp( app.use(errorHandler()); } + app.get(`${baseUriPath}`, (req, res) => { + res.send(indexHTML); + }); + + app.get('*', (req, res) => { + if (req.path.includes('api')) { + res.status(404).send(); + } + + res.send(indexHTML); + }); + return app; } module.exports = getApp; diff --git a/src/lib/util/rewriteHTML.test.ts b/src/lib/util/rewriteHTML.test.ts new file mode 100644 index 0000000000..54dd557929 --- /dev/null +++ b/src/lib/util/rewriteHTML.test.ts @@ -0,0 +1,67 @@ +import { rewriteHTML } from './rewriteHTML'; +import test from 'ava'; + +const input = ` + + + + + + + + + + Unleash - Enterprise ready feature toggles + + + + +
+ + + +`; + +test('rewriteHTML substitutes meta tag with existing rewrite value', t => { + const result = rewriteHTML(input, '/hosted'); + t.true(result.includes(``)); +}); + +test('rewriteHTML substitutes meta tag with empty value', t => { + const result = rewriteHTML(input, ''); + t.true(result.includes(``)); +}); + +test('rewriteHTML substitutes asset paths correctly with baseUriPath', t => { + const result = rewriteHTML(input, '/hosted'); + t.true( + result.includes( + ``, + ), + ); + t.true( + result.includes( + ` `, + ), + ); +}); + +test('rewriteHTML substitutes asset paths correctly without baseUriPath', t => { + const result = rewriteHTML(input, ''); + t.true( + result.includes( + ``, + ), + ); + t.true( + result.includes( + ` `, + ), + ); +}); diff --git a/src/lib/util/rewriteHTML.ts b/src/lib/util/rewriteHTML.ts new file mode 100644 index 0000000000..641c71ac64 --- /dev/null +++ b/src/lib/util/rewriteHTML.ts @@ -0,0 +1,7 @@ +export const rewriteHTML = (input: string, rewriteValue: string): string => { + let result = input; + result = result.replace(/::baseUriPath::/gi, rewriteValue); + result = result.replace(/\/static/gi, `${rewriteValue}/static`); + + return result; +}; diff --git a/src/test/e2e/helpers/test-helper.js b/src/test/e2e/helpers/test-helper.js index 8ce8af1a78..e338a91e1c 100644 --- a/src/test/e2e/helpers/test-helper.js +++ b/src/test/e2e/helpers/test-helper.js @@ -9,7 +9,12 @@ const { createTestConfig } = require('../../config/test-config'); const { IAuthType } = require('../../../lib/types/option'); const { createServices } = require('../../../lib/services'); -function createApp(stores, adminAuthentication = IAuthType.NONE, preHook) { +function createApp( + stores, + adminAuthentication = IAuthType.NONE, + preHook, + customOptions, +) { const config = createTestConfig({ authentication: { type: adminAuthentication, @@ -18,6 +23,7 @@ function createApp(stores, adminAuthentication = IAuthType.NONE, preHook) { server: { unleashUrl: 'http://localhost:4242', }, + ...customOptions, }); const services = createServices(stores, config); // TODO: use create from server-impl instead? @@ -39,4 +45,14 @@ module.exports = { const app = createApp(stores, IAuthType.CUSTOM, preHook); return supertest.agent(app); }, + async setupAppWithBaseUrl(stores) { + const app = createApp(stores, undefined, undefined, { + server: { + unleashUrl: 'http://localhost:4242', + basePathUri: '/hosted', + }, + }); + + return supertest.agent(app); + }, }; diff --git a/src/test/e2e/routes/routes.test.ts b/src/test/e2e/routes/routes.test.ts new file mode 100644 index 0000000000..c170e8953c --- /dev/null +++ b/src/test/e2e/routes/routes.test.ts @@ -0,0 +1,40 @@ +import test, { before } from 'ava'; +import { setupAppWithBaseUrl } from '../helpers/test-helper'; + +import dbInit from '../helpers/database-init'; + +let db; +let stores; + +before(async () => { + db = await dbInit('custom_auth_serial'); + stores = db.stores; +}); + +test.after.always(async () => { + await db.destroy(); +}); + +test('hitting a baseUri path returns HTML document', async t => { + t.plan(0); + const request = await setupAppWithBaseUrl(stores); + await request + .get('/hosted') + .expect(200) + .expect('Content-Type', 'text/html; charset=utf-8'); +}); + +test('hitting an api path that does not exist returns 404', async t => { + t.plan(0); + const request = await setupAppWithBaseUrl(stores); + await request.get('/hosted/api/i-dont-exist').expect(404); +}); + +test('hitting a non-api returns HTML document', async t => { + t.plan(0); + const request = await setupAppWithBaseUrl(stores); + await request + .get('/hosted/i-dont-exist') + .expect(200) + .expect('Content-Type', 'text/html; charset=utf-8'); +});