Skip to content

Commit 795e045

Browse files
committed
Display generated recovery tokens when enabling two factor
1 parent c522935 commit 795e045

File tree

2 files changed

+81
-48
lines changed

2 files changed

+81
-48
lines changed
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import http from '@/api/http';
22

3-
export default (code: string): Promise<void> => {
4-
return new Promise((resolve, reject) => {
5-
http.post('/api/client/account/two-factor', { code })
6-
.then(() => resolve())
7-
.catch(reject);
8-
});
3+
export default async (code: string): Promise<string[]> => {
4+
const { data } = await http.post('/api/client/account/two-factor', { code });
5+
6+
return data.attributes.tokens;
97
};

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

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,22 @@ import React, { useEffect, useState } from 'react';
22
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
33
import { Form, Formik, FormikHelpers } from 'formik';
44
import { object, string } from 'yup';
5-
import Field from '@/components/elements/Field';
65
import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
76
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
8-
import FlashMessageRender from '@/components/FlashMessageRender';
97
import { Actions, useStoreActions } from 'easy-peasy';
108
import { ApplicationStore } from '@/state';
119
import { httpErrorToHuman } from '@/api/http';
10+
import FlashMessageRender from '@/components/FlashMessageRender';
11+
import Field from '@/components/elements/Field';
1212

1313
interface Values {
1414
code: string;
1515
}
1616

17-
export default ({ ...props }: RequiredModalProps) => {
17+
export default ({ onDismissed, ...props }: RequiredModalProps) => {
1818
const [ token, setToken ] = useState('');
1919
const [ loading, setLoading ] = useState(true);
20+
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
2021

2122
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
2223
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
@@ -27,22 +28,30 @@ export default ({ ...props }: RequiredModalProps) => {
2728
.then(setToken)
2829
.catch(error => {
2930
console.error(error);
31+
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
3032
});
3133
}, []);
3234

3335
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
3436
clearFlashes('account:two-factor');
3537
enableAccountTwoFactor(code)
36-
.then(() => {
37-
updateUserData({ useTotp: true });
38-
props.onDismissed();
38+
.then(tokens => {
39+
setRecoveryTokens(tokens);
3940
})
4041
.catch(error => {
4142
console.error(error);
4243

4344
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
44-
setSubmitting(false);
45-
});
45+
})
46+
.then(() => setSubmitting(false));
47+
};
48+
49+
const dismiss = () => {
50+
if (recoveryTokens.length > 0) {
51+
updateUserData({ useTotp: true });
52+
}
53+
54+
onDismissed();
4655
};
4756

4857
return (
@@ -58,47 +67,73 @@ export default ({ ...props }: RequiredModalProps) => {
5867
{({ isSubmitting, isValid }) => (
5968
<Modal
6069
{...props}
70+
onDismissed={dismiss}
6171
dismissable={!isSubmitting}
6272
showSpinnerOverlay={loading || isSubmitting}
73+
closeOnEscape={!recoveryTokens}
74+
closeOnBackground={!recoveryTokens}
6375
>
64-
<Form className={'mb-0'}>
65-
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
66-
<div className={'flex flex-wrap'}>
67-
<div className={'w-full md:flex-1'}>
68-
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
69-
{!token || !token.length ?
70-
<img
71-
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
72-
className={'w-64 h-64 rounded'}
73-
/>
74-
:
75-
<img
76-
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
77-
onLoad={() => setLoading(false)}
78-
className={'w-full h-full shadow-none rounded-0'}
79-
/>
80-
}
81-
</div>
76+
{recoveryTokens.length > 0 ?
77+
<>
78+
<h2 className={'mb-4'}>Two-factor authentication enabled</h2>
79+
<p className={'text-neutral-300'}>
80+
Two-factor authentication has been enabled on your account. Should you loose access to
81+
this device you'll need to use on of the codes displayed below in order to access your
82+
account.
83+
</p>
84+
<p className={'text-neutral-300 mt-4'}>
85+
<strong>These codes will not be displayed again.</strong> Please take note of them now
86+
by storing them in a secure repository such as a password manager.
87+
</p>
88+
<pre className={'mt-4 rounded font-mono bg-neutral-900 p-4'}>
89+
{recoveryTokens.map(token => <code key={token} className={'block mb-1'}>{token}</code>)}
90+
</pre>
91+
<div className={'text-right'}>
92+
<button className={'mt-6 btn btn-lg btn-primary'} onClick={dismiss}>
93+
Close
94+
</button>
8295
</div>
83-
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
84-
<div className={'flex-1'}>
85-
<Field
86-
id={'code'}
87-
name={'code'}
88-
type={'text'}
89-
title={'Code From Authenticator'}
90-
description={'Enter the code from your authenticator device after scanning the QR image.'}
91-
autoFocus={!loading}
92-
/>
96+
</>
97+
:
98+
<Form className={'mb-0'}>
99+
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
100+
<div className={'flex flex-wrap'}>
101+
<div className={'w-full md:flex-1'}>
102+
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
103+
{!token || !token.length ?
104+
<img
105+
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
106+
className={'w-64 h-64 rounded'}
107+
/>
108+
:
109+
<img
110+
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
111+
onLoad={() => setLoading(false)}
112+
className={'w-full h-full shadow-none rounded-0'}
113+
/>
114+
}
115+
</div>
93116
</div>
94-
<div className={'mt-6 md:mt-0 text-right'}>
95-
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
96-
Setup
97-
</button>
117+
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
118+
<div className={'flex-1'}>
119+
<Field
120+
id={'code'}
121+
name={'code'}
122+
type={'text'}
123+
title={'Code From Authenticator'}
124+
description={'Enter the code from your authenticator device after scanning the QR image.'}
125+
autoFocus={!loading}
126+
/>
127+
</div>
128+
<div className={'mt-6 md:mt-0 text-right'}>
129+
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
130+
Setup
131+
</button>
132+
</div>
98133
</div>
99134
</div>
100-
</div>
101-
</Form>
135+
</Form>
136+
}
102137
</Modal>
103138
)}
104139
</Formik>

0 commit comments

Comments
 (0)