You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
306 lines
8.4 KiB
TypeScript
306 lines
8.4 KiB
TypeScript
import React from 'react';
|
|
|
|
import { SessionButton, SessionButtonColor } from '../session/SessionButton';
|
|
import { missingCaseError, PasswordUtil } from '../../util';
|
|
import { ToastUtils } from '../../session/utils';
|
|
import { getPasswordHash } from '../../data/data';
|
|
import { SessionWrapperModal } from '../session/SessionWrapperModal';
|
|
import { SpacerLG, SpacerSM } from '../basic/Text';
|
|
import autoBind from 'auto-bind';
|
|
import { sessionPassword } from '../../state/ducks/modalDialog';
|
|
export type PasswordAction = 'set' | 'change' | 'remove';
|
|
|
|
interface Props {
|
|
passwordAction: PasswordAction;
|
|
onOk: () => void;
|
|
}
|
|
|
|
interface State {
|
|
error: string | null;
|
|
currentPasswordEntered: string | null;
|
|
currentPasswordConfirmEntered: string | null;
|
|
currentPasswordRetypeEntered: string | null;
|
|
}
|
|
|
|
export class SessionPasswordDialog extends React.Component<Props, State> {
|
|
private passportInput: HTMLInputElement | null = null;
|
|
|
|
constructor(props: any) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
error: null,
|
|
currentPasswordEntered: null,
|
|
currentPasswordConfirmEntered: null,
|
|
currentPasswordRetypeEntered: null,
|
|
};
|
|
|
|
autoBind(this);
|
|
}
|
|
|
|
public componentDidMount() {
|
|
setTimeout(() => {
|
|
// tslint:disable-next-line: no-unused-expression
|
|
this.passportInput && this.passportInput.focus();
|
|
}, 1);
|
|
}
|
|
|
|
public render() {
|
|
const { passwordAction } = this.props;
|
|
const placeholders =
|
|
passwordAction === 'change'
|
|
? [
|
|
window.i18n('typeInOldPassword'),
|
|
window.i18n('enterPassword'),
|
|
window.i18n('confirmPassword'),
|
|
]
|
|
: [window.i18n('enterPassword'), window.i18n('confirmPassword')];
|
|
|
|
const confirmButtonColor =
|
|
passwordAction === 'remove' ? SessionButtonColor.Danger : SessionButtonColor.Green;
|
|
|
|
return (
|
|
<SessionWrapperModal
|
|
title={window.i18n(`${passwordAction}Password`)}
|
|
onClose={this.closeDialog}
|
|
>
|
|
<SpacerSM />
|
|
|
|
<div className="session-modal__input-group">
|
|
<input
|
|
type="password"
|
|
id="password-modal-input"
|
|
ref={input => {
|
|
this.passportInput = input;
|
|
}}
|
|
placeholder={placeholders[0]}
|
|
onKeyUp={this.onPasswordInput}
|
|
/>
|
|
{passwordAction !== 'remove' && (
|
|
<input
|
|
type="password"
|
|
id="password-modal-input-confirm"
|
|
placeholder={placeholders[1]}
|
|
onKeyUp={this.onPasswordConfirmInput}
|
|
/>
|
|
)}
|
|
{passwordAction === 'change' && (
|
|
<input
|
|
type="password"
|
|
id="password-modal-input-reconfirm"
|
|
placeholder={placeholders[2]}
|
|
onKeyUp={this.onPasswordRetypeInput}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<SpacerSM />
|
|
{this.showError()}
|
|
|
|
<div className="session-modal__button-group">
|
|
<SessionButton
|
|
text={window.i18n('ok')}
|
|
buttonColor={confirmButtonColor}
|
|
onClick={this.setPassword}
|
|
/>
|
|
|
|
<SessionButton text={window.i18n('cancel')} onClick={this.closeDialog} />
|
|
</div>
|
|
</SessionWrapperModal>
|
|
);
|
|
}
|
|
|
|
public async validatePasswordHash(password: string | null) {
|
|
// Check if the password matches the hash we have stored
|
|
const hash = await getPasswordHash();
|
|
if (hash && !PasswordUtil.matchesHash(password, hash)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private showError() {
|
|
const message = this.state.error;
|
|
|
|
return (
|
|
<>
|
|
{message && (
|
|
<>
|
|
<div className="session-label warning">{message}</div>
|
|
<SpacerLG />
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns false and set the state error field in the input is not a valid password
|
|
* or returns true
|
|
*/
|
|
private validatePassword(firstPassword: string) {
|
|
// if user did not fill the first password field, we can't do anything
|
|
const errorFirstInput = PasswordUtil.validatePassword(firstPassword);
|
|
if (errorFirstInput !== null) {
|
|
this.setState({
|
|
error: errorFirstInput,
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private async handleActionSet(enteredPassword: string, enteredPasswordConfirm: string) {
|
|
// be sure both password are valid
|
|
if (!this.validatePassword(enteredPassword)) {
|
|
return;
|
|
}
|
|
// no need to validate second password. we just need to check that enteredPassword is valid, and that both password matches
|
|
|
|
if (enteredPassword !== enteredPasswordConfirm) {
|
|
this.setState({
|
|
error: window.i18n('setPasswordInvalid'),
|
|
});
|
|
return;
|
|
}
|
|
await window.setPassword(enteredPassword, null);
|
|
ToastUtils.pushToastSuccess(
|
|
'setPasswordSuccessToast',
|
|
window.i18n('setPasswordTitle'),
|
|
window.i18n('setPasswordToastDescription'),
|
|
'lock'
|
|
);
|
|
|
|
this.props.onOk();
|
|
this.closeDialog();
|
|
}
|
|
|
|
private async handleActionChange(
|
|
oldPassword: string,
|
|
newPassword: string,
|
|
newConfirmedPassword: string
|
|
) {
|
|
// We don't validate oldPassword on change: this is validate on the validatePasswordHash below
|
|
// we only validate the newPassword here
|
|
if (!this.validatePassword(newPassword)) {
|
|
return;
|
|
}
|
|
|
|
// Check the retyped password matches the new password
|
|
if (newPassword !== newConfirmedPassword) {
|
|
this.setState({
|
|
error: window.i18n('passwordsDoNotMatch'),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const isValidWithStoredInDB = Boolean(await this.validatePasswordHash(oldPassword));
|
|
if (!isValidWithStoredInDB) {
|
|
this.setState({
|
|
error: window.i18n('changePasswordInvalid'),
|
|
});
|
|
return;
|
|
}
|
|
await window.setPassword(newPassword, oldPassword);
|
|
|
|
ToastUtils.pushToastSuccess(
|
|
'setPasswordSuccessToast',
|
|
window.i18n('changePasswordTitle'),
|
|
window.i18n('changePasswordToastDescription'),
|
|
'lock'
|
|
);
|
|
|
|
this.props.onOk();
|
|
this.closeDialog();
|
|
}
|
|
|
|
private async handleActionRemove(oldPassword: string) {
|
|
// We don't validate oldPassword on change: this is validate on the validatePasswordHash below
|
|
const isValidWithStoredInDB = Boolean(await this.validatePasswordHash(oldPassword));
|
|
if (!isValidWithStoredInDB) {
|
|
this.setState({
|
|
error: window.i18n('removePasswordInvalid'),
|
|
});
|
|
return;
|
|
}
|
|
await window.setPassword(null, oldPassword);
|
|
|
|
ToastUtils.pushToastWarning(
|
|
'setPasswordSuccessToast',
|
|
window.i18n('removePasswordTitle'),
|
|
window.i18n('removePasswordToastDescription')
|
|
);
|
|
|
|
this.props.onOk();
|
|
this.closeDialog();
|
|
}
|
|
|
|
// tslint:disable-next-line: cyclomatic-complexity
|
|
private async setPassword() {
|
|
const { passwordAction } = this.props;
|
|
const {
|
|
currentPasswordEntered,
|
|
currentPasswordConfirmEntered,
|
|
currentPasswordRetypeEntered,
|
|
} = this.state;
|
|
|
|
// Trim leading / trailing whitespace for UX
|
|
const firstPasswordEntered = (currentPasswordEntered || '').trim();
|
|
const secondPasswordEntered = (currentPasswordConfirmEntered || '').trim();
|
|
const thirdPasswordEntered = (currentPasswordRetypeEntered || '').trim();
|
|
|
|
switch (passwordAction) {
|
|
case 'set': {
|
|
await this.handleActionSet(firstPasswordEntered, secondPasswordEntered);
|
|
return;
|
|
}
|
|
case 'change': {
|
|
await this.handleActionChange(
|
|
firstPasswordEntered,
|
|
secondPasswordEntered,
|
|
thirdPasswordEntered
|
|
);
|
|
return;
|
|
}
|
|
case 'remove': {
|
|
await this.handleActionRemove(firstPasswordEntered);
|
|
return;
|
|
}
|
|
default:
|
|
throw missingCaseError(passwordAction);
|
|
}
|
|
}
|
|
|
|
private closeDialog() {
|
|
window.inboxStore?.dispatch(sessionPassword(null));
|
|
}
|
|
|
|
private async onPasswordInput(event: any) {
|
|
if (event.key === 'Enter') {
|
|
return this.setPassword();
|
|
}
|
|
const currentPasswordEntered = event.target.value;
|
|
|
|
this.setState({ currentPasswordEntered });
|
|
}
|
|
|
|
private async onPasswordConfirmInput(event: any) {
|
|
if (event.key === 'Enter') {
|
|
return this.setPassword();
|
|
}
|
|
const currentPasswordConfirmEntered = event.target.value;
|
|
|
|
this.setState({ currentPasswordConfirmEntered });
|
|
}
|
|
|
|
private async onPasswordRetypeInput(event: any) {
|
|
if (event.key === 'Enter') {
|
|
return this.setPassword();
|
|
}
|
|
const currentPasswordRetypeEntered = event.target.value;
|
|
|
|
this.setState({ currentPasswordRetypeEntered });
|
|
}
|
|
}
|