Add base logic to configure two factor on account

This commit is contained in:
Dane Everitt 2019-12-22 17:03:44 -08:00
parent edf27a5542
commit eb39826f46
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
15 changed files with 389 additions and 54 deletions

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (code: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.post('/api/client/account/two-factor', { code })
.then(() => resolve())
.catch(reject);
});
};

View file

@ -0,0 +1,9 @@
import http from '@/api/http';
export default (): Promise<string> => {
return new Promise((resolve, reject) => {
http.get('/api/client/account/two-factor')
.then(({ data }) => resolve(data.data.image_url_data))
.catch(reject);
});
};

View file

@ -8,6 +8,7 @@ import ServerRouter from '@/routers/ServerRouter';
import AuthenticationRouter from '@/routers/AuthenticationRouter';
import { Provider } from 'react-redux';
import { SiteSettings } from '@/state/settings';
import { DefaultTheme, ThemeProvider } from 'styled-components';
interface ExtendedWindow extends Window {
SiteConfiguration?: SiteSettings;
@ -23,6 +24,16 @@ interface ExtendedWindow extends Window {
};
}
const theme: DefaultTheme = {
breakpoints: {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
},
};
const App = () => {
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
if (PterodactylUser && !store.getState().user.data) {
@ -43,21 +54,23 @@ const App = () => {
}
return (
<StoreProvider store={store}>
<Provider store={store}>
<Router basename={'/'}>
<div className={'mx-auto w-auto'}>
<BrowserRouter basename={'/'}>
<Switch>
<Route path="/server/:id" component={ServerRouter}/>
<Route path="/auth" component={AuthenticationRouter}/>
<Route path="/" component={DashboardRouter}/>
</Switch>
</BrowserRouter>
</div>
</Router>
</Provider>
</StoreProvider>
<ThemeProvider theme={theme}>
<StoreProvider store={store}>
<Provider store={store}>
<Router basename={'/'}>
<div className={'mx-auto w-auto'}>
<BrowserRouter basename={'/'}>
<Switch>
<Route path="/server/:id" component={ServerRouter}/>
<Route path="/auth" component={AuthenticationRouter}/>
<Route path="/" component={DashboardRouter}/>
</Switch>
</BrowserRouter>
</div>
</Router>
</Provider>
</StoreProvider>
</ThemeProvider>
);
};

View file

@ -2,16 +2,38 @@ import * as React from 'react';
import ContentBox from '@/components/elements/ContentBox';
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFactorForm';
import styled from 'styled-components';
import { breakpoint } from 'styled-components-breakpoint';
const Container = styled.div`
${tw`flex flex-wrap my-10`};
& > div {
${tw`w-full`};
${breakpoint('md')`
width: calc(50% - 1rem);
`}
${breakpoint('xl')`
${tw`w-auto flex-1`};
`}
}
`;
export default () => {
return (
<div className={'flex my-10'}>
<ContentBox className={'flex-1 mr-4'} title={'Update Password'} showFlashes={'account:password'}>
<Container>
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
<UpdatePasswordForm/>
</ContentBox>
<ContentBox className={'flex-1 ml-4'} title={'Update Email Address'} showFlashes={'account:email'}>
<ContentBox className={'mt-8 md:mt-0 md:ml-8'} title={'Update Email Address'} showFlashes={'account:email'}>
<UpdateEmailAddressForm/>
</ContentBox>
</div>
<ContentBox className={'xl:ml-8 mt-8 xl:mt-0'} title={'Configure Two Factor'}>
<ConfigureTwoFactorForm/>
</ContentBox>
</Container>
);
};

View file

@ -0,0 +1,38 @@
import React, { useState } from 'react';
import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
export default () => {
const user = useStoreState((state: ApplicationStore) => state.user.data!);
const [visible, setVisible] = useState(false);
return user.useTotp ?
<div>
<p className={'text-sm'}>
Two-factor authentication is currently enabled on your account.
</p>
<div className={'mt-6'}>
<button className={'btn btn-red btn-secondary btn-sm'}>
Disable
</button>
</div>
</div>
:
<div>
<SetupTwoFactorModal visible={visible} onDismissed={() => setVisible(false)}/>
<p className={'text-sm'}>
You do not currently have two-factor authentication enabled on your account. Click
the button below to begin configuring it.
</p>
<div className={'mt-6'}>
<button
onClick={() => setVisible(true)}
className={'btn btn-green btn-secondary btn-sm'}
>
Begin Setup
</button>
</div>
</div>
;
};

View file

@ -0,0 +1,113 @@
import React, { useEffect, useState } from 'react';
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import { Form, Formik, FormikActions } from 'formik';
import { object, string } from 'yup';
import Field from '@/components/elements/Field';
import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
interface Values {
code: string;
}
export default ({ visible, onDismissed }: RequiredModalProps) => {
const [ token, setToken ] = useState('');
const [ loading, setLoading ] = useState(true);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
useEffect(() => {
if (!visible) {
clearFlashes('account:two-factor');
getTwoFactorTokenUrl()
.then(setToken)
.catch(error => {
console.error(error);
});
}
}, [ visible ]);
const submit = ({ code }: Values, { resetForm, setSubmitting }: FormikActions<Values>) => {
clearFlashes('account:two-factor');
enableAccountTwoFactor(code)
.then(() => {
resetForm();
setToken('');
setLoading(true);
})
.catch(error => {
console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
setSubmitting(false);
});
};
return (
<Formik
onSubmit={submit}
initialValues={{ code: '' }}
validationSchema={object().shape({
code: string()
.required('You must provide an authentication code to continue.')
.matches(/^(\d){6}$/, 'Authenticator code must be 6 digits.'),
})}
>
{({ isSubmitting, isValid, resetForm }) => (
<Modal
visible={visible}
onDismissed={() => {
resetForm();
setToken('');
setLoading(true);
onDismissed();
}}
dismissable={!isSubmitting}
showSpinnerOverlay={loading || isSubmitting}
>
<Form className={'mb-0'}>
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
<div className={'flex'}>
<div className={'flex-1'}>
<div className={'w-64 h-64 bg-neutral-600 p-2 rounded'}>
{!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={'flex-1 flex 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>
<div className={'text-right'}>
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
Setup
</button>
</div>
</div>
</div>
</Form>
</Modal>
)}
</Formik>
);
};

View file

@ -1 +1 @@
declare function tw(a: TemplateStringsArray | string): any;
declare function tw (a: TemplateStringsArray | string): any;

17
resources/scripts/style.d.ts vendored Normal file
View file

@ -0,0 +1,17 @@
import { Breakpoints, css, DefaultTheme, StyledProps } from 'styled-components';
declare module 'styled-components' {
type Breakpoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
export interface DefaultTheme {
breakpoints: {
[name in 'xs' | 'sm' | 'md' | 'lg' | 'xl']: number;
};
}
}
declare module 'styled-components-breakpoint' {
type CSSFunction = (...params: Parameters<typeof css>) => <P extends object>({ theme }: StyledProps<P>) => ReturnType<typeof css>;
export const breakpoint: (breakpointA: Breakpoints, breakpointB?: Breakpoints) => CSSFunction;
}

View file

@ -145,32 +145,29 @@ a.btn {
@apply .rounded .p-2 .uppercase .tracking-wide .text-sm;
transition: all 150ms linear;
/**
* Button Colors
*/
&.btn-primary {
@apply .bg-primary-500 .border-primary-600 .border .text-primary-50;
&.btn-secondary {
@apply .border .border-neutral-600 .bg-transparent .text-neutral-200;
&:hover:not(:disabled) {
@apply .bg-primary-600 .border-primary-700;
@apply .border-neutral-500 .text-neutral-100;
}
}
&.btn-green {
@apply .bg-green-500 .border-green-600 .border .text-green-50;
&:hover:not(:disabled) {
@apply .bg-green-600 .border-green-700;
}
}
&.btn-red {
&:not(.btn-secondary) {
&.btn-red:hover:not(:disabled) {
@apply .bg-red-500 .border-red-600 .text-red-50;
}
&.btn-green:hover:not(:disabled) {
@apply .bg-green-500 .border-green-600 .text-green-50;
}
}
&.btn-primary {
&:not(.btn-secondary) {
@apply .bg-primary-500 .border-primary-600 .border .text-primary-50;
}
&:hover:not(:disabled) {
@apply .bg-red-600 .border-red-700;
@apply .bg-primary-600 .border-primary-700;
}
}
@ -182,16 +179,24 @@ a.btn {
}
}
&.btn-secondary {
@apply .border .border-neutral-600 .bg-transparent .text-neutral-200;
&:hover:not(:disabled) {
@apply .border-neutral-500 .text-neutral-100;
&.btn-green {
&:not(.btn-secondary) {
@apply .bg-green-500 .border-green-600 .border .text-green-50;
}
&.btn-red:hover:not(:disabled) {
&:hover:not(:disabled), &.btn-secondary:active:not(:disabled) {
@apply .bg-green-600 .border-green-700;
}
}
&.btn-red {
&:not(.btn-secondary) {
@apply .bg-red-500 .border-red-600 .text-red-50;
}
&:hover:not(:disabled), &.btn-secondary:active:not(:disabled) {
@apply .bg-red-600 .border-red-700;
}
}
/**