diff --git a/ui/desktop/app/controllers/.gitkeep b/ui/desktop/app/controllers/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/ui/desktop/app/controllers/application.js b/ui/desktop/app/controllers/application.js
new file mode 100644
index 000000000..0eb5226e0
--- /dev/null
+++ b/ui/desktop/app/controllers/application.js
@@ -0,0 +1,13 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+import config from '../config/environment';
+
+export default class ApplicationController extends Controller {
+ // =services
+
+ @service session;
+
+ // =attributes
+
+ notifyTimeout = config.notifyTimeout;
+}
diff --git a/ui/desktop/app/router.js b/ui/desktop/app/router.js
index 67ec24b1d..98626ddce 100644
--- a/ui/desktop/app/router.js
+++ b/ui/desktop/app/router.js
@@ -15,4 +15,5 @@ Router.map(function () {
});
});
});
+ this.route('origin');
});
diff --git a/ui/desktop/app/routes/application.js b/ui/desktop/app/routes/application.js
index 3c5a507fb..1c1211c89 100644
--- a/ui/desktop/app/routes/application.js
+++ b/ui/desktop/app/routes/application.js
@@ -16,11 +16,37 @@ export default class ApplicationRoute extends Route.extend(
// =services
@service session;
+ @service ipc;
// =attributes
+ /**
+ * @type {string}
+ */
routeIfUnauthenticated = 'index';
+ /**
+ * @type {ApplicationAdapter}
+ */
+ get adapter() {
+ return this.store.adapterFor('application');
+ }
+
+ /**
+ * Check that the origin specified in the renderer matches the origin
+ * reported by the main process. If they differ, update the main process
+ * origin so that the renderer's CSP can be rewritten to allow requests.
+ */
+ async beforeModel() {
+ const rendererOrigin = this.session.data.origin;
+ const mainOrigin = await this.ipc.invoke('getOrigin');
+ const isMatch = rendererOrigin === mainOrigin;
+ this.adapter.host = rendererOrigin;
+ if (rendererOrigin && !isMatch) {
+ await this.ipc.invoke('setOrigin', rendererOrigin);
+ }
+ }
+
/**
* After becoming authenticated, does nothing. This overrides the default
* behavior of the ApplicationRouteMixin, which is to redirect after auth.
diff --git a/ui/desktop/app/routes/index.js b/ui/desktop/app/routes/index.js
index 3ce2a42e1..1bc0a0f02 100644
--- a/ui/desktop/app/routes/index.js
+++ b/ui/desktop/app/routes/index.js
@@ -1,12 +1,23 @@
import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
export default class IndexRoute extends Route {
+
+ // =services
+
+ @service session;
+
// =methods
/**
- * Redirects to scopes route for further processing.
+ * If no origin is specified yet, redirects to origin, otherwise scopes.
*/
redirect() {
- this.replaceWith('scopes');
+ const rendererOrigin = this.session.data.origin;
+ if (!rendererOrigin) {
+ this.replaceWith('origin');
+ } else {
+ this.replaceWith('scopes');
+ }
}
}
diff --git a/ui/desktop/app/routes/origin.js b/ui/desktop/app/routes/origin.js
new file mode 100644
index 000000000..cb5ba961e
--- /dev/null
+++ b/ui/desktop/app/routes/origin.js
@@ -0,0 +1,49 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+
+export default class OriginRoute extends Route {
+
+ // =services
+
+ @service session;
+ @service ipc;
+ @service intl;
+ @service notify;
+
+ // =attributes
+
+ /**
+ * @type {ApplicationAdapter}
+ */
+ get adapter() {
+ return this.store.adapterFor('application');
+ }
+
+ // =methods
+
+ /**
+ * Points the API to the specified origin and persists it to the session,
+ * as well as the main process. When the main process receives the origin,
+ * it is expected that the renderer will be restarted.
+ * @param {string} origin
+ */
+ @action
+ async setOrigin(origin) {
+ this.adapter.host = origin;
+ // Check if we have a Boundary endpoint...
+ try {
+ // If scopes load, this could be a Boundary API
+ await this.store.query('scope', {});
+ this.session.set('data.origin', origin);
+ await this.ipc.invoke('setOrigin', origin);
+ this.replaceWith('index');
+ } catch (e) {
+ // If scopes don't load, we assume this is not a Boundary API
+ const errorMessage =
+ this.intl.t('errors.origin-verification-failed.description');
+ this.notify.error(errorMessage, { closeAfter: null });
+ }
+ }
+
+}
diff --git a/ui/desktop/app/templates/application.hbs b/ui/desktop/app/templates/application.hbs
index 8f1034fb1..eb09a8cef 100644
--- a/ui/desktop/app/templates/application.hbs
+++ b/ui/desktop/app/templates/application.hbs
@@ -6,9 +6,12 @@
-
- {{outlet "header-nav"}}
-
+ {{#if this.session.isAuthenticated}}
+
+ {{outlet "header-nav"}}
+
+ {{/if}}
+
@@ -17,3 +20,14 @@
+
+
+
+ {{message.text}}
+
+
diff --git a/ui/desktop/app/templates/origin.hbs b/ui/desktop/app/templates/origin.hbs
new file mode 100644
index 000000000..5616eccb4
--- /dev/null
+++ b/ui/desktop/app/templates/origin.hbs
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/ui/desktop/config/ember-intl.js b/ui/desktop/config/ember-intl.js
new file mode 100644
index 000000000..783bb4032
--- /dev/null
+++ b/ui/desktop/config/ember-intl.js
@@ -0,0 +1,94 @@
+/*jshint node:true*/
+
+module.exports = function (/* environment */) {
+ return {
+ /**
+ * Merges the fallback locale's translations into all other locales as a
+ * build-time fallback strategy.
+ *
+ * This will **not** prevent missing translation warnings or errors from occurring.
+ * It's meant as safety net when warning are enabled.
+ * When enabled along with `errorOnMissingTranslations` any fallback attempts will result in an error.
+ *
+ * @property fallbackLocale
+ * @type {String?}
+ * @default "null"
+ */
+ fallbackLocale: null,
+
+ /**
+ * Path where translations are kept. This is relative to the project root.
+ * For example, if your translations are an npm dependency, set this to:
+ *`'./node_modules/path/to/translations'`
+ *
+ * @property inputPath
+ * @type {String}
+ * @default "'translations'"
+ */
+ inputPath: 'translations',
+
+ /**
+ * Prevents the translations from being bundled with the application code.
+ * This enables asynchronously loading the translations for the active locale
+ * by fetching them from the asset folder of the build.
+ *
+ * See: https://ember-intl.github.io/ember-intl/docs/guide/asynchronously-loading-translations
+ *
+ * @property publicOnly
+ * @type {Boolean}
+ * @default "false"
+ */
+ publicOnly: false,
+
+ /**
+ * Add the subdirectories of the translations as a namespace for all keys.
+ *
+ * @property wrapTranslationsWithNamespace
+ * @type {Boolean}
+ * @default "false"
+ */
+ wrapTranslationsWithNamespace: true,
+
+ /**
+ * Cause a build error if ICU argument mismatches are detected.
+ *
+ * @property errorOnNamedArgumentMismatch
+ * @type {Boolean}
+ * @default "false"
+ */
+ errorOnNamedArgumentMismatch: false,
+
+ /**
+ * Cause a build error if missing translations are detected.
+ *
+ * See https://ember-intl.github.io/ember-intl/docs/guide/missing-translations#throwing-a-build-error-on-missing-required-translation
+ *
+ * @property errorOnMissingTranslations
+ * @type {Boolean}
+ * @default "false"
+ */
+ errorOnMissingTranslations: false,
+
+ /**
+ * removes empty translations from the build output.
+ *
+ * @property stripEmptyTranslations
+ * @type {Boolean}
+ * @default "false"
+ */
+ stripEmptyTranslations: false,
+
+ /**
+ * Filter missing translations to ignore expected missing translations.
+ *
+ * See https://ember-intl.github.io/ember-intl/docs/guide/missing-translations#requiring-translations
+ *
+ * @property requiresTranslation
+ * @type {Function}
+ * @default "function(key,locale) { return true }"
+ */
+ requiresTranslation(/* key, locale */) {
+ return true;
+ },
+ };
+};
diff --git a/ui/desktop/config/environment.js b/ui/desktop/config/environment.js
index e358c0cac..57a61e665 100644
--- a/ui/desktop/config/environment.js
+++ b/ui/desktop/config/environment.js
@@ -26,12 +26,16 @@ module.exports = function (environment) {
// when it is created
},
+ isElectron: Boolean(process.env.EMBER_CLI_ELECTRON),
+
api: {
host: API_HOST,
namespace: 'v1',
},
appName: APP_NAME,
+
+ notifyTimeout: 4000,
};
if (environment === 'development') {
@@ -52,6 +56,9 @@ module.exports = function (environment) {
ENV.APP.rootElement = '#ember-testing';
ENV.APP.autoboot = false;
+
+ // Notification timeout should be 0 for fast tests
+ ENV.notifyTimeout = 0;
}
if (environment === 'production') {
diff --git a/ui/desktop/electron-app/src/handlers.js b/ui/desktop/electron-app/src/handlers.js
index f5bcf175b..e6a35ba9e 100644
--- a/ui/desktop/electron-app/src/handlers.js
+++ b/ui/desktop/electron-app/src/handlers.js
@@ -1,17 +1,30 @@
const { ipcMain } = require('electron');
const isDev = require('electron-is-dev');
-
const log = (method, request) => {
if (isDev) console.log(`[ipc-main](${method}): `, request);
}
+let origin = 'serve://boundary';
/**
- * A demonstration method `getFoobars` which returns an object with a message.
+ * Returns the current runtime origin, which is used by the main thread to
+ * rewrite the CSP to allow requests.
*/
-ipcMain.handle('getFoobars', async (event, request) => {
- log('getFoobars', request);
- return {
- message: 'this is a response from an ipcMain handler'
- };
+ipcMain.handle('getOrigin', async (event, request) => {
+ log('getOrigin', request);
+ return origin;
+});
+
+/**
+ * TODO: sets the main process origin to the specified value. This value will
+ * be used to rewrite the CSP to allow requests to that origin. If the origin
+ * was not previously set, the main process will set it and restart
+ * the renderer.
+ *
+ * For now, we update the internal origin but do not yet rewrite the CSP.
+ */
+ipcMain.handle('setOrigin', async (event, request) => {
+ log('setOrigin', request);
+ origin = request.data;
+ return origin;
});
diff --git a/ui/desktop/tests/unit/controllers/application-test.js b/ui/desktop/tests/unit/controllers/application-test.js
new file mode 100644
index 000000000..e4b814b28
--- /dev/null
+++ b/ui/desktop/tests/unit/controllers/application-test.js
@@ -0,0 +1,12 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Controller | application', function(hooks) {
+ setupTest(hooks);
+
+ // TODO: Replace this with your real tests.
+ test('it exists', function(assert) {
+ let controller = this.owner.lookup('controller:application');
+ assert.ok(controller);
+ });
+});
diff --git a/ui/desktop/tests/unit/routes/origin-test.js b/ui/desktop/tests/unit/routes/origin-test.js
new file mode 100644
index 000000000..93e187a4c
--- /dev/null
+++ b/ui/desktop/tests/unit/routes/origin-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | origin', function(hooks) {
+ setupTest(hooks);
+
+ test('it exists', function(assert) {
+ let route = this.owner.lookup('route:origin');
+ assert.ok(route);
+ });
+});
diff --git a/ui/desktop/translations/form/en-us.yaml b/ui/desktop/translations/form/en-us.yaml
index b126880d9..a4c393943 100644
--- a/ui/desktop/translations/form/en-us.yaml
+++ b/ui/desktop/translations/form/en-us.yaml
@@ -11,3 +11,6 @@ type:
help: The type of the resource, to help differentiate schemas
project:
label: Project
+origin:
+ label: Origin
+ description: The Boundary server origin (e.g. protocol://host)