Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve New User password validation #8176

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
5 changes: 3 additions & 2 deletions packages/frontend/app/components/new-user.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
{{/if}}
</div>
<div class="item" data-test-password>
<label for="password-{{templateId}}">
{{!-- <label for="password-{{templateId}}">
{{t "general.password"}}:
</label>
<input
Expand All @@ -142,7 +142,8 @@
{{on "input" (pick "target.value" (set this "password"))}}
{{on "keyup" (queue (fn this.addErrorDisplayFor "password") (perform this.saveOrCancel))}}
/>
<ValidationError @validatable={{this}} @property="password" />
<ValidationError @validatable={{this}} @property="password" /> --}}
<PasswordValidator />
</div>
<div class="item" data-test-school>
<label for="primary-school-{{templateId}}">
Expand Down
5 changes: 4 additions & 1 deletion packages/frontend/app/components/new-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export default class NewUserComponent extends Component {
@Length(1, 100)
@NotBlank()
username = null;
@tracked @NotBlank() password = null;
@tracked
@Length(5)
@NotBlank()
password = null;
@tracked @Length(1, 20) phone = null;
@tracked schoolId = null;
@tracked primaryCohortId = null;
Expand Down
59 changes: 59 additions & 0 deletions packages/frontend/app/components/password-validator.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{{#if (has-block)}}
{{#let (unique-id) as |templateId|}}
<div class="password-validator" data-test-password-validator>
<label for="password-{{templateId}}">
{{t "general.password"}}:
</label>
{{yield
this.password
this.setPassword
this.addErrorDisplayFor
this.keyboard
this.hasErrorForPassword
this.passwordStrengthScore
}}
</div>
{{/let}}
{{else}}
{{#let (unique-id) as |templateId|}}
<div class="password-validator" data-test-password-validator>
<label for="password-{{templateId}}">
{{t "general.password"}}:
</label>
<input
id="password-{{templateId}}"
type="text"
value={{this.password}}
{{on "input" (pick "target.value" this.setPassword)}}
{{on "keyup" (queue (fn this.addErrorDisplayFor "password"))}}
{{on "keyup" this.keyboard}}
data-test-password-input
/>
{{#if this.hasErrorForPassword}}
<ValidationError @validatable={{this}} @property="password" />
{{else if (gt this.password.length 0)}}
<span
class="password-strength {{concat 'strength-' this.passwordStrengthScore}}"
data-test-password-strength-text
>
{{#if (eq this.passwordStrengthScore 0)}}
{{t "general.tryHarder"}}
{{else if (eq this.passwordStrengthScore 1)}}
{{t "general.bad"}}
{{else if (eq this.passwordStrengthScore 2)}}
{{t "general.weak"}}
{{else if (eq this.passwordStrengthScore 3)}}
{{t "general.good"}}
{{else if (eq this.passwordStrengthScore 4)}}
{{t "general.strong"}}
{{/if}}
</span>
<meter
max="4"
value={{this.passwordStrengthScore}}
data-test-password-strength-meter
></meter>
{{/if}}
</div>
{{/let}}
{{/if}}
59 changes: 59 additions & 0 deletions packages/frontend/app/components/password-validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Component from '@glimmer/component';
import { cached, tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { isEmpty } from '@ember/utils';
import { dropTask, timeout } from 'ember-concurrency';
import { validatable, Length, NotBlank } from 'ilios-common/decorators/validation';
import { TrackedAsyncData } from 'ember-async-data';

@validatable
export default class PasswordValidatorComponent extends Component {
@service intl;

@tracked @Length(5) @NotBlank() password = null;
@tracked isSaving = false;
@tracked passwordStrengthScore = 0;

@cached
get hasErrorForPasswordData() {
return new TrackedAsyncData(this.hasErrorFor('password'));
}

get hasErrorForPassword() {
// console.log('hasErrorForPassword', this.hasErrorForPasswordData.value);
return this.hasErrorForPasswordData.isResolved ? this.hasErrorForPasswordData.value : false;
}

@action
async keyboard(event) {
const keyCode = event.keyCode;

if (13 === keyCode) {
await this.save.perform();
}
}

@action
async setPassword(password) {
this.password = password;
await this.calculatePasswordStrengthScore();
}

async calculatePasswordStrengthScore() {
const { default: zxcvbn } = await import('zxcvbn');
const password = isEmpty(this.password) ? '' : this.password;
const obj = zxcvbn(password);
this.passwordStrengthScore = obj.score;
}

save = dropTask(async () => {
this.addErrorDisplaysFor(['password']);
const isValid = await this.isValid();
if (!isValid) {
return false;
}
await timeout(250); // artificial "validation processing"
this.clearErrorDisplay();
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'frontend/tests/helpers';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { component } from 'frontend/tests/pages/components/password-validator';

module('Integration | Component | password-validator', function (hooks) {
setupRenderingTest(hooks);

test('it renders', async function (assert) {
await render(hbs`<PasswordValidator />`);

assert.ok(component, 'component exists');
assert.strictEqual(component.label, 'Password:', 'has Password label');
});

test('it fails blank password', async function (assert) {
await render(hbs`<PasswordValidator />`);

assert.false(component.hasError, 'no error with no input');
await component.submit();
assert.true(component.hasError, 'shows blank password error');
});

test('it fails short password', async function (assert) {
await render(hbs`<PasswordValidator />`);

await component.fillIn('abc');
await component.submit();
assert.true(component.hasError);
});

test('it passes valid password', async function (assert) {
await render(hbs`<PasswordValidator />`);

await component.fillIn('abcde');
assert.false(component.hasError);
});

test('password strength 0 display', async function (assert) {
await render(hbs`<PasswordValidator />`);

await component.fillIn('12345');
assert.strictEqual(component.meter.value, 0);
assert.strictEqual(component.strength.text, 'Try Harder');
assert.ok(component.strength.hasZeroClass);
});

test('password strength 1 display', async function (assert) {
await render(hbs`<PasswordValidator />`);

await component.fillIn('12345ab');
assert.strictEqual(component.meter.value, 1);
assert.strictEqual(component.strength.text, 'Bad');
assert.ok(component.strength.hasOneClass);
});

test('password strength 2 display', async function (assert) {
await render(hbs`<PasswordValidator />`);

await component.fillIn('12345ab13&');
assert.strictEqual(component.meter.value, 2);
assert.strictEqual(component.strength.text, 'Weak');
assert.ok(component.strength.hasTwoClass);
});

test('password strength 3 display', async function (assert) {
await render(hbs`<PasswordValidator />`);

await component.fillIn('12345ab13&!!');
assert.strictEqual(component.meter.value, 3);
assert.strictEqual(component.strength.text, 'Good');
assert.ok(component.strength.hasThreeClass);
});

test('password strength 4 display', async function (assert) {
await render(hbs`<PasswordValidator />`);

await component.fillIn('12345ab13&HHtB');
assert.strictEqual(component.meter.value, 4);
assert.strictEqual(component.strength.text, 'Strong');
assert.ok(component.strength.hasFourClass);
});
});
34 changes: 34 additions & 0 deletions packages/frontend/tests/pages/components/password-validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
clickable,
create,
fillable,
hasClass,
isVisible,
text,
triggerable,
value,
} from 'ember-cli-page-object';

const definition = {
scope: '[data-test-password-validator]',
label: text('label'),
fillIn: fillable('input'),
value: value('input'),
validate: clickable('button'),
hasError: isVisible('.validation-error-message'),
submit: triggerable('keyup', 'input', { eventProperties: { key: 'Enter' } }),
strength: {
scope: '[data-test-password-strength-text]',
hasZeroClass: hasClass('strength-0'),
hasOneClass: hasClass('strength-1'),
hasTwoClass: hasClass('strength-2'),
hasThreeClass: hasClass('strength-3'),
hasFourClass: hasClass('strength-4'),
},
meter: {
scope: '[data-test-password-strength-meter]',
},
};

export default definition;
export const component = create(definition);
Loading