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)