Merge pull request #195 from hashicorp/feature-change-password-ui
feat: 🎸 add change password route and component
This commit is contained in:
15 files changed
+272
-1
No files matched your search
@@ -0,0 +1,50 @@
|
||||
<Rose::Form
|
||||
@onSubmit={{fn this.submit @submit}}
|
||||
@cancel={{fn this.cancel @cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@error={{@model.errors.base}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="currentPassword"
|
||||
@type="password"
|
||||
@value={{this.currentPassword}}
|
||||
@label={{t "form.label.current_password"}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
autocomplete="current-password"
|
||||
as |field|>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<errors.message>{{error.message}}</errors.message>
|
||||
{{/each}}
|
||||
</field.errors>
|
||||
{{/if}}
|
||||
</form.input>
|
||||
|
||||
<form.input
|
||||
@name="newPassword"
|
||||
@type="password"
|
||||
@value={{this.newPassword}}
|
||||
@label={{t "form.label.new_password"}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
autocomplete="new-password"
|
||||
as |field|>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
<errors.message>{{error.message}}</errors.message>
|
||||
{{/each}}
|
||||
</field.errors>
|
||||
{{/if}}
|
||||
</form.input>
|
||||
|
||||
<form.actions
|
||||
@disabled={{this.cannotSave}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
|
||||
</Rose::Form>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export default class FormAccountPasswordSetPasswordIndexComponent extends Compon
|
||||
*/
|
||||
@computed('password')
|
||||
get cannotSave() {
|
||||
return !(this.password?.length > 0);
|
||||
return !this.password;
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -91,4 +91,8 @@ Router.map(function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.route('account', function() {
|
||||
this.route('change-password');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,5 @@
|
||||
<Form::Account::Password::ChangePassword
|
||||
@model={{@model}}
|
||||
@submit={{route-action "changePassword" @model}}
|
||||
@cancel={{route-action "cancel" @model}}
|
||||
/>
|
||||
@@ -14,6 +14,9 @@
|
||||
@iconOnly={{true}}
|
||||
@icon="user-square-fill" as |dropdown|
|
||||
>
|
||||
<dropdown.link @route="account.change-password">
|
||||
{{t 'actions.change-password'}}
|
||||
</dropdown.link>
|
||||
<dropdown.button
|
||||
{{on "click" (route-action "invalidateSession")}}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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`
|
||||
<Form::Account::Password::ChangePassword
|
||||
@model={{this.model}}
|
||||
@submit={{this.submit}}
|
||||
@cancel={{this.cancel}}
|
||||
/>`);
|
||||
|
||||
assert.ok(find('form'));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user