From 0b87f136996295821d67da696e61c7df301be083 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 7 Dec 2018 12:16:04 +1100 Subject: [PATCH] Added password dialog view. --- _locales/en/messages.json | 49 +++++- app/password_util.js | 6 +- background.html | 30 ++++ js/background.js | 6 + js/views/app_view.js | 4 + js/views/inbox_view.js | 33 +++- js/views/nickname_dialog_view.js | 2 +- js/views/password_dialog_view.js | 214 +++++++++++++++++++++++ js/views/standalone_registration_view.js | 25 ++- password_preload.js | 2 + stylesheets/_conversation.scss | 91 +++++----- stylesheets/_global.scss | 8 + stylesheets/_theme_dark.scss | 38 ++-- test/index.html | 1 + 14 files changed, 430 insertions(+), 79 deletions(-) create mode 100644 js/views/password_dialog_view.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e7420151b..e168d78c8 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1698,11 +1698,56 @@ "description": "Button action that the user can click to set a password" }, "changePassword": { - "message": "Set Password", + "message": "Change Password", "description": "Button action that the user can click to change a password" }, "removePassword": { - "message": "Set Password", + "message": "Remove Password", "description": "Button action that the user can click to remove a password" + }, + "typeInOldPassword": { + "message": "Please type in your old password" + }, + "invalidPassword": { + "message": "Invalid password" + }, + "passwordsDoNotMatch": { + "message": "Passwords do not match" + }, + "setPasswordFail": { + "message": "Failed to set password" + }, + "removePasswordFail": { + "message": "Failed to remove password" + }, + "changePasswordFail": { + "message": "Failed to change password" + }, + "setPasswordSuccess": { + "message": "Password set" + }, + "removePasswordSuccess": { + "message": "Password removed" + }, + "changePasswordSuccess": { + "message": "Password changed" + }, + "passwordLengthError": { + "message": "Password must be atleast 6 characters long", + "description": "Error string shown to the user when password doesn't meet length criteria" + }, + "passwordTypeError": { + "message": "Password must be a string", + "description": "Error string shown to the user when password is not a string" + }, + "change": { + "message": "Change" + }, + "set": { + "message": "Set" + }, + "remove": { + "message": "Remove" } + } diff --git a/app/password_util.js b/app/password_util.js index b6c5e2b39..736a1c530 100644 --- a/app/password_util.js +++ b/app/password_util.js @@ -3,13 +3,13 @@ const { sha512 } = require('js-sha512'); const generateHash = (phrase) => phrase && sha512(phrase.trim()); const matchesHash = (phrase, hash) => phrase && sha512(phrase.trim()) === hash.trim(); -const validatePassword = (phrase) => { +const validatePassword = (phrase, i18n) => { if (typeof phrase !== 'string') { - return 'Password must be a string' + return i18n ? i18n('passwordTypeError') : 'Password must be a string' } if (phrase && phrase.trim().length < 6) { - return 'Password must be atleast 6 characters long'; + return i18n ? i18n('passwordLengthError') : 'Password must be atleast 6 characters long'; } // An empty password is still valid :P diff --git a/background.html b/background.html index 83b1806b8..ff0d7a76b 100644 --- a/background.html +++ b/background.html @@ -165,6 +165,35 @@ 0:00 + + + diff --git a/js/background.js b/js/background.js index faef150b6..831ad3044 100644 --- a/js/background.js +++ b/js/background.js @@ -602,6 +602,12 @@ } }); + Whisper.events.on('showPasswordDialog', options => { + if (appView) { + appView.showPasswordDialog(options); + } + }); + Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => { try { const conversation = ConversationController.get(pubKey); diff --git a/js/views/app_view.js b/js/views/app_view.js index f21542c24..04c4b1514 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -189,5 +189,9 @@ }); this.el.append(dialog.el); }, + showPasswordDialog({ type, resolve, reject }) { + const dialog = Whisper.getPasswordDialogView(type, resolve, reject); + this.el.append(dialog.el); + }, }); })(); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index dc52dbbbe..ea5339192 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -334,29 +334,37 @@ const ourNumber = textsecure.storage.user.getNumber(); clipboard.writeText(ourNumber); - const toast = new Whisper.MessageToastView({ - message: i18n('copiedPublicKey'), - }); - toast.$el.appendTo(this.$('.gutter')); - toast.render(); + this.showToastMessageInGutter(i18n('copiedPublicKey')); }), this._mainHeaderItem('editDisplayName', () => { window.Whisper.events.trigger('onEditProfile'); }), - ...this.passwordHeaderItems || [], ]; }, async onPasswordUpdated() { const hasPassword = await Signal.Data.getPasswordHash(); const items = this.getMainHeaderItems(); + + const showPasswordDialog = (type, resolve) => Whisper.events.trigger('showPasswordDialog', { + type, + resolve, + }); + + const passwordItem = (textKey, type) => this._mainHeaderItem( + textKey, + () => showPasswordDialog(type, () => { + this.showToastMessageInGutter(i18n(`${textKey}Success`)); + }) + ); + if (hasPassword) { items.push( - this._mainHeaderItem('changePassword'), - this._mainHeaderItem('removePassword') + passwordItem('changePassword', 'change'), + passwordItem('removePassword', 'remove') ); } else { items.push( - this._mainHeaderItem('setPassword') + passwordItem('setPassword', 'set') ); } @@ -369,6 +377,13 @@ onClick, }; }, + showToastMessageInGutter(message) { + const toast = new Whisper.MessageToastView({ + message, + }); + toast.$el.appendTo(this.$('.gutter')); + toast.render(); + }, }); Whisper.ExpiredAlertBanner = Whisper.View.extend({ diff --git a/js/views/nickname_dialog_view.js b/js/views/nickname_dialog_view.js index 7af20cb46..aa0af8143 100644 --- a/js/views/nickname_dialog_view.js +++ b/js/views/nickname_dialog_view.js @@ -7,7 +7,7 @@ window.Whisper = window.Whisper || {}; Whisper.NicknameDialogView = Whisper.View.extend({ - className: 'nickname-dialog modal', + className: 'loki-dialog nickname-dialog modal', templateName: 'nickname-dialog', initialize(options) { this.message = options.message; diff --git a/js/views/password_dialog_view.js b/js/views/password_dialog_view.js new file mode 100644 index 000000000..d919da1d2 --- /dev/null +++ b/js/views/password_dialog_view.js @@ -0,0 +1,214 @@ +/* global Whisper, i18n, _, Signal, passwordUtil */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + const PasswordDialogView = Whisper.View.extend({ + className: 'loki-dialog password-dialog modal', + templateName: 'password-dialog', + initialize(options) { + this.type = options.type; + this.resolve = options.resolve; + this.okText = options.okText || i18n('ok'); + + this.reject = options.reject; + this.cancelText = options.cancelText || i18n('cancel'); + + this.title = options.title; + + this.render(); + this.updateUI(); + }, + events: { + keyup: 'onKeyup', + 'click .ok': 'ok', + 'click .cancel': 'cancel', + }, + render_attributes() { + return { + showCancel: !this.hideCancel, + cancel: this.cancelText, + ok: this.okText, + title: this.title, + }; + }, + async updateUI() { + if (this.disableOkButton()) { + this.$('.ok').prop('disabled', true); + } else { + this.$('.ok').prop('disabled', false); + } + }, + disableOkButton() { + const password = this.$('#password').val(); + return _.isEmpty(password); + }, + async validate() { + const password = this.$('#password').val(); + const passwordConfirmation = this.$('#password-confirmation').val(); + + const pairValidation = this.validatePasswordPair(password, passwordConfirmation); + const hashValidation = await this.validatePasswordHash(password); + + return (pairValidation || hashValidation); + }, + async validatePasswordHash(password) { + // Check if the password matches the hash we have stored + const hash = await Signal.Data.getPasswordHash(); + if (hash && !passwordUtil.matchesHash(password, hash)) { + return i18n('invalidPassword'); + } + return null; + }, + validatePasswordPair(password, passwordConfirmation) { + if (!_.isEmpty(password)) { + + // Check if the password is first valid + const passwordValidation = passwordUtil.validatePassword(password, i18n); + if (passwordValidation) { + return passwordValidation; + } + + // Check if the confirmation password is the same + if (!passwordConfirmation || password.trim() !== passwordConfirmation.trim()) { + return i18n('passwordsDoNotMatch'); + } + } + return null; + }, + okPressed() { + const password = this.$('#password').val(); + if (this.type === 'set') { + window.setPassword(password.trim()); + } else if (this.type === 'remove') { + window.setPassword(null, password.trim()); + } + }, + okErrored() { + if (this.type === 'set') { + this.showError(i18n('setPasswordFail')); + } else if (this.type === 'remove') { + this.showError(i18n('removePasswordFail')); + } + }, + async ok() { + const error = await this.validate(); + if (error) { + this.showError(error); + return; + } + + // Clear any errors + this.showError(null); + + try { + this.okPressed(); + + this.remove(); + if (this.resolve) { + this.resolve(); + } + } catch (e) { + this.okErrored(); + } + }, + cancel() { + this.remove(); + if (this.reject) { + this.reject(); + } + }, + onKeyup(event) { + this.updateUI(); + switch (event.key) { + case 'Enter': + this.ok(); + break; + case 'Escape': + case 'Esc': + this.cancel(); + break; + default: + return; + } + event.preventDefault(); + }, + focusCancel() { + this.$('.cancel').focus(); + }, + showError(message) { + if (_.isEmpty(message)) { + this.$('.error').text(''); + this.$('.error').hide(); + } else { + this.$('.error').text(`Error: ${message}`); + this.$('.error').show(); + } + }, + }); + + const ChangePasswordDialogView = PasswordDialogView.extend({ + templateName: 'password-change-dialog', + disableOkButton() { + const oldPassword = this.$('#old-password').val(); + const newPassword = this.$('#new-password').val(); + return _.isEmpty(oldPassword) || _.isEmpty(newPassword); + }, + async validate() { + const oldPassword = this.$('#old-password').val(); + + // Validate the old password + if (!_.isEmpty(oldPassword) ) { + const oldPasswordValidation = passwordUtil.validatePassword(oldPassword, i18n); + if (oldPasswordValidation) { + return oldPasswordValidation; + } + } else { + return i18n('typeInOldPassword'); + } + + const password = this.$('#new-password').val(); + const passwordConfirmation = this.$('#new-password-confirmation').val(); + + const pairValidation = this.validatePasswordPair(password, passwordConfirmation); + const hashValidation = await this.validatePasswordHash(oldPassword); + + return pairValidation || hashValidation; + }, + okPressed() { + const oldPassword = this.$('#old-password').val(); + const newPassword = this.$('#new-password').val(); + window.setPassword(newPassword.trim(), oldPassword.trim()); + }, + okErrored() { + this.showError(i18n('changePasswordFail')); + }, + }); + + Whisper.getPasswordDialogView = (type, resolve, reject) => { + + // This is a differently styled dialog + if (type === 'change') { + return new ChangePasswordDialogView({ + title: i18n('changePassword'), + okTitle: i18n('change'), + resolve, + reject, + }); + } + + // Set and Remove is basically the same UI + const title = type === 'remove' ? i18n('removePassword') : i18n('setPassword'); + const okTitle = type === 'remove' ? i18n('remove') : i18n('set'); + return new PasswordDialogView({ + title, + okTitle, + type, + resolve, + reject, + }); + }; +})(); diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js index 6f32d874a..1c02a1407 100644 --- a/js/views/standalone_registration_view.js +++ b/js/views/standalone_registration_view.js @@ -193,12 +193,14 @@ const input = this.trim(this.$passwordInput.val()); const confirmationInput = this.trim(this.$passwordConfirmationInput.val()); - const error = passwordUtil.validatePassword(input); - if (error) + const error = passwordUtil.validatePassword(input, i18n); + if (error) { return error; + } - if (input !== confirmationInput) + if (input !== confirmationInput) { return 'Password don\'t match'; + } return null; }, @@ -207,13 +209,30 @@ if (passwordValidation) { this.$passwordInput.addClass('error-input'); this.$passwordConfirmationInput.addClass('error-input'); + + this.$passwordInput.removeClass('match-input'); + this.$passwordConfirmationInput.removeClass('match-input'); + this.$passwordInputError.text(passwordValidation); this.$passwordInputError.show(); + } else { this.$passwordInput.removeClass('error-input'); this.$passwordConfirmationInput.removeClass('error-input'); + this.$passwordInputError.text(''); this.$passwordInputError.hide(); + + // Show green box around inputs that match + const input = this.trim(this.$passwordInput.val()); + const confirmationInput = this.trim(this.$passwordConfirmationInput.val()); + if (input && input === confirmationInput) { + this.$passwordInput.addClass('match-input'); + this.$passwordConfirmationInput.addClass('match-input'); + } else { + this.$passwordInput.removeClass('match-input'); + this.$passwordConfirmationInput.removeClass('match-input'); + } } }, trim(value) { diff --git a/password_preload.js b/password_preload.js index f8dcb3eb3..1a857a58e 100644 --- a/password_preload.js +++ b/password_preload.js @@ -24,6 +24,8 @@ window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getAppInstance = () => config.appInstance; +window.passwordUtil = require('./app/password_util'); + window.onLogin = (passPhrase) => new Promise((resolve, reject) => { ipcRenderer.once('password-window-login-response', (event, error) => { if (error) { diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index bfda09310..4c23e6bf5 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -353,7 +353,7 @@ } } -.nickname-dialog { +.loki-dialog { display: flex; align-items: center; justify-content: center; @@ -366,55 +366,62 @@ border-radius: $border-radius; overflow: auto; box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.3); + } - .buttons { - - button { - float: right; - margin-left: 10px; - background-color: $color-loki-green; - border-radius: 100px; - padding: 5px 15px; - border: 1px solid $color-loki-green; - color: white; - outline: none; + button { + float: right; + margin-left: 10px; + background-color: $color-loki-green; + border-radius: 100px; + padding: 5px 15px; + border: 1px solid $color-loki-green; + color: white; + outline: none; + + &:hover, &:disabled { + background-color: $color-loki-green-dark; + border-color: $color-loki-green-dark; + } - &:hover { - background-color: $color-loki-green-dark; - border-color: $color-loki-green-dark; - } - } + &:disabled { + cursor: not-allowed; } + } + + input { + width: 100%; + padding: 8px; + margin-bottom: 15px; + border: 0; + outline: none; + border-radius: 4px; + background-color: $color-loki-light-gray; - input { - width: 100%; - padding: 8px; - margin-bottom: 15px; - border: 0; + &:focus { outline: none; - border-radius: 4px; - background-color: $color-loki-light-gray; } + } - h4 { - margin-top: 8px; - margin-bottom: 16px; - white-space: -moz-pre-wrap; /* Mozilla */ - white-space: -hp-pre-wrap; /* HP printers */ - white-space: -o-pre-wrap; /* Opera 7 */ - white-space: -pre-wrap; /* Opera 4-6 */ - white-space: pre-wrap; /* CSS 2.1 */ - white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ - word-wrap: break-word; /* IE */ - word-break: break-all; - } + h4 { + margin-top: 8px; + margin-bottom: 16px; + white-space: -moz-pre-wrap; /* Mozilla */ + white-space: -hp-pre-wrap; /* HP printers */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: pre-wrap; /* CSS 2.1 */ + white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ + word-wrap: break-word; /* IE */ + word-break: break-all; + } +} - .message { - font-style: italic; - color: $grey; - font-size: 12px; - margin-bottom: 16px; - } +.nickname-dialog { + .message { + font-style: italic; + color: $grey; + font-size: 12px; + margin-bottom: 16px; } } diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 3321781a2..d7f670714 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -993,6 +993,14 @@ textarea { outline: none; } } + + .match-input { + border: 3px solid $color-loki-green; + + &:focus { + outline: none; + } + } } @media (min-height: 750px) and (min-width: 700px) { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 7d9137355..3c1194d9b 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -93,34 +93,34 @@ body.dark-theme { } } - .nickname-dialog { + .loki-dialog { .content { background: $color-black; color: $color-dark-05; + } - .buttons { - button { - background-color: $color-dark-85; - border-radius: $border-radius; - border: 1px solid $color-dark-60; - color: $color-dark-05; - - &:hover { - background-color: $color-dark-70; - border-color: $color-dark-55; - } - } - } + button { + background-color: $color-dark-85; + border-radius: $border-radius; + border: 1px solid $color-dark-60; + color: $color-dark-05; - input { - color: $color-dark-05; + &:hover { background-color: $color-dark-70; border-color: $color-dark-55; } + } - .message { - color: $color-light-35; - } + input { + color: $color-dark-05; + background-color: $color-dark-70; + border-color: $color-dark-55; + } + } + + .nickname-dialog { + .message { + color: $color-light-35; } } diff --git a/test/index.html b/test/index.html index b03374a61..81a82bd38 100644 --- a/test/index.html +++ b/test/index.html @@ -403,6 +403,7 @@ +