refactor: 💡 remove route-action usage for scopes in desktop (#2447)

* refactor: 💡 remove route-action usage for scopes in desktop

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12621
This commit is contained in:
Lisbet Alvarez authored and GitHub committed 2024-09-17 16:36:47 -07:00
1 parent 98cd9dc1ec
commit 8605a54221
21 files changed
+395 -141

No files matched your search

+38
View File
@@ -5,10 +5,48 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class ApplicationController extends Controller {
// =services
@service ipc;
@service session;
@service clusterUrl;
@service flashMessages;
// =actions
/**
* Delegates invalidation to the session service.
*/
@action
invalidateSession() {
this.session.invalidate();
}
/**
* Disconnects from clusterUrl and invalidates session, thereby resetting
* the client and reloading to the onboarding clusterUrl screen.
*/
@action
disconnect() {
this.clusterUrl.resetClusterUrl();
this.invalidateSession();
}
@action
minimize() {
this.ipc.invoke('minimizeWindow');
}
@action
toggleFullScreen() {
this.ipc.invoke('toggleFullscreenWindow');
}
@action
close() {
this.ipc.invoke('closeWindow');
}
}
+34
View File
@@ -0,0 +1,34 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
export default class ClusterUrlController extends Controller {
// =services
@service router;
@service('cluster-url') clusterUrlService;
// =actions
/**
* Points the API to the specified clusterUrl. When the main process receives
* the clusterUrl, it is expected that the renderer will be restarted.
* @param {string} clusterUrl
*/
@action
@loading
@notifyError(() => 'errors.cluster-url-verification-failed.description', {
catch: true,
})
async setClusterUrl(clusterUrl) {
await this.clusterUrlService.setClusterUrl(clusterUrl);
this.router.replaceWith('index');
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller, { inject as controller } from '@ember/controller';
export default class ErrorController extends Controller {
@controller('application') appUtilities;
}
@@ -0,0 +1,81 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { getOwner } from '@ember/application';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
export default class ScopesScopeAuthenticateMethodIndexController extends Controller {
// =services
@service session;
@service ipc;
@service intl;
@service router;
// =methods
/**
* Kicks off the OIDC auth flow by opening a new window to the provider.
* @param {string} authenticatorName
* @param {object} options
*/
async startOIDCAuthentication(authenticatorName, options) {
const oidc = getOwner(this).lookup(authenticatorName);
// TODO: delegate this call from the session service so that we don't have
// to look up the authenticator directly
const json = await oidc.startAuthentication(options);
await this.openExternalOIDCFlow(json.attributes.auth_url);
}
/**
* Opens the specified URL in a new tab or window. By default this uses
* `window.open`, but may be overridden.
* @param {string} url
*/
async openExternalOIDCFlow(url) {
await this.ipc.invoke('openExternal', url);
}
// =actions
/**
* Delegates authentication to an authenticator of type matching the current
* auth method. If authentication fails, notifies with an alert.
*/
@action
@loading
@notifyError(() => 'errors.authentication-failed.title', {
sticky: false,
catch: true,
})
async authenticate(authMethod, creds) {
const scope = authMethod.scope;
const authenticatorName = `authenticator:${authMethod.type}`;
const requestCookies = false;
switch (authMethod.type) {
case 'password':
case 'ldap':
await this.session.authenticate(
authenticatorName,
creds,
requestCookies,
{ scope, authMethod },
);
this.router.transitionTo('index');
break;
case 'oidc':
await this.startOIDCAuthentication(authenticatorName, {
scope,
authMethod,
});
this.router.transitionTo('scopes.scope.authenticate.method.oidc');
break;
}
}
}
@@ -14,7 +14,7 @@ export default class ScopesScopeAuthenticateMethodOidcController extends Control
// =attributes
/**
* Authetnication URL for the pending OIDC flow, if any.
* Authentication URL for the pending OIDC flow, if any.
* @type {?string}
*/
get authURL() {
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeProjectsErrorController extends Controller {
@controller('application') appUtilities;
}
-33
View File
@@ -61,24 +61,6 @@ export default class ApplicationRoute extends Route {
// =actions
/**
* Delegates invalidation to the session service.
*/
@action
invalidateSession() {
this.session.invalidate();
}
/**
* Disconnects from clusterUrl and invalidates session, thereby resetting
* the client and reloading to the onboarding clusterUrl screen.
*/
@action
disconnect() {
this.clusterUrl.resetClusterUrl();
this.invalidateSession();
}
/**
* Invalidates the session if a 401 error occurs and returns false to
* prevent further error handling.
@@ -121,19 +103,4 @@ export default class ApplicationRoute extends Route {
rootEl.classList.remove('rose-theme-light');
}
}
@action
minimize() {
this.ipc.invoke('minimizeWindow');
}
@action
toggleFullScreen() {
this.ipc.invoke('toggleFullscreenWindow');
}
@action
close() {
this.ipc.invoke('closeWindow');
}
}
+2 -28
View File
@@ -5,8 +5,6 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import config from '../config/environment';
export default class ClusterUrlRoute extends Route {
@@ -47,37 +45,13 @@ export default class ClusterUrlRoute extends Route {
super.setupController(...arguments);
let clusterUrl = this.clusterUrl.rendererClusterUrl;
// If clusterUrl is unset and this is a development environment,
// autoset the clusterUrl field of the UI for better DX.
// auto-set the clusterUrl field of the UI for better DX.
// The controller URL is almost always the same as the current window when
// using mocks, and this makes development more rapid since developers
// do not need to fill a clusterUrl on every session.
if (!clusterUrl && config.autoOrigin) {
clusterUrl = this.window.location.origin;
}
controller.setProperties({ clusterUrl: clusterUrl });
}
/**
* Points the API to the specified clusterUrl. When the main process receives
* the clusterUrl, it is expected that the renderer will be restarted.
* @param {string} clusterUrl
*/
@action
@loading
async setClusterUrl(clusterUrl) {
try {
await this.clusterUrl.setClusterUrl(clusterUrl);
this.router.replaceWith('index');
} catch (e) {
// If scopes do not load, we assume this is not a Boundary API
const errorMessage = this.intl.t(
'errors.cluster-url-verification-failed.description',
);
this.flashMessages.danger(errorMessage, {
notificationType: 'error',
sticky: true,
dismiss: (flash) => flash.destroyMessage(),
});
}
controller.setProperties({ clusterUrl });
}
}
@@ -5,19 +5,11 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { getOwner } from '@ember/application';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
export default class ScopesScopeAuthenticateMethodRoute extends Route {
// =services
@service store;
@service session;
@service ipc;
@service intl;
@service router;
// =methods
@@ -30,62 +22,4 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
const adapterOptions = { scopeID: this.modelFor('scopes.scope').id };
return this.store.peekRecord('auth-method', id, adapterOptions);
}
/**
*
*/
async startOIDCAuthentication(authenticatorName, options) {
const oidc = getOwner(this).lookup(authenticatorName);
// TODO: delegate this call from the session service so that we don't have
// to look up the authenticator directly
const json = await oidc.startAuthentication(options);
await this.openExternalOIDCFlow(json.attributes.auth_url);
}
/**
* Opens the specified URL in a new tab or window. By default this uses
* `window.open`, but may be overriden.
* @param {string} url
*/
async openExternalOIDCFlow(url) {
await this.ipc.invoke('openExternal', url);
}
// =actions
/**
* Delegates authentication to an authenticator of type matching the current
* auth method. If authentication fails, notifies with an alert.
*/
@action
@loading
@notifyError(() => 'errors.authentication-failed.title', {
sticky: false,
catch: true,
})
async authenticate(creds) {
const scope = this.modelFor('scopes.scope');
const authMethod = this.modelFor('scopes.scope.authenticate.method');
const authenticatorName = `authenticator:${authMethod.type}`;
const requestCookies = false;
switch (authMethod.type) {
case 'password':
case 'ldap':
await this.session.authenticate(
authenticatorName,
creds,
requestCookies,
{ scope, authMethod },
);
this.router.transitionTo('index');
break;
case 'oidc':
await this.startOIDCAuthentication(authenticatorName, {
scope,
authMethod,
});
this.router.transitionTo('scopes.scope.authenticate.method.oidc');
break;
}
}
}
+4 -4
View File
@@ -26,21 +26,21 @@
@style='warning'
class='button-window-close'
title={{t 'actions.close'}}
{{on 'click' (route-action 'close')}}
{{on 'click' this.close}}
/>
<Rose::Button
@iconOnly='flight-icons/svg/square-16'
@style='secondary'
class='button-window-fullscreen'
title={{t 'actions.fullscreen'}}
{{on 'click' (route-action 'toggleFullScreen')}}
{{on 'click' this.toggleFullScreen}}
/>
<Rose::Button
@iconOnly='flight-icons/svg/minus-16'
@style='secondary'
class='button-window-minimize'
title={{t 'actions.minimize'}}
{{on 'click' (route-action 'minimize')}}
{{on 'click' this.minimize}}
/>
{{/if}}
@@ -50,7 +50,7 @@
@icon='flight-icons/svg/user-circle-16'
as |dropdown|
>
<dropdown.button {{on 'click' (route-action 'invalidateSession')}}>
<dropdown.button {{on 'click' this.invalidateSession}}>
{{t 'actions.deauthenticate'}}
</dropdown.button>
+1 -1
View File
@@ -10,7 +10,7 @@
>
<Rose::Form
class='full-width'
@onSubmit={{route-action 'setClusterUrl' this.clusterUrl}}
@onSubmit={{fn this.setClusterUrl this.clusterUrl}}
@cancel={{false}}
@disabled={{(is-loading)}}
as |form|
+1 -1
View File
@@ -12,7 +12,7 @@
</Rose::Layout::Centered>
<Rose::Layout::Centered>
<Rose::Button @style='primary' {{on 'click' (route-action 'disconnect')}}>
<Rose::Button @style='primary' {{on 'click' this.appUtilities.disconnect}}>
{{t 'actions.disconnect'}}
</Rose::Button>
</Rose::Layout::Centered>
@@ -5,6 +5,6 @@
<Form::Authenticate
@model={{@model}}
@submit={{route-action 'authenticate'}}
@submit={{fn this.authenticate @model}}
@disabled={{(is-loading)}}
/>
@@ -21,7 +21,7 @@
</Rose::Layout::Centered>
<Rose::Layout::Centered>
<Rose::Button @style='primary' {{on 'click' (route-action 'disconnect')}}>
<Rose::Button @style='primary' {{on 'click' this.appUtilities.disconnect}}>
{{t 'actions.disconnect'}}
</Rose::Button>
</Rose::Layout::Centered>
+4 -2
View File
@@ -21,7 +21,9 @@ class MockIPC {
return this.clusterUrl;
}
resetClusterUrl() {}
resetClusterUrl() {
this.clusterUrl = null;
}
hasMacOSChrome() {}
showWindowActions() {}
addTokenToDaemons() {}
@@ -40,7 +42,7 @@ class MockIPC {
* to support the IPC mechanism.
*
* This mock is necessary to guarantee synchronous and deterministic
* operation. The window's own API is evented and asychronous, which
* operation. The window's own API is evented and asynchronous, which
* cannot be relied on for testing purposes.
*
* @todo refactor mock postMessage and MessageChannel for reusability
@@ -5,13 +5,57 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { authenticateSession } from 'ember-simple-auth/test-support';
import WindowMockIPC from '../../helpers/window-mock-ipc';
module('Unit | Controller | application', function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
let controller;
let session;
let clusterUrl;
let mockIPC;
const setupMockIpc = (test) => {
test.owner.register('service:browser/window', WindowMockIPC);
mockIPC = test.owner.lookup('service:browser/window').mockIPC;
};
hooks.beforeEach(async function () {
authenticateSession({});
controller = this.owner.lookup('controller:application');
session = this.owner.lookup('service:session');
clusterUrl = this.owner.lookup('service:cluster-url');
setupMockIpc(this);
});
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:application');
assert.ok(controller);
assert.ok(controller.minimize);
assert.ok(controller.toggleFullScreen);
assert.ok(controller.close);
});
test('invalidateSession action de-authenticates a user', async function (assert) {
assert.true(session.isAuthenticated);
await controller.invalidateSession();
assert.false(session.isAuthenticated);
});
test('disconnect action de-authenticates a user and resets cluster url', async function (assert) {
const url = 'http://localhost:9200';
await clusterUrl.setClusterUrl(url);
assert.true(session.isAuthenticated);
assert.strictEqual(clusterUrl.rendererClusterUrl, url);
assert.strictEqual(mockIPC.clusterUrl, url);
await controller.disconnect();
assert.false(session.isAuthenticated);
assert.notOk(clusterUrl.rendererClusterUrl);
assert.notOk(mockIPC.clusterUrl);
});
});
@@ -0,0 +1,46 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { setupIntl } from 'ember-intl/test-support';
import WindowMockIPC from '../../helpers/window-mock-ipc';
module('Unit | Controller | cluster-url', function (hooks) {
setupTest(hooks);
setupIntl(hooks, 'en-us');
let controller;
let clusterUrl;
let mockIPC;
const setupMockIpc = (test) => {
test.owner.register('service:browser/window', WindowMockIPC);
mockIPC = test.owner.lookup('service:browser/window').mockIPC;
};
hooks.beforeEach(async function () {
authenticateSession({});
controller = this.owner.lookup('controller:cluster-url');
clusterUrl = this.owner.lookup('service:cluster-url');
setupMockIpc(this);
});
test('it exists', function (assert) {
assert.ok(controller);
});
test('setClusterUrl action de-authenticates a user and resets cluster url', async function (assert) {
const newClusterUrl = 'http://localhost:9200';
assert.notEqual(clusterUrl.rendererClusterUrl, newClusterUrl);
assert.notEqual(mockIPC.clusterUrl, newClusterUrl);
await controller.setClusterUrl(newClusterUrl);
assert.strictEqual(clusterUrl.rendererClusterUrl, newClusterUrl);
assert.strictEqual(mockIPC.clusterUrl, newClusterUrl);
});
});
@@ -0,0 +1,17 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Controller | error', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let controller = this.owner.lookup('controller:error');
assert.ok(controller);
assert.ok(controller.appUtilities);
});
});
@@ -0,0 +1,79 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { setupIntl } from 'ember-intl/test-support';
import { TYPE_AUTH_METHOD_PASSWORD } from 'api/models/auth-method';
module(
'Unit | Controller | scopes/scope/authenticate/method/index',
function (hooks) {
setupTest(hooks);
setupMirage(hooks);
setupIntl(hooks, 'en-us');
let controller;
let store;
let session;
const instances = {
scopes: {
global: null,
},
authMethod: null,
};
hooks.beforeEach(async function () {
authenticateSession({});
controller = this.owner.lookup(
'controller:scopes/scope/authenticate/method/index',
);
store = this.owner.lookup('service:store');
session = this.owner.lookup('service:session');
instances.scopes.global = this.server.create('scope', {
id: 'global',
type: 'global',
});
instances.authMethod = this.server.create('auth-method', {
scope: instances.scopes.global,
type: TYPE_AUTH_METHOD_PASSWORD,
});
});
test('it exists', function (assert) {
assert.ok(controller);
});
test('authenticate action saves login information', async function (assert) {
const identification = 'admin123';
const authMethod = await store.findRecord(
'auth-method',
instances.authMethod.id,
);
const { authenticator: authBefore, username: usernameBefore } =
session.data.authenticated;
assert.notOk(usernameBefore);
assert.strictEqual(authBefore, 'authenticator:test');
await controller.authenticate(authMethod, {
identification,
password: 'password',
});
const { authenticator: authAfter, username: usernameAfter } =
session.data.authenticated;
assert.strictEqual(
authAfter,
`authenticator:${instances.authMethod.type}`,
);
assert.strictEqual(usernameAfter, identification);
});
},
);
@@ -11,7 +11,6 @@ module(
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/authenticate/method/oidc',
@@ -0,0 +1,19 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'desktop/tests/helpers';
module('Unit | Controller | scopes/scope/projects/error', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/projects/error',
);
assert.ok(controller);
assert.ok(controller.appUtilities);
});
});