Skip to content

Commit 8229494

Browse files
committed
Use new two-step configuration modal
1 parent 870a964 commit 8229494

File tree

7 files changed

+219
-182
lines changed

7 files changed

+219
-182
lines changed

resources/scripts/components/dashboard/AccountOverviewContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default () => {
4444
<ContentBox css={tw`mt-8 sm:mt-0 sm:ml-8`} title={'Update Email Address'} showFlashes={'account:email'}>
4545
<UpdateEmailAddressForm />
4646
</ContentBox>
47-
<ContentBox css={tw`md:ml-8 mt-8 md:mt-0`} title={'Configure Two Factor'}>
47+
<ContentBox css={tw`md:ml-8 mt-8 md:mt-0`} title={'Two-Step Verification'}>
4848
<ConfigureTwoFactorForm />
4949
</ContentBox>
5050
</Container>

resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
import React, { useState } from 'react';
22
import { useStoreState } from 'easy-peasy';
33
import { ApplicationStore } from '@/state';
4-
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
5-
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
64
import tw from 'twin.macro';
7-
import Button from '@/components/elements/Button';
5+
import { Button } from '@/components/elements/button/index';
6+
import SetupTOTPModal from '@/components/dashboard/forms/SetupTOTPModal';
7+
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
88

99
export default () => {
10-
const [visible, setVisible] = useState(false);
10+
const [visible, setVisible] = useState<'enable' | 'disable' | null>(null);
1111
const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp);
1212

1313
return (
1414
<div>
15-
{visible &&
16-
(isEnabled ? (
17-
<DisableTwoFactorModal visible={visible} onModalDismissed={() => setVisible(false)} />
18-
) : (
19-
<SetupTwoFactorModal visible={visible} onModalDismissed={() => setVisible(false)} />
20-
))}
15+
<SetupTOTPModal open={visible === 'enable'} onClose={() => setVisible(null)} />
16+
<DisableTwoFactorModal visible={visible === 'disable'} onModalDismissed={() => setVisible(null)} />
2117
<p css={tw`text-sm`}>
2218
{isEnabled
23-
? 'Two-factor authentication is currently enabled on your account.'
24-
: 'You do not currently have two-factor authentication enabled on your account. Click the button below to begin configuring it.'}
19+
? 'Two-step verification is currently enabled on your account.'
20+
: 'You do not currently have two-step verification enabled on your account. Click the button below to begin configuring it.'}
2521
</p>
2622
<div css={tw`mt-6`}>
27-
<Button color={'red'} isSecondary onClick={() => setVisible(true)}>
28-
{isEnabled ? 'Disable' : 'Enable'}
29-
</Button>
23+
{isEnabled ? (
24+
<Button.Danger onClick={() => setVisible('disable')}>Disable Two-Step</Button.Danger>
25+
) : (
26+
<Button onClick={() => setVisible('enable')}>Enable Two-Step</Button>
27+
)}
3028
</div>
3129
</div>
3230
);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import { DialogProps } from '@/components/elements/dialog/Dialog';
3+
import { Dialog } from '@/components/elements/dialog';
4+
import { Button } from '@/components/elements/button/index';
5+
import CopyOnClick from '@/components/elements/CopyOnClick';
6+
import { Alert } from '@/components/elements/alert';
7+
8+
interface RecoveryTokenDialogProps extends DialogProps {
9+
tokens: string[];
10+
}
11+
12+
export default ({ tokens, open, onClose }: RecoveryTokenDialogProps) => {
13+
const grouped = [] as [string, string][];
14+
tokens.forEach((token, index) => {
15+
if (index % 2 === 0) {
16+
grouped.push([token, tokens[index + 1] || '']);
17+
}
18+
});
19+
20+
return (
21+
<Dialog
22+
open={open}
23+
onClose={onClose}
24+
title={'Two-Step Authentication Enabled'}
25+
description={
26+
'Store the codes below somewhere safe. If you lose access to your phone you can use these backup codes to sign in.'
27+
}
28+
hideCloseIcon
29+
preventExternalClose
30+
>
31+
<Dialog.Icon position={'container'} type={'success'} />
32+
<CopyOnClick text={tokens.join('\n')} showInNotification={false}>
33+
<pre className={'bg-gray-800 rounded p-2 mt-6'}>
34+
{grouped.map((value) => (
35+
<span key={value.join('_')} className={'block'}>
36+
{value[0]}
37+
<span className={'mx-2 selection:bg-gray-800'}>&nbsp;</span>
38+
{value[1]}
39+
<span className={'selection:bg-gray-800'}>&nbsp;</span>
40+
</span>
41+
))}
42+
</pre>
43+
</CopyOnClick>
44+
<Alert type={'danger'} className={'mt-3'}>
45+
These codes will not be shown again.
46+
</Alert>
47+
<Dialog.Footer>
48+
<Button.Text onClick={onClose}>Done</Button.Text>
49+
</Dialog.Footer>
50+
</Dialog>
51+
);
52+
};
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { Dialog } from '@/components/elements/dialog';
3+
import { DialogProps } from '@/components/elements/dialog/Dialog';
4+
import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData';
5+
import { useFlashKey } from '@/plugins/useFlash';
6+
import tw from 'twin.macro';
7+
import QRCode from 'qrcode.react';
8+
import { Button } from '@/components/elements/button/index';
9+
import Spinner from '@/components/elements/Spinner';
10+
import { Input } from '@/components/elements/inputs';
11+
import CopyOnClick from '@/components/elements/CopyOnClick';
12+
import Tooltip from '@/components/elements/tooltip/Tooltip';
13+
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
14+
import FlashMessageRender from '@/components/FlashMessageRender';
15+
import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog';
16+
import { Actions, useStoreActions } from 'easy-peasy';
17+
import { ApplicationStore } from '@/state';
18+
19+
type SetupTOTPModalProps = DialogProps;
20+
21+
export default ({ open, onClose }: SetupTOTPModalProps) => {
22+
const [submitting, setSubmitting] = useState(false);
23+
const [value, setValue] = useState('');
24+
const [tokens, setTokens] = useState<string[]>([]);
25+
const [token, setToken] = useState<TwoFactorTokenData | null>(null);
26+
const { clearAndAddHttpError } = useFlashKey('account:two-step');
27+
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
28+
29+
useEffect(() => {
30+
if (!open) return;
31+
32+
getTwoFactorTokenData()
33+
.then(setToken)
34+
.then(() => updateUserData({ useTotp: true }))
35+
.catch((error) => clearAndAddHttpError(error));
36+
}, [open]);
37+
38+
useEffect(() => {
39+
if (!open) return;
40+
41+
return () => {
42+
setToken(null);
43+
setValue('');
44+
setSubmitting(false);
45+
clearAndAddHttpError(undefined);
46+
};
47+
}, [open]);
48+
49+
const submit = () => {
50+
if (submitting) return;
51+
52+
setSubmitting(true);
53+
clearAndAddHttpError();
54+
55+
enableAccountTwoFactor(value)
56+
.then(setTokens)
57+
.catch(clearAndAddHttpError)
58+
.then(() => setSubmitting(false));
59+
};
60+
61+
return (
62+
<>
63+
<RecoveryTokensDialog tokens={tokens} open={open && tokens.length > 0} onClose={onClose} />
64+
<Dialog
65+
open={open && !tokens.length}
66+
onClose={onClose}
67+
title={'Enable Two-Step Verification'}
68+
preventExternalClose={submitting}
69+
description={
70+
"Help protect your account from unauthorized access. You'll be prompted for a verification code each time you sign in."
71+
}
72+
>
73+
<FlashMessageRender byKey={'account:two-step'} className={'mt-4'} />
74+
<div
75+
className={
76+
'flex items-center justify-center w-56 h-56 p-2 bg-gray-800 rounded-lg shadow mx-auto mt-6'
77+
}
78+
>
79+
{!token ? (
80+
<Spinner />
81+
) : (
82+
<QRCode
83+
renderAs={'svg'}
84+
value={token.image_url_data}
85+
css={tw`w-full h-full shadow-none rounded`}
86+
/>
87+
)}
88+
</div>
89+
<CopyOnClick text={token?.secret}>
90+
<p className={'font-mono text-sm text-gray-100 text-center mt-2'}>
91+
{token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'}
92+
</p>
93+
</CopyOnClick>
94+
<div className={'mt-6'}>
95+
<p>
96+
Scan the QR code above using the two-step authentication app of your choice. Then, enter the
97+
6-digit code generated into the field below.
98+
</p>
99+
</div>
100+
<Input.Text
101+
variant={Input.Text.Variants.Loose}
102+
value={value}
103+
onChange={(e) => setValue(e.currentTarget.value)}
104+
className={'mt-4'}
105+
placeholder={'000000'}
106+
type={'text'}
107+
inputMode={'numeric'}
108+
autoComplete={'one-time-code'}
109+
pattern={'\\d{6}'}
110+
/>
111+
<Dialog.Footer>
112+
<Button.Text onClick={onClose}>Cancel</Button.Text>
113+
<Tooltip
114+
disabled={value.length === 6}
115+
content={
116+
!token ? 'Waiting for QR code to load...' : 'You must enter the 6-digit code to continue.'
117+
}
118+
delay={100}
119+
>
120+
<Button disabled={!token || value.length !== 6} onClick={submit}>
121+
Enable
122+
</Button>
123+
</Tooltip>
124+
</Dialog.Footer>
125+
</Dialog>
126+
</>
127+
);
128+
};

resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx

Lines changed: 0 additions & 159 deletions
This file was deleted.

0 commit comments

Comments
 (0)