diff --git a/app/assets/javascripts/discourse/app/components/toggle-password-mask.hbs b/app/assets/javascripts/discourse/app/components/toggle-password-mask.hbs new file mode 100644 index 00000000000..6e98b87f102 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/toggle-password-mask.hbs @@ -0,0 +1,6 @@ + diff --git a/app/assets/javascripts/discourse/app/controllers/create-account.js b/app/assets/javascripts/discourse/app/controllers/create-account.js index 997a72afd53..b364d8d2ebd 100644 --- a/app/assets/javascripts/discourse/app/controllers/create-account.js +++ b/app/assets/javascripts/discourse/app/controllers/create-account.js @@ -42,6 +42,7 @@ export default Controller.extend( prefilledUsername: null, userFields: null, isDeveloper: false, + maskPassword: true, hasAuthOptions: notEmpty("authOptions"), canCreateLocal: setting("enable_local_logins"), @@ -68,6 +69,7 @@ export default Controller.extend( rejectedPasswords: [], prefilledUsername: null, isDeveloper: false, + maskPassword: true, }); this._createUserFields(); }, @@ -435,6 +437,11 @@ export default Controller.extend( }); }, + @action + togglePasswordMask() { + this.toggleProperty("maskPassword"); + }, + actions: { externalLogin(provider) { this.login.send("externalLogin", provider, { signup: true }); diff --git a/app/assets/javascripts/discourse/app/controllers/invites-show.js b/app/assets/javascripts/discourse/app/controllers/invites-show.js index ea121fac4bb..2d99c580f8b 100644 --- a/app/assets/javascripts/discourse/app/controllers/invites-show.js +++ b/app/assets/javascripts/discourse/app/controllers/invites-show.js @@ -1,7 +1,7 @@ import { alias, bool, not, readOnly } from "@ember/object/computed"; import Controller, { inject as controller } from "@ember/controller"; import DiscourseURL from "discourse/lib/url"; -import EmberObject from "@ember/object"; +import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import NameValidation from "discourse/mixins/name-validation"; import PasswordValidation from "discourse/mixins/password-validation"; @@ -47,6 +47,7 @@ export default Controller.extend( inviteImageUrl: getUrl("/images/envelope.svg"), isInviteLink: readOnly("model.is_invite_link"), rejectedEmails: null, + maskPassword: true, init() { this._super(...arguments); @@ -288,6 +289,11 @@ export default Controller.extend( }); }, + @action + togglePasswordMask() { + this.toggleProperty("maskPassword"); + }, + actions: { submit() { const userFields = this.userFields; diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index de5238a41dd..9d35dc7c977 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -41,6 +41,7 @@ export default Controller.extend(ModalFunctionality, { showLoginButtons: true, showSecondFactor: false, awaitingApproval: false, + maskPassword: true, canLoginLocal: setting("enable_local_logins"), canLoginLocalWithEmail: setting("enable_local_logins_via_email"), @@ -58,6 +59,7 @@ export default Controller.extend(ModalFunctionality, { showSecurityKey: false, showLoginButtons: true, awaitingApproval: false, + maskPassword: true, }); }, @@ -188,6 +190,11 @@ export default Controller.extend(ModalFunctionality, { this.send("showForgotPassword"); }, + @action + togglePasswordMask() { + this.toggleProperty("maskPassword"); + }, + actions: { forgotPassword() { this.handleForgotPassword(); diff --git a/app/assets/javascripts/discourse/app/controllers/password-reset.js b/app/assets/javascripts/discourse/app/controllers/password-reset.js index 472a69005dd..6e232fd5b23 100644 --- a/app/assets/javascripts/discourse/app/controllers/password-reset.js +++ b/app/assets/javascripts/discourse/app/controllers/password-reset.js @@ -33,6 +33,7 @@ export default Controller.extend(PasswordValidation, { successMessage: null, requiresApproval: false, redirected: false, + maskPassword: true, @discourseComputed() continueButtonText() { @@ -58,6 +59,11 @@ export default Controller.extend(PasswordValidation, { DiscourseURL.redirectTo(this.redirectTo || "/"); }, + @action + togglePasswordMask() { + this.toggleProperty("maskPassword"); + }, + actions: { submit() { ajax({ diff --git a/app/assets/javascripts/discourse/app/templates/invites/show.hbs b/app/assets/javascripts/discourse/app/templates/invites/show.hbs index 737efb67349..dfe4adf1c20 100644 --- a/app/assets/javascripts/discourse/app/templates/invites/show.hbs +++ b/app/assets/javascripts/discourse/app/templates/invites/show.hbs @@ -94,17 +94,20 @@ {{#unless this.externalAuthsOnly}}
- + - -
- {{this.passwordInstructions}} -
- {{d-icon "exclamation-triangle"}} {{i18n "login.caps_lock_warning"}} +
{{/unless}} diff --git a/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs b/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs index a3a0b8f52bb..5882b03799e 100644 --- a/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs +++ b/app/assets/javascripts/discourse/app/templates/modal/create-account.hbs @@ -68,18 +68,25 @@ diff --git a/app/assets/javascripts/discourse/app/templates/password-reset.hbs b/app/assets/javascripts/discourse/app/templates/password-reset.hbs index cdb63e219fd..98184716f48 100644 --- a/app/assets/javascripts/discourse/app/templates/password-reset.hbs +++ b/app/assets/javascripts/discourse/app/templates/password-reset.hbs @@ -36,8 +36,12 @@

{{i18n "user.change_password.choose"}}

- -   + + +
diff --git a/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js b/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js index e3f03607608..48272b7fc92 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/invite-accept-test.js @@ -3,7 +3,7 @@ import { exists, query, } 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 I18n from "I18n"; import { test } from "qunit"; @@ -111,6 +111,16 @@ acceptance("Invite accept", function (needs) { "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"); assert.ok( exists(".invites-show .btn-primary:disabled"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/password-reset-test.js b/app/assets/javascripts/discourse/tests/acceptance/password-reset-test.js index ce8f1069e8e..94bc4bb27d6 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/password-reset-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/password-reset-test.js @@ -80,7 +80,7 @@ acceptance("Password Reset", function (needs) { ); 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( query(".password-reset .tip.bad").innerHTML.includes( @@ -89,9 +89,19 @@ acceptance("Password Reset", function (needs) { "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"); 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"); }); @@ -125,7 +135,7 @@ acceptance("Password Reset", function (needs) { await fillIn(".password-reset input", "perf3ctly5ecur3"); sinon.stub(DiscourseURL, "redirectTo"); - await click(".password-reset form button"); + await click(".password-reset form button[type='submit']"); assert.ok( DiscourseURL.redirectTo.calledWith("/"), "it redirects after submitting form" diff --git a/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js b/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js index 3721273dd2f..2f5592f0b69 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sign-in-test.js @@ -23,6 +23,17 @@ acceptance("Signing In", function () { "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 await fillIn("#login-account-password", "correct"); await click(".modal-footer .btn-primary"); diff --git a/app/assets/stylesheets/common/base/login.scss b/app/assets/stylesheets/common/base/login.scss index 1cf5d7336c8..a54dc91b712 100644 --- a/app/assets/stylesheets/common/base/login.scss +++ b/app/assets/stylesheets/common/base/login.scss @@ -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); + display: flex; + justify-content: space-between; } .tip:not(:empty) + label.more-info { @@ -334,6 +336,20 @@ body.invite-page { 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 @@ -399,7 +415,9 @@ body.invite-page { } .tip { 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; +} diff --git a/app/assets/stylesheets/common/components/buttons.scss b/app/assets/stylesheets/common/components/buttons.scss index b3b68645983..4767f7308f7 100644 --- a/app/assets/stylesheets/common/components/buttons.scss +++ b/app/assets/stylesheets/common/components/buttons.scss @@ -339,6 +339,21 @@ border: 0; padding: 0; 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 { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ef289fc5591..fc2e71403f1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2124,6 +2124,10 @@ en: title: "Log in" username: "User" 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_description: "Please enter the authentication code from your app:" second_factor_backup: "Log in using a backup code"