Update totp disable modal; require password for enable operation

This commit is contained in:
DaneEveritt 2022-07-03 14:27:37 -04:00
parent 92926ca193
commit 2d836156d2
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
10 changed files with 182 additions and 121 deletions

View file

@ -1,16 +1,24 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import tw from 'twin.macro';
import { Button } from '@/components/elements/button/index';
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
import SetupTOTPModal from '@/components/dashboard/forms/SetupTOTPModal';
import SetupTOTPDialog from '@/components/dashboard/forms/SetupTOTPDialog';
import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog';
import DisableTOTPDialog from '@/components/dashboard/forms/DisableTOTPDialog';
import { useFlashKey } from '@/plugins/useFlash';
export default () => {
const [tokens, setTokens] = useState<string[]>([]);
const [visible, setVisible] = useState<'enable' | 'disable' | null>(null);
const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp);
const { clearAndAddHttpError } = useFlashKey('account:two-step');
useEffect(() => {
return () => {
clearAndAddHttpError();
};
}, [visible]);
const onTokens = (tokens: string[]) => {
setTokens(tokens);
@ -19,9 +27,9 @@ export default () => {
return (
<div>
<SetupTOTPModal open={visible === 'enable'} onClose={() => setVisible(null)} onTokens={onTokens} />
<SetupTOTPDialog open={visible === 'enable'} onClose={() => setVisible(null)} onTokens={onTokens} />
<RecoveryTokensDialog tokens={tokens} open={tokens.length > 0} onClose={() => setTokens([])} />
<DisableTwoFactorModal visible={visible === 'disable'} onModalDismissed={() => setVisible(null)} />
<DisableTOTPDialog open={visible === 'disable'} onClose={() => setVisible(null)} />
<p css={tw`text-sm`}>
{isEnabled
? 'Two-step verification is currently enabled on your account.'

View file

@ -0,0 +1,72 @@
import React, { useContext, useEffect, useState } from 'react';
import asDialog from '@/hoc/asDialog';
import { Dialog, DialogWrapperContext } from '@/components/elements/dialog';
import { Button } from '@/components/elements/button/index';
import { Input } from '@/components/elements/inputs';
import Tooltip from '@/components/elements/tooltip/Tooltip';
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
import { useFlashKey } from '@/plugins/useFlash';
import { useStoreActions } from '@/state/hooks';
import FlashMessageRender from '@/components/FlashMessageRender';
const DisableTOTPDialog = () => {
const [submitting, setSubmitting] = useState(false);
const [password, setPassword] = useState('');
const { clearAndAddHttpError } = useFlashKey('account:two-step');
const { close, setProps } = useContext(DialogWrapperContext);
const updateUserData = useStoreActions((actions) => actions.user.updateUserData);
useEffect(() => {
setProps((state) => ({ ...state, preventExternalClose: submitting }));
}, [submitting]);
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
if (submitting) return;
setSubmitting(true);
clearAndAddHttpError();
disableAccountTwoFactor(password)
.then(() => {
updateUserData({ useTotp: false });
close();
})
.catch(clearAndAddHttpError)
.then(() => setSubmitting(false));
};
return (
<form id={'disable-totp-form'} className={'mt-6'} onSubmit={submit}>
<FlashMessageRender byKey={'account:two-step'} className={'-mt-2 mb-6'} />
<label className={'block pb-1'} htmlFor={'totp-password'}>
Password
</label>
<Input.Text
id={'totp-password'}
type={'password'}
variant={Input.Text.Variants.Loose}
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
/>
<Dialog.Footer>
<Button.Text onClick={close}>Cancel</Button.Text>
<Tooltip
delay={100}
disabled={password.length > 0}
content={'You must enter your account password to continue.'}
>
<Button.Danger type={'submit'} form={'disable-totp-form'} disabled={submitting || !password.length}>
Disable
</Button.Danger>
</Tooltip>
</Dialog.Footer>
</form>
);
};
export default asDialog({
title: 'Disable Two-Step Verification',
description: 'Disabling two-step verification will make your account less secure.',
})(DisableTOTPDialog);

View file

@ -1,73 +0,0 @@
import React, { useContext } from 'react';
import { Form, Formik, FormikHelpers } from 'formik';
import FlashMessageRender from '@/components/FlashMessageRender';
import Field from '@/components/elements/Field';
import { object, string } from 'yup';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import asModal from '@/hoc/asModal';
import ModalContext from '@/context/ModalContext';
interface Values {
password: string;
}
const DisableTwoFactorModal = () => {
const { dismiss, setPropOverrides } = useContext(ModalContext);
const { clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const submit = ({ password }: Values, { setSubmitting }: FormikHelpers<Values>) => {
setPropOverrides({ showSpinnerOverlay: true, dismissable: false });
disableAccountTwoFactor(password)
.then(() => {
updateUserData({ useTotp: false });
dismiss();
})
.catch((error) => {
console.error(error);
clearAndAddHttpError({ error, key: 'account:two-factor' });
setSubmitting(false);
setPropOverrides(null);
});
};
return (
<Formik
onSubmit={submit}
initialValues={{
password: '',
}}
validationSchema={object().shape({
password: string().required('You must provide your current password in order to continue.'),
})}
>
{({ isValid }) => (
<Form className={'mb-0'}>
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'} />
<Field
id={'password'}
name={'password'}
type={'password'}
label={'Current Password'}
description={
'In order to disable two-factor authentication you will need to provide your account password.'
}
autoFocus
/>
<div css={tw`mt-6 text-right`}>
<Button color={'red'} disabled={!isValid}>
Disable Two-Factor
</Button>
</div>
</Form>
)}
</Formik>
);
};
export default asModal()(DisableTwoFactorModal);

View file

@ -22,11 +22,12 @@ interface Props {
const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
const [submitting, setSubmitting] = useState(false);
const [value, setValue] = useState('');
const [password, setPassword] = useState('');
const [token, setToken] = useState<TwoFactorTokenData | null>(null);
const { clearAndAddHttpError } = useFlashKey('account:two-step');
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const { close } = useContext(DialogWrapperContext);
const { close, setProps } = useContext(DialogWrapperContext);
useEffect(() => {
getTwoFactorTokenData()
@ -34,13 +35,19 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
.catch((error) => clearAndAddHttpError(error));
}, []);
const submit = () => {
useEffect(() => {
setProps((state) => ({ ...state, preventExternalClose: submitting }));
}, [submitting]);
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
if (submitting) return;
setSubmitting(true);
clearAndAddHttpError();
enableAccountTwoFactor(value)
enableAccountTwoFactor(value, password)
.then((tokens) => {
updateUserData({ useTotp: true });
onTokens(tokens);
@ -52,7 +59,7 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
};
return (
<>
<form id={'enable-totp-form'} onSubmit={submit}>
<FlashMessageRender byKey={'account:two-step'} className={'mt-4'} />
<div
className={'flex items-center justify-center w-56 h-56 p-2 bg-gray-800 rounded-lg shadow mx-auto mt-6'}
@ -68,36 +75,53 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => {
{token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'}
</p>
</CopyOnClick>
<div className={'mt-6'}>
<p>
Scan the QR code above using the two-step authentication app of your choice. Then, enter the 6-digit
code generated into the field below.
</p>
</div>
<p id={'totp-code-description'} className={'mt-6'}>
Scan the QR code above using the two-step authentication app of your choice. Then, enter the 6-digit
code generated into the field below.
</p>
<Input.Text
aria-labelledby={'totp-code-description'}
variant={Input.Text.Variants.Loose}
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
className={'mt-4'}
className={'mt-3'}
placeholder={'000000'}
type={'text'}
inputMode={'numeric'}
autoComplete={'one-time-code'}
pattern={'\\d{6}'}
/>
<label htmlFor={'totp-password'} className={'block mt-3'}>
Account Password
</label>
<Input.Text
variant={Input.Text.Variants.Loose}
className={'mt-1'}
type={'password'}
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
/>
<Dialog.Footer>
<Button.Text onClick={close}>Cancel</Button.Text>
<Tooltip
disabled={value.length === 6}
content={!token ? 'Waiting for QR code to load...' : 'You must enter the 6-digit code to continue.'}
disabled={password.length > 0 && value.length === 6}
content={
!token
? 'Waiting for QR code to load...'
: 'You must enter the 6-digit code and your password to continue.'
}
delay={100}
>
<Button disabled={!token || value.length !== 6} onClick={submit}>
<Button
disabled={!token || value.length !== 6 || !password.length}
type={'submit'}
form={'enable-totp-form'}
>
Enable
</Button>
</Tooltip>
</Dialog.Footer>
</>
</form>
);
};

View file

@ -7,7 +7,7 @@ import Field from '@/components/elements/Field';
import { httpErrorToHuman } from '@/api/http';
import { ApplicationStore } from '@/state';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import { Button } from '@/components/elements/button/index';
interface Values {
email: string;
@ -66,9 +66,7 @@ export default () => {
/>
</div>
<div css={tw`mt-6`}>
<Button size={'small'} disabled={isSubmitting || !isValid}>
Update Email
</Button>
<Button disabled={isSubmitting || !isValid}>Update Email</Button>
</div>
</Form>
</React.Fragment>

View file

@ -8,7 +8,7 @@ import updateAccountPassword from '@/api/account/updateAccountPassword';
import { httpErrorToHuman } from '@/api/http';
import { ApplicationStore } from '@/state';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import { Button } from '@/components/elements/button/index';
interface Values {
current: string;
@ -91,9 +91,7 @@ export default () => {
/>
</div>
<div css={tw`mt-6`}>
<Button size={'small'} disabled={isSubmitting || !isValid}>
Update Password
</Button>
<Button disabled={isSubmitting || !isValid}>Update Password</Button>
</div>
</Form>
</React.Fragment>

View file

@ -27,7 +27,7 @@ export interface RenderDialogProps extends DialogProps {
export type WrapperProps = Omit<RenderDialogProps, 'children' | 'open' | 'onClose'>;
export interface DialogWrapperContextType {
props: Readonly<WrapperProps>;
setProps: Callback<Partial<WrapperProps>>;
setProps: React.Dispatch<React.SetStateAction<WrapperProps>>;
close: () => void;
}