import React from 'react'; import { SessionModal } from './SessionModal'; import { SessionButton, SessionButtonColor } from './SessionButton'; import { PasswordUtil } from '../../util/'; import { ToastUtils } from '../../session/utils'; import { toast } from 'react-toastify'; import { SessionToast, SessionToastType } from './SessionToast'; import { SessionIconType } from './icon'; import { DefaultTheme, withTheme } from 'styled-components'; export enum PasswordAction { Set = 'set', Change = 'change', Remove = 'remove', } interface Props { action: PasswordAction; onOk: any; onClose: any; theme: DefaultTheme; } interface State { error: string | null; currentPasswordEntered: string | null; currentPasswordConfirmEntered: string | null; } class SessionPasswordModalInner extends React.Component { private passportInput: HTMLInputElement | null = null; constructor(props: any) { super(props); this.state = { error: null, currentPasswordEntered: null, currentPasswordConfirmEntered: null, }; this.showError = this.showError.bind(this); this.setPassword = this.setPassword.bind(this); this.closeDialog = this.closeDialog.bind(this); this.onPasswordInput = this.onPasswordInput.bind(this); this.onPasswordConfirmInput = this.onPasswordConfirmInput.bind(this); this.onPaste = this.onPaste.bind(this); } public componentDidMount() { setTimeout(() => { // tslint:disable-next-line: no-unused-expression this.passportInput && this.passportInput.focus(); }, 1); } public render() { const { action, onOk } = this.props; const placeholders = action === PasswordAction.Change ? [window.i18n('typeInOldPassword'), window.i18n('enterPassword')] : [window.i18n('enterPassword'), window.i18n('confirmPassword')]; const confirmButtonColor = action === PasswordAction.Remove ? SessionButtonColor.Danger : SessionButtonColor.Primary; return ( null} onClose={this.closeDialog} theme={this.props.theme} >
{ this.passportInput = input; }} placeholder={placeholders[0]} onKeyUp={this.onPasswordInput} maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} onPaste={this.onPaste} /> {action !== PasswordAction.Remove && ( )}
{this.showError()}
this.setPassword(onOk)} />
); } public async validatePasswordHash(password: string | null) { // Check if the password matches the hash we have stored const hash = await window.Signal.Data.getPasswordHash(); if (hash && !PasswordUtil.matchesHash(password, hash)) { return false; } return true; } private showError() { const message = this.state.error; return ( <> {message && ( <>
{message}
)} ); } // tslint:disable-next-line: cyclomatic-complexity private async setPassword(onSuccess?: any) { const { action } = this.props; const { currentPasswordEntered, currentPasswordConfirmEntered, } = this.state; const { Set, Remove, Change } = PasswordAction; // Trim leading / trailing whitespace for UX const enteredPassword = (currentPasswordEntered || '').trim(); const enteredPasswordConfirm = (currentPasswordConfirmEntered || '').trim(); // if user did not fill the first password field, we can't do anything const errorFirstInput = PasswordUtil.validatePassword( enteredPassword, window.i18n ); if (errorFirstInput !== null) { this.setState({ error: errorFirstInput, }); return; } // if action is Set or Change, we need a valid ConfirmPassword if (action === Set || action === Change) { const errorSecondInput = PasswordUtil.validatePassword( enteredPasswordConfirm, window.i18n ); if (errorSecondInput !== null) { this.setState({ error: errorSecondInput, }); return; } } // Passwords match or remove password successful const newPassword = action === Remove ? null : enteredPasswordConfirm; const oldPassword = action === Set ? null : enteredPassword; // Check if password match, when setting, changing or removing let valid; if (action === Set) { valid = enteredPassword === enteredPasswordConfirm; } else { valid = Boolean(await this.validatePasswordHash(oldPassword)); } if (!valid) { let str; switch (action) { case Set: str = window.i18n('setPasswordInvalid'); break; case Change: str = window.i18n('changePasswordInvalid'); break; case Remove: str = window.i18n('removePasswordInvalid'); break; default: throw new Error(`Invalid action ${action}`); } this.setState({ error: str, }); return; } await window.setPassword(newPassword, oldPassword); let title; let description; switch (action) { case Set: title = window.i18n('setPasswordTitle'); description = window.i18n('setPasswordToastDescription'); break; case Change: title = window.i18n('changePasswordTitle'); description = window.i18n('changePasswordToastDescription'); break; case Remove: title = window.i18n('removePasswordTitle'); description = window.i18n('removePasswordToastDescription'); break; default: throw new Error(`Invalid action ${action}`); } if (action !== Remove) { ToastUtils.pushToastSuccess( 'setPasswordSuccessToast', title, description, SessionIconType.Lock ); } else { ToastUtils.pushToastWarning( 'setPasswordSuccessToast', title, description ); } onSuccess(this.props.action); this.closeDialog(); } private closeDialog() { if (this.props.onClose) { this.props.onClose(); } } private onPaste(event: any) { const clipboard = event.clipboardData.getData('text'); if (clipboard.length > window.CONSTANTS.MAX_PASSWORD_LENGTH) { const title = String( window.i18n( 'pasteLongPasswordToastTitle', window.CONSTANTS.MAX_PASSWORD_LENGTH ) ); ToastUtils.pushToastWarning('passwordModal', title); } // Prevent pating into input return false; } private async onPasswordInput(event: any) { if (event.key === 'Enter') { return this.setPassword(this.props.onOk); } const currentPasswordEntered = event.target.value; this.setState({ currentPasswordEntered }); } private async onPasswordConfirmInput(event: any) { if (event.key === 'Enter') { return this.setPassword(this.props.onOk); } const currentPasswordConfirmEntered = event.target.value; this.setState({ currentPasswordConfirmEntered }); } } export const SessionPasswordModal = withTheme(SessionPasswordModalInner);