diff --git a/ui/core/app/components/form/account/password/change-password/index.hbs b/ui/core/app/components/form/account/password/change-password/index.hbs
new file mode 100644
index 000000000..a0d85390d
--- /dev/null
+++ b/ui/core/app/components/form/account/password/change-password/index.hbs
@@ -0,0 +1,50 @@
+
+
+
+ {{#if @model.errors.name}}
+
+ {{#each @model.errors.name as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{#if @model.errors.name}}
+
+ {{#each @model.errors.name as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+
+
diff --git a/ui/core/app/components/form/account/password/change-password/index.js b/ui/core/app/components/form/account/password/change-password/index.js
new file mode 100644
index 000000000..0375feaea
--- /dev/null
+++ b/ui/core/app/components/form/account/password/change-password/index.js
@@ -0,0 +1,73 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { computed } from '@ember/object';
+import { action } from '@ember/object';
+import { next } from '@ember/runloop';
+
+export default class FormAccountPasswordChangePasswordIndexComponent extends Component {
+
+ // =properties
+
+ /**
+ * @type {string}
+ */
+ @tracked currentPassword;
+
+ /**
+ * @type {string}
+ */
+ @tracked newPassword;
+
+ /**
+ * @type {boolean}
+ */
+ @computed('currentPassword', 'newPassword')
+ get canSave() {
+ return this.currentPassword && this.newPassword;
+ }
+
+ /**
+ * @type {boolean}
+ */
+ @computed('canSave')
+ get cannotSave() {
+ return !this.canSave;
+ }
+
+ // =methods
+
+ /**
+ * Unsets the password fields.
+ */
+ resetPasswords() {
+ this.currentPassword = null;
+ this.newPassword = null;
+ }
+
+ // =actions
+
+ /**
+ * Call passed submit function with passwords.
+ * Unset passwords before callack.
+ * @param {function} fn
+ */
+ @action
+ submit(fn) {
+ const currentPassword = this.currentPassword;
+ const newPassword = this.newPassword;
+ next(() => this.resetPasswords());
+ fn(currentPassword, newPassword);
+ }
+
+ /**
+ * Call passed cancel function.
+ * Unset passwords before callback.
+ * @param {function} fn
+ */
+ @action
+ cancel(fn) {
+ this.resetPasswords();
+ fn();
+ }
+
+}
diff --git a/ui/core/app/components/form/account/password/set-password/index.js b/ui/core/app/components/form/account/password/set-password/index.js
index 124f783b9..5752ea980 100644
--- a/ui/core/app/components/form/account/password/set-password/index.js
+++ b/ui/core/app/components/form/account/password/set-password/index.js
@@ -18,7 +18,7 @@ export default class FormAccountPasswordSetPasswordIndexComponent extends Compon
*/
@computed('password')
get cannotSave() {
- return !(this.password?.length > 0);
+ return !this.password;
}
// =methods
diff --git a/ui/core/app/router.js b/ui/core/app/router.js
index dc30cf017..204ced768 100644
--- a/ui/core/app/router.js
+++ b/ui/core/app/router.js
@@ -91,4 +91,8 @@ Router.map(function () {
});
});
});
+
+ this.route('account', function() {
+ this.route('change-password');
+ });
});
diff --git a/ui/core/app/routes/account.js b/ui/core/app/routes/account.js
new file mode 100644
index 000000000..34e9a9e11
--- /dev/null
+++ b/ui/core/app/routes/account.js
@@ -0,0 +1,19 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+export default class AccountRoute extends Route {
+
+ // =services
+
+ @service session;
+
+ // =methods
+
+ /**
+ * If arriving here unauthenticated, redirect to index for further processing.
+ */
+ beforeModel() {
+ if (!this.session.isAuthenticated) this.transitionTo('index');
+ }
+
+}
diff --git a/ui/core/app/routes/account/change-password.js b/ui/core/app/routes/account/change-password.js
new file mode 100644
index 000000000..858230e9c
--- /dev/null
+++ b/ui/core/app/routes/account/change-password.js
@@ -0,0 +1,60 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+
+/**
+ * The change password flow is associated only with password-type auth methods.
+ */
+export default class AccountChangePasswordRoute extends Route {
+
+ // =services
+
+ @service session;
+ @service intl;
+ @service notify;
+
+ // =methods
+
+ /**
+ * Loads the account associated with the auth token.
+ * @return {?AccountModel}
+ */
+ model() {
+ const account_id = this.session?.data?.authenticated?.account_id;
+ return this.store.findRecord('account', account_id);
+ }
+
+ // =actions
+
+ /**
+ * Changes the password on the account and then redirects to the index route.
+ * @param {AccountModel} account
+ * @param {string} currentPassword
+ * @param {string} newPassword
+ */
+ @action
+ async changePassword(account, currentPassword, newPassword) {
+ try {
+ await account.changePassword(currentPassword, newPassword);
+ // Transition to index after success. We ignore errors in the transition,
+ // since the index route is responsible for additional
+ // redirects depending on the authentication context. These additional
+ // redirects may abort this initial transition, resulting in a
+ // TransitionAborted error, which we do not want to show to the user.
+ await this.replaceWith('index').catch(() => {});
+ this.notify.success(this.intl.t('notify.save-success'));
+ } catch (error) {
+ // TODO: replace with translated strings
+ this.notify.error(error.message, { closeAfter: null });
+ }
+ }
+
+ /**
+ * On cancel, redirects to index route for further processing.
+ */
+ @action
+ cancel() {
+ this.replaceWith('index');
+ }
+
+}
diff --git a/ui/core/app/templates/account.hbs b/ui/core/app/templates/account.hbs
new file mode 100644
index 000000000..e2147cab0
--- /dev/null
+++ b/ui/core/app/templates/account.hbs
@@ -0,0 +1 @@
+{{outlet}}
\ No newline at end of file
diff --git a/ui/core/app/templates/account/change-password.hbs b/ui/core/app/templates/account/change-password.hbs
new file mode 100644
index 000000000..ae761d18a
--- /dev/null
+++ b/ui/core/app/templates/account/change-password.hbs
@@ -0,0 +1,5 @@
+
diff --git a/ui/core/app/templates/application.hbs b/ui/core/app/templates/application.hbs
index 2422a624e..2f295e9b4 100644
--- a/ui/core/app/templates/application.hbs
+++ b/ui/core/app/templates/application.hbs
@@ -14,6 +14,9 @@
@iconOnly={{true}}
@icon="user-square-fill" as |dropdown|
>
+
+ {{t 'actions.change-password'}}
+
diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js
index 7f2ea5ba0..e7044298f 100644
--- a/ui/core/mirage/config.js
+++ b/ui/core/mirage/config.js
@@ -70,6 +70,10 @@ export default function() {
if (method === 'set-password') {
updatedAttrs.attributes.password = attrs.password;
}
+ // Set new password
+ if (method === 'change-password') {
+ updatedAttrs.attributes.password = attrs.new_password;
+ }
return account.update(updatedAttrs);
});
diff --git a/ui/core/mirage/route-handlers/auth.js b/ui/core/mirage/route-handlers/auth.js
index a0e409646..96813a7d5 100644
--- a/ui/core/mirage/route-handlers/auth.js
+++ b/ui/core/mirage/route-handlers/auth.js
@@ -37,6 +37,7 @@ export function authHandler({ scopes, authMethods }, request) {
scope: scopeAttrs,
id: 'token123',
token: 'thetokenstring',
+ account_id: '1',
user_id: 'user123',
auth_method_id: 'authmethod123',
created_time: '',
diff --git a/ui/core/tests/integration/components/form/account/password/change-password-test.js b/ui/core/tests/integration/components/form/account/password/change-password-test.js
new file mode 100644
index 000000000..54a6f61dd
--- /dev/null
+++ b/ui/core/tests/integration/components/form/account/password/change-password-test.js
@@ -0,0 +1,26 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module(
+ 'Integration | Component | form/account/password/change-password',
+ function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function (assert) {
+ this.submit = () => {};
+ this.cancel = () => {};
+ this.model = { type: 'password' };
+
+ await render(hbs`
+ `);
+
+ assert.ok(find('form'));
+ });
+ }
+);
diff --git a/ui/core/tests/unit/routes/account-test.js b/ui/core/tests/unit/routes/account-test.js
new file mode 100644
index 000000000..ab640d01d
--- /dev/null
+++ b/ui/core/tests/unit/routes/account-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | account', function(hooks) {
+ setupTest(hooks);
+
+ test('it exists', function(assert) {
+ let route = this.owner.lookup('route:account');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/tests/unit/routes/account/change-password-test.js b/ui/core/tests/unit/routes/account/change-password-test.js
new file mode 100644
index 000000000..ee6856d35
--- /dev/null
+++ b/ui/core/tests/unit/routes/account/change-password-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | account/change-password', function(hooks) {
+ setupTest(hooks);
+
+ test('it exists', function(assert) {
+ let route = this.owner.lookup('route:account/change-password');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml
index 301ac40a2..e458c2b50 100644
--- a/ui/core/translations/en-us.yaml
+++ b/ui/core/translations/en-us.yaml
@@ -30,6 +30,7 @@ actions:
add-hosts: Add Hosts
add-account: Create Account
set-password: Set Password
+ change-password: Change Password
auth:
titles:
no-scopes: No Scopes Available
@@ -154,6 +155,8 @@ form:
protocol: Protocol
actions: Actions
default_port: Default Port
+ current_password: Current Password
+ new_password: New Password
errors:
titles:
unauthenticated-error: You are not signed in