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');
+});