Display generated recovery tokens when enabling two factor

This commit is contained in:
Dane Everitt 2020-07-02 22:23:25 -07:00
parent c522935403
commit 795e045950
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
2 changed files with 82 additions and 49 deletions

View file

@ -1,9 +1,7 @@
import http from '@/api/http'; import http from '@/api/http';
export default (code: string): Promise<void> => { export default async (code: string): Promise<string[]> => {
return new Promise((resolve, reject) => { const { data } = await http.post('/api/client/account/two-factor', { code });
http.post('/api/client/account/two-factor', { code })
.then(() => resolve()) return data.attributes.tokens;
.catch(reject);
});
}; };

View file

@ -2,21 +2,22 @@ import React, { useEffect, useState } from 'react';
import Modal, { RequiredModalProps } from '@/components/elements/Modal'; import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import { Form, Formik, FormikHelpers } from 'formik'; import { Form, Formik, FormikHelpers } from 'formik';
import { object, string } from 'yup'; import { object, string } from 'yup';
import Field from '@/components/elements/Field';
import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl'; import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor'; import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy'; import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender';
import Field from '@/components/elements/Field';
interface Values { interface Values {
code: string; code: string;
} }
export default ({ ...props }: RequiredModalProps) => { export default ({ onDismissed, ...props }: RequiredModalProps) => {
const [ token, setToken ] = useState(''); const [ token, setToken ] = useState('');
const [ loading, setLoading ] = useState(true); const [ loading, setLoading ] = useState(true);
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData); const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
@ -27,22 +28,30 @@ export default ({ ...props }: RequiredModalProps) => {
.then(setToken) .then(setToken)
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
}); });
}, []); }, []);
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('account:two-factor'); clearFlashes('account:two-factor');
enableAccountTwoFactor(code) enableAccountTwoFactor(code)
.then(() => { .then(tokens => {
updateUserData({ useTotp: true }); setRecoveryTokens(tokens);
props.onDismissed();
}) })
.catch(error => { .catch(error => {
console.error(error); console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
setSubmitting(false); })
}); .then(() => setSubmitting(false));
};
const dismiss = () => {
if (recoveryTokens.length > 0) {
updateUserData({ useTotp: true });
}
onDismissed();
}; };
return ( return (
@ -58,47 +67,73 @@ export default ({ ...props }: RequiredModalProps) => {
{({ isSubmitting, isValid }) => ( {({ isSubmitting, isValid }) => (
<Modal <Modal
{...props} {...props}
onDismissed={dismiss}
dismissable={!isSubmitting} dismissable={!isSubmitting}
showSpinnerOverlay={loading || isSubmitting} showSpinnerOverlay={loading || isSubmitting}
closeOnEscape={!recoveryTokens}
closeOnBackground={!recoveryTokens}
> >
<Form className={'mb-0'}> {recoveryTokens.length > 0 ?
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/> <>
<div className={'flex flex-wrap'}> <h2 className={'mb-4'}>Two-factor authentication enabled</h2>
<div className={'w-full md:flex-1'}> <p className={'text-neutral-300'}>
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}> Two-factor authentication has been enabled on your account. Should you loose access to
{!token || !token.length ? this device you'll need to use on of the codes displayed below in order to access your
<img account.
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='} </p>
className={'w-64 h-64 rounded'} <p className={'text-neutral-300 mt-4'}>
<strong>These codes will not be displayed again.</strong> Please take note of them now
by storing them in a secure repository such as a password manager.
</p>
<pre className={'mt-4 rounded font-mono bg-neutral-900 p-4'}>
{recoveryTokens.map(token => <code key={token} className={'block mb-1'}>{token}</code>)}
</pre>
<div className={'text-right'}>
<button className={'mt-6 btn btn-lg btn-primary'} onClick={dismiss}>
Close
</button>
</div>
</>
:
<Form className={'mb-0'}>
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
<div className={'flex flex-wrap'}>
<div className={'w-full md:flex-1'}>
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
{!token || !token.length ?
<img
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
className={'w-64 h-64 rounded'}
/>
:
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
onLoad={() => setLoading(false)}
className={'w-full h-full shadow-none rounded-0'}
/>
}
</div>
</div>
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
<div className={'flex-1'}>
<Field
id={'code'}
name={'code'}
type={'text'}
title={'Code From Authenticator'}
description={'Enter the code from your authenticator device after scanning the QR image.'}
autoFocus={!loading}
/> />
: </div>
<img <div className={'mt-6 md:mt-0 text-right'}>
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`} <button className={'btn btn-primary btn-sm'} disabled={!isValid}>
onLoad={() => setLoading(false)} Setup
className={'w-full h-full shadow-none rounded-0'} </button>
/> </div>
}
</div> </div>
</div> </div>
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}> </Form>
<div className={'flex-1'}> }
<Field
id={'code'}
name={'code'}
type={'text'}
title={'Code From Authenticator'}
description={'Enter the code from your authenticator device after scanning the QR image.'}
autoFocus={!loading}
/>
</div>
<div className={'mt-6 md:mt-0 text-right'}>
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
Setup
</button>
</div>
</div>
</div>
</Form>
</Modal> </Modal>
)} )}
</Formik> </Formik>