feat: extracted password to new modal

added shiny hook to password protect anywhere
pull/3083/head
William Grant 4 weeks ago
parent b6d5c24af4
commit d346f28942

@ -420,6 +420,7 @@
"readReceiptSettingDescription": "Send read receipts in one-to-one chats.",
"readReceiptSettingTitle": "Read Receipts",
"received": "Received",
"recoveryPasswordDescription": "Use your recovery password to load your account on new devices.<br/>Your account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.",
"recoveryPasswordEnter": "Enter your recovery password",
"recoveryPasswordErrorMessageGeneric": "Please check your recovery password and try again.",
"recoveryPasswordErrorMessageIncorrect": "Some of the words in your Recovery Password are incorrect. Please check and try again.",

@ -0,0 +1,106 @@
import { Dispatch, SetStateAction } from 'react';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { ToastUtils } from '../../session/utils';
import { matchesHash } from '../../util/passwordUtils';
import { updateEnterPasswordModal } from '../../state/ducks/modalDialog';
import { SpacerSM } from '../basic/Text';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
const StyledModalContainer = styled.div`
margin: var(--margins-md) var(--margins-sm);
`;
export type EnterPasswordModalProps = {
passwordHash: string;
passwordValid: boolean;
setPasswordValid: Dispatch<SetStateAction<boolean>>;
onClickOk: () => any;
onClickClose: () => any;
title?: string;
};
export const EnterPasswordModal = (props: EnterPasswordModalProps) => {
const { passwordHash, setPasswordValid, onClickOk, onClickClose, title } = props;
const dispatch = useDispatch();
const onClose = () => {
onClickClose();
dispatch(updateEnterPasswordModal(null));
};
const confirmPassword = () => {
const passwordValue = (document.getElementById('seed-input-password') as any)?.value;
const isPasswordValid = matchesHash(passwordValue as string, passwordHash);
if (!passwordValue) {
ToastUtils.pushToastError('enterPasswordErrorToast', window.i18n('noGivenPassword'));
return;
}
if (passwordHash && !isPasswordValid) {
ToastUtils.pushToastError('enterPasswordErrorToast', window.i18n('invalidPassword'));
return;
}
setPasswordValid(true);
window.removeEventListener('keyup', onEnter);
void onClickOk();
};
const onEnter = (event: any) => {
if (event.key === 'Enter') {
confirmPassword();
}
};
return (
<SessionWrapperModal
title={title || window.i18n('enterPassword')}
onClose={onClose}
showExitIcon={true}
>
<StyledModalContainer>
<SpacerSM />
<div className="session-modal__input-group">
<input
type="password"
id="seed-input-password"
data-testid="password-input"
placeholder={window.i18n('enterPassword')}
onKeyUp={onEnter}
/>
</div>
<SpacerSM />
<div
className="session-modal__button-group"
style={{ justifyContent: 'center', width: '100%' }}
>
<SessionButton
text={window.i18n('done')}
buttonType={SessionButtonType.Simple}
onClick={confirmPassword}
dataTestId="session-confirm-ok-button"
/>
<SessionButton
text={window.i18n('cancel')}
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={onClose}
dataTestId="session-confirm-cancel-button"
/>
</div>
</StyledModalContainer>
</SessionWrapperModal>
);
};

@ -7,6 +7,7 @@ import {
getDeleteAccountModalState,
getEditProfileDialog,
getEditProfilePictureModalState,
getEnterPasswordModalState,
getInviteContactModal,
getOnionPathDialog,
getReactClearAllDialog,
@ -22,6 +23,7 @@ import { BanOrUnBanUserDialog } from './BanOrUnbanUserDialog';
import { DeleteAccountModal } from './DeleteAccountModal';
import { EditProfileDialog } from './EditProfileDialog';
import { EditProfilePictureModal } from './EditProfilePictureModal';
import { EnterPasswordModal } from './EnterPasswordModal';
import { InviteContactsDialog } from './InviteContactsDialog';
import { AddModeratorsDialog } from './ModeratorsAddDialog';
import { RemoveModeratorsDialog } from './ModeratorsRemoveDialog';
@ -30,8 +32,8 @@ import { ReactClearAllModal } from './ReactClearAllModal';
import { ReactListModal } from './ReactListModal';
import { SessionConfirm } from './SessionConfirm';
import { SessionNicknameDialog } from './SessionNicknameDialog';
import { SessionPasswordDialog } from './SessionPasswordDialog';
import { SessionSeedModal } from './SessionSeedModal';
import { SessionSetPasswordDialog } from './SessionSetPasswordDialog';
import { UpdateGroupMembersDialog } from './UpdateGroupMembersDialog';
import { UpdateGroupNameDialog } from './UpdateGroupNameDialog';
import { UserDetailsDialog } from './UserDetailsDialog';
@ -48,6 +50,7 @@ export const ModalContainer = () => {
const editProfileModalState = useSelector(getEditProfileDialog);
const onionPathModalState = useSelector(getOnionPathDialog);
const recoveryPhraseModalState = useSelector(getRecoveryPhraseDialog);
const enterPasswordModalState = useSelector(getEnterPasswordModalState);
const sessionPasswordModalState = useSelector(getSessionPasswordDialog);
const deleteAccountModalState = useSelector(getDeleteAccountModalState);
const banOrUnbanUserModalState = useSelector(getBanOrUnbanUserModalState);
@ -70,7 +73,8 @@ export const ModalContainer = () => {
{editProfileModalState && <EditProfileDialog {...editProfileModalState} />}
{onionPathModalState && <OnionPathModal {...onionPathModalState} />}
{recoveryPhraseModalState && <SessionSeedModal {...recoveryPhraseModalState} />}
{sessionPasswordModalState && <SessionPasswordDialog {...sessionPasswordModalState} />}
{enterPasswordModalState && <EnterPasswordModal {...enterPasswordModalState} />}
{sessionPasswordModalState && <SessionSetPasswordDialog {...sessionPasswordModalState} />}
{deleteAccountModalState && <DeleteAccountModal {...deleteAccountModalState} />}
{confirmModalState && <SessionConfirm {...confirmModalState} />}
{reactListModalState && <ReactListModal {...reactListModalState} />}

@ -1,97 +1,20 @@
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import useMount from 'react-use/lib/useMount';
import styled from 'styled-components';
import { Data } from '../../data/data';
import { ToastUtils } from '../../session/utils';
import { matchesHash } from '../../util/passwordUtils';
import { mnDecode } from '../../session/crypto/mnemonic';
import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
import { SpacerSM } from '../basic/Text';
import { usePasswordModal } from '../../hooks/usePasswordModal';
import { getTheme } from '../../state/selectors/theme';
import { getThemeValue } from '../../themes/globals';
import { getCurrentRecoveryPhrase } from '../../util/storage';
import { SessionQRCode } from '../SessionQRCode';
import { SessionWrapperModal } from '../SessionWrapperModal';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
interface PasswordProps {
setPasswordValid: (val: boolean) => any;
passwordHash: string;
}
const Password = (props: PasswordProps) => {
const { setPasswordValid, passwordHash } = props;
const i18n = window.i18n;
const dispatch = useDispatch();
const onClose = () => dispatch(recoveryPhraseModal(null));
const confirmPassword = () => {
const passwordValue = (document.getElementById('seed-input-password') as any)?.value;
const isPasswordValid = matchesHash(passwordValue as string, passwordHash);
if (!passwordValue) {
ToastUtils.pushToastError('enterPasswordErrorToast', i18n('noGivenPassword'));
return false;
}
if (passwordHash && !isPasswordValid) {
ToastUtils.pushToastError('enterPasswordErrorToast', i18n('invalidPassword'));
return false;
}
setPasswordValid(true);
window.removeEventListener('keyup', onEnter);
return true;
};
const onEnter = (event: any) => {
if (event.key === 'Enter') {
confirmPassword();
}
};
return (
<>
<div className="session-modal__input-group">
<input
type="password"
id="seed-input-password"
data-testid="password-input"
placeholder={i18n('enterPassword')}
onKeyUp={onEnter}
/>
</div>
<SpacerSM />
<div
className="session-modal__button-group"
style={{ justifyContent: 'center', width: '100%' }}
>
<SessionButton
text={i18n('done')}
buttonType={SessionButtonType.Simple}
onClick={confirmPassword}
dataTestId="session-confirm-ok-button"
/>
<SessionButton
text={i18n('cancel')}
buttonType={SessionButtonType.Simple}
buttonColor={SessionButtonColor.Danger}
onClick={onClose}
dataTestId="session-confirm-cancel-button"
/>
</div>
</>
);
};
import { SessionButton, SessionButtonType } from '../basic/SessionButton';
interface SeedProps {
recoveryPhrase: string;
@ -102,7 +25,6 @@ const StyledRecoveryPhrase = styled.i``;
const Seed = (props: SeedProps) => {
const { recoveryPhrase, onClickCopy } = props;
const i18n = window.i18n;
const dispatch = useDispatch();
const theme = useSelector(getTheme);
@ -129,7 +51,7 @@ const Seed = (props: SeedProps) => {
maxWidth: '600px',
}}
>
{i18n('recoveryPhraseSavePromptMain')}
{window.i18n('recoveryPhraseSavePromptMain')}
</p>
<SessionQRCode
@ -161,7 +83,7 @@ const Seed = (props: SeedProps) => {
style={{ justifyContent: 'center', width: '100%' }}
>
<SessionButton
text={i18n('editMenuCopy')}
text={window.i18n('editMenuCopy')}
buttonType={SessionButtonType.Simple}
onClick={() => {
copyRecoveryPhrase(recoveryPhrase);
@ -184,52 +106,35 @@ const SessionSeedModalInner = (props: ModalInnerProps) => {
const { onClickOk } = props;
const [loadingSeed, setLoadingSeed] = useState(true);
const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [hasPassword, setHasPassword] = useState<null | boolean>(null);
const [passwordValid, setPasswordValid] = useState(false);
const [passwordHash, setPasswordHash] = useState('');
const dispatch = useDispatch();
useMount(() => {
async function validateAccess() {
if (passwordHash || recoveryPhrase) {
return;
}
const hash = await Data.getPasswordHash();
setHasPassword(!!hash);
setPasswordHash(hash || '');
const onClose = () => dispatch(recoveryPhraseModal(null));
usePasswordModal({
onSuccess: () => {
const newRecoveryPhrase = getCurrentRecoveryPhrase();
setRecoveryPhrase(newRecoveryPhrase);
setLoadingSeed(false);
}
setTimeout(() => (document.getElementById('seed-input-password') as any)?.focus(), 100);
void validateAccess();
},
onClose,
title: window.window.i18n('sessionRecoveryPassword'),
});
const onClose = () => dispatch(recoveryPhraseModal(null));
if (loadingSeed) {
return null;
}
return (
<>
{!loadingSeed && (
<SessionWrapperModal
title={window.i18n('sessionRecoveryPassword')}
onClose={onClose}
showExitIcon={true}
>
<StyledSeedModalContainer>
<SpacerSM />
{hasPassword && !passwordValid ? (
<Password passwordHash={passwordHash} setPasswordValid={setPasswordValid} />
) : (
<Seed recoveryPhrase={recoveryPhrase} onClickCopy={onClickOk} />
)}
</StyledSeedModalContainer>
</SessionWrapperModal>
)}
</>
<SessionWrapperModal
title={window.i18n('sessionRecoveryPassword')}
onClose={onClose}
showExitIcon={true}
>
<StyledSeedModalContainer>
<SpacerSM />
<Seed recoveryPhrase={recoveryPhrase} onClickCopy={onClickOk} />
</StyledSeedModalContainer>
</SessionWrapperModal>
);
};

@ -25,7 +25,7 @@ interface State {
currentPasswordRetypeEntered: string | null;
}
export class SessionPasswordDialog extends Component<Props, State> {
export class SessionSetPasswordDialog extends Component<Props, State> {
private passportInput: HTMLInputElement | null = null;
constructor(props: any) {

@ -0,0 +1,54 @@
import { isEmpty } from 'lodash';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { useMount } from 'react-use';
import { Data } from '../data/data';
import { updateEnterPasswordModal } from '../state/ducks/modalDialog';
export function usePasswordModal({
onSuccess,
onClose,
title,
}: {
onSuccess: () => void;
onClose: () => void;
title?: string;
}) {
const [passwordHash, setPasswordHash] = useState('');
const [passwordValid, setPasswordValid] = useState(false);
const dispatch = useDispatch();
const validateAccess = async () => {
if (!isEmpty(passwordHash)) {
return;
}
const hash = await Data.getPasswordHash();
if (hash && !isEmpty(hash)) {
setPasswordHash(hash);
dispatch(
updateEnterPasswordModal({
passwordHash,
passwordValid,
setPasswordValid,
onClickOk: () => {
onSuccess();
setPasswordHash('');
dispatch(updateEnterPasswordModal(null));
},
onClickClose: () => {
onClose();
setPasswordHash('');
dispatch(updateEnterPasswordModal(null));
},
title,
})
);
}
};
useMount(() => {
void validateAccess();
});
}

@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { EnterPasswordModalProps } from '../../components/dialog/EnterPasswordModal';
import { SessionConfirmDialogProps } from '../../components/dialog/SessionConfirm';
import type { EditProfilePictureModalProps, PasswordAction } from '../../types/ReduxTypes';
@ -19,6 +20,7 @@ export type ChangeNickNameModalState = InviteContactModalState;
export type EditProfileModalState = object | null;
export type OnionPathModalState = EditProfileModalState;
export type RecoveryPhraseModalState = EditProfileModalState;
export type EnterPasswordModalState = EnterPasswordModalProps | null;
export type DeleteAccountModalState = EditProfileModalState;
export type SessionPasswordModalState = { passwordAction: PasswordAction; onOk: () => void } | null;
@ -49,6 +51,7 @@ export type ModalState = {
editProfileModal: EditProfileModalState;
onionPathModal: OnionPathModalState;
recoveryPhraseModal: RecoveryPhraseModalState;
enterPasswordModal: EnterPasswordModalState;
sessionPasswordModal: SessionPasswordModalState;
deleteAccountModal: DeleteAccountModalState;
reactListModalState: ReactModalsState;
@ -69,6 +72,7 @@ export const initialModalState: ModalState = {
editProfileModal: null,
onionPathModal: null,
recoveryPhraseModal: null,
enterPasswordModal: null,
sessionPasswordModal: null,
deleteAccountModal: null,
reactListModalState: null,
@ -116,6 +120,9 @@ const ModalSlice = createSlice({
recoveryPhraseModal(state, action: PayloadAction<RecoveryPhraseModalState | null>) {
return { ...state, recoveryPhraseModal: action.payload };
},
updateEnterPasswordModal(state, action: PayloadAction<EnterPasswordModalState | null>) {
return { ...state, enterPasswordModal: action.payload };
},
sessionPassword(state, action: PayloadAction<SessionPasswordModalState>) {
return { ...state, sessionPasswordModal: action.payload };
},
@ -147,6 +154,7 @@ export const {
editProfileModal,
onionPathModal,
recoveryPhraseModal,
updateEnterPasswordModal,
sessionPassword,
updateDeleteAccountModal,
updateBanOrUnbanUserModal,

@ -8,6 +8,7 @@ import {
DeleteAccountModalState,
EditProfileModalState,
EditProfilePictureModalState,
EnterPasswordModalState,
InviteContactModalState,
ModalState,
OnionPathModalState,
@ -85,6 +86,11 @@ export const getRecoveryPhraseDialog = createSelector(
(state: ModalState): RecoveryPhraseModalState => state.recoveryPhraseModal
);
export const getEnterPasswordModalState = createSelector(
getModal,
(state: ModalState): EnterPasswordModalState => state.enterPasswordModal
);
export const getSessionPasswordDialog = createSelector(
getModal,
(state: ModalState): SessionPasswordModalState => state.sessionPasswordModal

@ -420,6 +420,7 @@ export type LocalizerKeys =
| 'readReceiptSettingDescription'
| 'readReceiptSettingTitle'
| 'received'
| 'recoveryPasswordDescription'
| 'recoveryPasswordEnter'
| 'recoveryPasswordErrorMessageGeneric'
| 'recoveryPasswordErrorMessageIncorrect'

Loading…
Cancel
Save