diff --git a/js/views/password_dialog_view.js b/js/views/password_dialog_view.js index d3221b566..57d2afa00 100644 --- a/js/views/password_dialog_view.js +++ b/js/views/password_dialog_view.js @@ -1,4 +1,4 @@ -/* global Whisper */ +/* global Whisper, i18n, _, Signal, passwordUtil */ // eslint-disable-next-line func-names (function() { @@ -6,28 +6,223 @@ window.Whisper = window.Whisper || {}; - Whisper.PasswordDialogView = Whisper.View.extend({ + const PasswordDialogView = Whisper.View.extend({ className: 'loki-dialog password-dialog modal', - initialize() { - this.close = this.close.bind(this); + 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(); - render() { - this.dialogView = new Whisper.ReactWrapperView({ - className: 'password-dialog-wrapper', - Component: window.Signal.Components.SessionPasswordChangeModal, - props: { - onClose: this.close, - }, - }); + const pairValidation = this.validatePasswordPair( + password, + passwordConfirmation + ); + const hashValidation = await this.validatePasswordHash(password); - this.$el.append(this.dialogView.el); - return this; + 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); - close() { + 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, + }); + }; })();