FEATURE: add user toggle to mask/unmask passwords (#19306)
This commit is contained in:
parent
a176ce2fd0
commit
bd5f57e90c
|
@ -0,0 +1,6 @@
|
||||||
|
<DButton
|
||||||
|
@action={{@togglePasswordMask}}
|
||||||
|
@label={{if @maskPassword "login.show_password" "login.hide_password"}}
|
||||||
|
@class="btn-link toggle-password-mask"
|
||||||
|
@title={{if @maskPassword "login.show_password_title" "login.hide_password_title"}}
|
||||||
|
/>
|
|
@ -42,6 +42,7 @@ export default Controller.extend(
|
||||||
prefilledUsername: null,
|
prefilledUsername: null,
|
||||||
userFields: null,
|
userFields: null,
|
||||||
isDeveloper: false,
|
isDeveloper: false,
|
||||||
|
maskPassword: true,
|
||||||
|
|
||||||
hasAuthOptions: notEmpty("authOptions"),
|
hasAuthOptions: notEmpty("authOptions"),
|
||||||
canCreateLocal: setting("enable_local_logins"),
|
canCreateLocal: setting("enable_local_logins"),
|
||||||
|
@ -68,6 +69,7 @@ export default Controller.extend(
|
||||||
rejectedPasswords: [],
|
rejectedPasswords: [],
|
||||||
prefilledUsername: null,
|
prefilledUsername: null,
|
||||||
isDeveloper: false,
|
isDeveloper: false,
|
||||||
|
maskPassword: true,
|
||||||
});
|
});
|
||||||
this._createUserFields();
|
this._createUserFields();
|
||||||
},
|
},
|
||||||
|
@ -435,6 +437,11 @@ export default Controller.extend(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
togglePasswordMask() {
|
||||||
|
this.toggleProperty("maskPassword");
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
externalLogin(provider) {
|
externalLogin(provider) {
|
||||||
this.login.send("externalLogin", provider, { signup: true });
|
this.login.send("externalLogin", provider, { signup: true });
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { alias, bool, not, readOnly } from "@ember/object/computed";
|
import { alias, bool, not, readOnly } from "@ember/object/computed";
|
||||||
import Controller, { inject as controller } from "@ember/controller";
|
import Controller, { inject as controller } from "@ember/controller";
|
||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import EmberObject from "@ember/object";
|
import EmberObject, { action } from "@ember/object";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import NameValidation from "discourse/mixins/name-validation";
|
import NameValidation from "discourse/mixins/name-validation";
|
||||||
import PasswordValidation from "discourse/mixins/password-validation";
|
import PasswordValidation from "discourse/mixins/password-validation";
|
||||||
|
@ -47,6 +47,7 @@ export default Controller.extend(
|
||||||
inviteImageUrl: getUrl("/images/envelope.svg"),
|
inviteImageUrl: getUrl("/images/envelope.svg"),
|
||||||
isInviteLink: readOnly("model.is_invite_link"),
|
isInviteLink: readOnly("model.is_invite_link"),
|
||||||
rejectedEmails: null,
|
rejectedEmails: null,
|
||||||
|
maskPassword: true,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -288,6 +289,11 @@ export default Controller.extend(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
togglePasswordMask() {
|
||||||
|
this.toggleProperty("maskPassword");
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
submit() {
|
submit() {
|
||||||
const userFields = this.userFields;
|
const userFields = this.userFields;
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
showLoginButtons: true,
|
showLoginButtons: true,
|
||||||
showSecondFactor: false,
|
showSecondFactor: false,
|
||||||
awaitingApproval: false,
|
awaitingApproval: false,
|
||||||
|
maskPassword: true,
|
||||||
|
|
||||||
canLoginLocal: setting("enable_local_logins"),
|
canLoginLocal: setting("enable_local_logins"),
|
||||||
canLoginLocalWithEmail: setting("enable_local_logins_via_email"),
|
canLoginLocalWithEmail: setting("enable_local_logins_via_email"),
|
||||||
|
@ -58,6 +59,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
showSecurityKey: false,
|
showSecurityKey: false,
|
||||||
showLoginButtons: true,
|
showLoginButtons: true,
|
||||||
awaitingApproval: false,
|
awaitingApproval: false,
|
||||||
|
maskPassword: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -188,6 +190,11 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
this.send("showForgotPassword");
|
this.send("showForgotPassword");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
togglePasswordMask() {
|
||||||
|
this.toggleProperty("maskPassword");
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
forgotPassword() {
|
forgotPassword() {
|
||||||
this.handleForgotPassword();
|
this.handleForgotPassword();
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default Controller.extend(PasswordValidation, {
|
||||||
successMessage: null,
|
successMessage: null,
|
||||||
requiresApproval: false,
|
requiresApproval: false,
|
||||||
redirected: false,
|
redirected: false,
|
||||||
|
maskPassword: true,
|
||||||
|
|
||||||
@discourseComputed()
|
@discourseComputed()
|
||||||
continueButtonText() {
|
continueButtonText() {
|
||||||
|
@ -58,6 +59,11 @@ export default Controller.extend(PasswordValidation, {
|
||||||
DiscourseURL.redirectTo(this.redirectTo || "/");
|
DiscourseURL.redirectTo(this.redirectTo || "/");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
togglePasswordMask() {
|
||||||
|
this.toggleProperty("maskPassword");
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
submit() {
|
submit() {
|
||||||
ajax({
|
ajax({
|
||||||
|
|
|
@ -94,17 +94,20 @@
|
||||||
|
|
||||||
{{#unless this.externalAuthsOnly}}
|
{{#unless this.externalAuthsOnly}}
|
||||||
<div class="input password-input input-group">
|
<div class="input password-input input-group">
|
||||||
<PasswordField @value={{this.accountPassword}} @class={{value-entered this.accountPassword}} @type="password" @id="new-account-password" @capsLockOn={{this.capsLockOn}} />
|
<PasswordField @value={{this.accountPassword}} @class={{value-entered this.accountPassword}} @type={{if this.maskPassword "password" "text"}} @id="new-account-password" @capsLockOn={{this.capsLockOn}} />
|
||||||
<label class="alt-placeholder" for="new-account-password">
|
<label class="alt-placeholder" for="new-account-password">
|
||||||
{{i18n "invites.password_label"}}
|
{{i18n "invites.password_label"}}
|
||||||
<span class="required">*</span>
|
<span class="required">*</span>
|
||||||
</label>
|
</label>
|
||||||
<InputTip @validation={{this.passwordValidation}} />
|
<div class="create-account__password-info">
|
||||||
<div class="instructions">
|
<div class="create-account__password-tip-validation">
|
||||||
{{this.passwordInstructions}}
|
<InputTip @validation={{this.passwordValidation}} @id="password-validation" />
|
||||||
<div class="caps-lock-warning {{unless this.capsLockOn " hidden"}}">
|
<span class="more-info">{{this.passwordInstructions}}</span>
|
||||||
{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}
|
<div class="caps-lock-warning {{unless this.capsLockOn "hidden"}}">
|
||||||
|
{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<TogglePasswordMask @maskPassword={{this.maskPassword}} @togglePasswordMask={{this.togglePasswordMask}} @parentController={{"invites-show"}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
|
@ -68,18 +68,25 @@
|
||||||
|
|
||||||
<div class="input-group create-account__password">
|
<div class="input-group create-account__password">
|
||||||
{{#if this.passwordRequired}}
|
{{#if this.passwordRequired}}
|
||||||
<PasswordField @value={{this.accountPassword}} @class={{value-entered this.accountPassword}} @type="password" @id="new-account-password" @autocomplete="current-password" @capsLockOn={{this.capsLockOn}} @aria-describedby="password-validation" @aria-invalid={{this.passwordValidation.failed}} />
|
<PasswordField @value={{this.accountPassword}} @class={{value-entered this.accountPassword}} @type={{if this.maskPassword "password" "text"}} id="new-account-password" @autocomplete="current-password" @capsLockOn={{this.capsLockOn}} @aria-describedby="password-validation" @aria-invalid={{this.passwordValidation.failed}} />
|
||||||
<label class="alt-placeholder" for="new-account-password">
|
<label class="alt-placeholder" for="new-account-password">
|
||||||
{{i18n "user.password.title"}}
|
{{i18n "user.password.title"}}
|
||||||
{{~#if this.userFields~}}
|
{{~#if this.userFields~}}
|
||||||
<span class="required">*</span>
|
<span class="required">*</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
|
<div class="create-account__password-info">
|
||||||
<InputTip @validation={{this.passwordValidation}} @id="password-validation" />
|
<div class="create-account__password-tip-validation">
|
||||||
<span class="more-info">{{this.passwordInstructions}}</span>
|
<InputTip @validation={{this.passwordValidation}} @id="password-validation" />
|
||||||
<div class="caps-lock-warning {{unless this.capsLockOn "hidden"}}">
|
<span class="more-info">{{this.passwordInstructions}}</span>
|
||||||
{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}
|
<div class="caps-lock-warning {{unless this.capsLockOn "hidden"}}">
|
||||||
|
{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TogglePasswordMask
|
||||||
|
@maskPassword={{this.maskPassword}}
|
||||||
|
@togglePasswordMask={{this.togglePasswordMask}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,16 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<PasswordField @value={{this.loginPassword}} @type="password" class={{value-entered this.loginPassword}} id="login-account-password" autocomplete="current-password" maxlength="200" @capsLockOn={{this.capsLockOn}} disabled={{this.disableLoginFields}} tabindex="1" />
|
<PasswordField @value={{this.loginPassword}} @type={{if this.maskPassword "password" "text"}} class={{value-entered this.loginPassword}} id="login-account-password" autocomplete="current-password" maxlength="200" @capsLockOn={{this.capsLockOn}} disabled={{this.disableLoginFields}} tabindex="1" />
|
||||||
<label class="alt-placeholder" for="login-account-password">{{i18n "login.password"}}</label>
|
<label class="alt-placeholder" for="login-account-password">{{i18n "login.password"}}</label>
|
||||||
<a href id="forgot-password-link" tabindex="3" {{on "click" this.handleForgotPassword}}>{{i18n "forgot_password.action"}}</a>
|
<div class="login__password-links">
|
||||||
|
<a href id="forgot-password-link" tabindex="3" {{on "click" this.handleForgotPassword}}>{{i18n "forgot_password.action"}}</a>
|
||||||
|
<TogglePasswordMask
|
||||||
|
@maskPassword={{this.maskPassword}}
|
||||||
|
@togglePasswordMask={{this.togglePasswordMask}}
|
||||||
|
tabindex="3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="caps-lock-warning {{unless this.capsLockOn "hidden"}}">{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}</div>
|
<div class="caps-lock-warning {{unless this.capsLockOn "hidden"}}">{{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,8 +36,12 @@
|
||||||
<h2>{{i18n "user.change_password.choose"}}</h2>
|
<h2>{{i18n "user.change_password.choose"}}</h2>
|
||||||
|
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<PasswordField @value={{this.accountPassword}} @type="password" @id="new-account-password" @capsLockOn={{this.capsLockOn}} @autofocus="autofocus" />
|
<PasswordField @value={{this.accountPassword}} @type={{if this.maskPassword "password" "text"}} @id="new-account-password" @capsLockOn={{this.capsLockOn}} @autofocus="autofocus" />
|
||||||
<InputTip @validation={{this.passwordValidation}} />
|
<TogglePasswordMask
|
||||||
|
@maskPassword={{this.maskPassword}}
|
||||||
|
@togglePasswordMask={{this.togglePasswordMask}}
|
||||||
|
/>
|
||||||
|
<InputTip @validation={{this.passwordValidation}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="instructions">
|
<div class="instructions">
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
exists,
|
exists,
|
||||||
query,
|
query,
|
||||||
} from "discourse/tests/helpers/qunit-helpers";
|
} from "discourse/tests/helpers/qunit-helpers";
|
||||||
import { fillIn, visit } from "@ember/test-helpers";
|
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||||
import PreloadStore from "discourse/lib/preload-store";
|
import PreloadStore from "discourse/lib/preload-store";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { test } from "qunit";
|
import { test } from "qunit";
|
||||||
|
@ -111,6 +111,16 @@ acceptance("Invite accept", function (needs) {
|
||||||
"submit is disabled because name and email is not filled"
|
"submit is disabled because name and email is not filled"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists("#new-account-password[type='password']"),
|
||||||
|
"password is masked by default"
|
||||||
|
);
|
||||||
|
await click(".toggle-password-mask");
|
||||||
|
assert.ok(
|
||||||
|
exists("#new-account-password[type='text']"),
|
||||||
|
"password is unmasked when toggle is clicked"
|
||||||
|
);
|
||||||
|
|
||||||
await fillIn("#new-account-name", "John Doe");
|
await fillIn("#new-account-name", "John Doe");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
exists(".invites-show .btn-primary:disabled"),
|
exists(".invites-show .btn-primary:disabled"),
|
||||||
|
|
|
@ -80,7 +80,7 @@ acceptance("Password Reset", function (needs) {
|
||||||
);
|
);
|
||||||
|
|
||||||
await fillIn(".password-reset input", "jonesyAlienSlayer");
|
await fillIn(".password-reset input", "jonesyAlienSlayer");
|
||||||
await click(".password-reset form button");
|
await click(".password-reset form button[type='submit']");
|
||||||
assert.ok(exists(".password-reset .tip.bad"), "input is not valid");
|
assert.ok(exists(".password-reset .tip.bad"), "input is not valid");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
query(".password-reset .tip.bad").innerHTML.includes(
|
query(".password-reset .tip.bad").innerHTML.includes(
|
||||||
|
@ -89,9 +89,19 @@ acceptance("Password Reset", function (needs) {
|
||||||
"server validation error message shows"
|
"server validation error message shows"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
exists("#new-account-password[type='password']"),
|
||||||
|
"password is masked by default"
|
||||||
|
);
|
||||||
|
await click(".toggle-password-mask");
|
||||||
|
assert.ok(
|
||||||
|
exists("#new-account-password[type='text']"),
|
||||||
|
"password is unmasked after toggle is clicked"
|
||||||
|
);
|
||||||
|
|
||||||
await fillIn(".password-reset input", "perf3ctly5ecur3");
|
await fillIn(".password-reset input", "perf3ctly5ecur3");
|
||||||
sinon.stub(DiscourseURL, "redirectTo");
|
sinon.stub(DiscourseURL, "redirectTo");
|
||||||
await click(".password-reset form button");
|
await click(".password-reset form button[type='submit']");
|
||||||
assert.ok(DiscourseURL.redirectTo.calledWith("/"), "form is gone");
|
assert.ok(DiscourseURL.redirectTo.calledWith("/"), "form is gone");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -125,7 +135,7 @@ acceptance("Password Reset", function (needs) {
|
||||||
await fillIn(".password-reset input", "perf3ctly5ecur3");
|
await fillIn(".password-reset input", "perf3ctly5ecur3");
|
||||||
|
|
||||||
sinon.stub(DiscourseURL, "redirectTo");
|
sinon.stub(DiscourseURL, "redirectTo");
|
||||||
await click(".password-reset form button");
|
await click(".password-reset form button[type='submit']");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
DiscourseURL.redirectTo.calledWith("/"),
|
DiscourseURL.redirectTo.calledWith("/"),
|
||||||
"it redirects after submitting form"
|
"it redirects after submitting form"
|
||||||
|
|
|
@ -23,6 +23,17 @@ acceptance("Signing In", function () {
|
||||||
"enables the login button"
|
"enables the login button"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Test password unmasking
|
||||||
|
assert.ok(
|
||||||
|
exists("#login-account-password[type='password']"),
|
||||||
|
"password is masked by default"
|
||||||
|
);
|
||||||
|
await click(".toggle-password-mask");
|
||||||
|
assert.ok(
|
||||||
|
exists("#login-account-password[type='text']"),
|
||||||
|
"password is unmasked after toggle is clicked"
|
||||||
|
);
|
||||||
|
|
||||||
// Use the correct password
|
// Use the correct password
|
||||||
await fillIn("#login-account-password", "correct");
|
await fillIn("#login-account-password", "correct");
|
||||||
await click(".modal-footer .btn-primary");
|
await click(".modal-footer .btn-primary");
|
||||||
|
|
|
@ -217,9 +217,11 @@ body.invite-page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#forgot-password-link,
|
#email-login-link,
|
||||||
#email-login-link {
|
.login__password-links {
|
||||||
font-size: var(--font-down-1);
|
font-size: var(--font-down-1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip:not(:empty) + label.more-info {
|
.tip:not(:empty) + label.more-info {
|
||||||
|
@ -334,6 +336,20 @@ body.invite-page {
|
||||||
color: var(--primary-medium);
|
color: var(--primary-medium);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#new-account-password {
|
||||||
|
width: 15em;
|
||||||
|
}
|
||||||
|
.tip {
|
||||||
|
margin: 0 0 0.5em;
|
||||||
|
}
|
||||||
|
.toggle-password-mask {
|
||||||
|
margin-left: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-password-mask {
|
||||||
|
align-self: start;
|
||||||
|
line-height: 1.4; // aligns with input description text
|
||||||
}
|
}
|
||||||
|
|
||||||
// admin invite page
|
// admin invite page
|
||||||
|
@ -399,7 +415,9 @@ body.invite-page {
|
||||||
}
|
}
|
||||||
.tip {
|
.tip {
|
||||||
font-size: var(--font-down-1);
|
font-size: var(--font-down-1);
|
||||||
margin-bottom: 0.25em;
|
&:not(:empty) {
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,3 +508,8 @@ button#new-account-link {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-account__password-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
|
@ -339,6 +339,21 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: var(--tertiary);
|
color: var(--tertiary);
|
||||||
|
.discourse-no-touch & {
|
||||||
|
&:hover {
|
||||||
|
color: var(--tertiary);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
color: var(--tertiary);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--tertiary);
|
||||||
|
background: transparent;
|
||||||
|
@include default-focus;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-mini-toggle {
|
.btn-mini-toggle {
|
||||||
|
|
|
@ -2124,6 +2124,10 @@ en:
|
||||||
title: "Log in"
|
title: "Log in"
|
||||||
username: "User"
|
username: "User"
|
||||||
password: "Password"
|
password: "Password"
|
||||||
|
show_password: "Show"
|
||||||
|
hide_password: "Hide"
|
||||||
|
show_password_title: "Show password"
|
||||||
|
hide_password_title: "Hide password"
|
||||||
second_factor_title: "Two-Factor Authentication"
|
second_factor_title: "Two-Factor Authentication"
|
||||||
second_factor_description: "Please enter the authentication code from your app:"
|
second_factor_description: "Please enter the authentication code from your app:"
|
||||||
second_factor_backup: "Log in using a backup code"
|
second_factor_backup: "Log in using a backup code"
|
||||||
|
|
Loading…
Reference in New Issue