feat: use pininput for 2fa

This commit is contained in:
diced
2023-03-04 14:40:54 -08:00
parent df013a52d1
commit 2c24cafab8
3 changed files with 93 additions and 58 deletions

View File

@@ -1,4 +1,4 @@
import { Button, Center, Image, Modal, NumberInput, Text, Title } from '@mantine/core'; import { Button, Center, Image, Modal, NumberInput, PinInput, Text, Title } from '@mantine/core';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { CheckIcon, CrossIcon } from 'components/icons'; import { CheckIcon, CrossIcon } from 'components/icons';
@@ -9,9 +9,7 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
const [secret, setSecret] = useState(''); const [secret, setSecret] = useState('');
const [qrCode, setQrCode] = useState(''); const [qrCode, setQrCode] = useState('');
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const [code, setCode] = useState(undefined);
const [error, setError] = useState(''); const [error, setError] = useState('');
const form = useForm();
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -34,15 +32,15 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
})(); })();
}, [opened]); }, [opened]);
const disableTotp = async () => { const disableTotp = async (code) => {
setDisabled(true); setDisabled(true);
const str = code.toString(); if (code.length !== 6) {
if (str.length !== 6) { setDisabled(false);
return setError('Code must be 6 digits'); return setError('Code must be 6 digits');
} }
const resp = await useFetch('/api/user/mfa/totp', 'DELETE', { const resp = await useFetch('/api/user/mfa/totp', 'DELETE', {
code: str, code,
}); });
if (resp.error) { if (resp.error) {
@@ -63,16 +61,16 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
setDisabled(false); setDisabled(false);
}; };
const verifyCode = async () => { const verifyCode = async (code) => {
setDisabled(true); setDisabled(true);
const str = code.toString(); if (code.length !== 6) {
if (str.length !== 6) { setDisabled(false);
return setError('Code must be 6 digits'); return setError('Code must be 6 digits');
} }
const resp = await useFetch('/api/user/mfa/totp', 'POST', { const resp = await useFetch('/api/user/mfa/totp', 'POST', {
secret, secret,
code: str, code,
register: true, register: true,
}); });
@@ -94,6 +92,13 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
setDisabled(false); setDisabled(false);
}; };
const handlePinChange = (value) => {
if (value.length === 6) {
setDisabled(true);
deleteTotp ? disableTotp(value) : verifyCode(value);
}
};
return ( return (
<Modal <Modal
opened={opened} opened={opened}
@@ -112,39 +117,39 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
<Center> <Center>
<Image height={180} width={180} src={qrCode} alt='QR Code' withPlaceholder /> <Image height={180} width={180} src={qrCode} alt='QR Code' withPlaceholder />
</Center> </Center>
<Text my='sm'>QR Code not working? Try manually entering the code into your app: {secret}</Text>
</> </>
)} )}
<form <Center my='md'>
onSubmit={form.onSubmit(() => { <PinInput
deleteTotp ? disableTotp() : verifyCode();
})}
>
<NumberInput
placeholder='2FA Code'
label='Verify'
size='xl'
hideControls
maxLength={6}
minLength={6}
value={code}
onChange={(e) => setCode(e)}
data-autofocus data-autofocus
error={error} length={6}
/> oneTimeCode
type='number'
<Button placeholder=''
onChange={handlePinChange}
autoFocus={true}
error={!!error}
disabled={disabled} disabled={disabled}
size='lg' size='xl'
fullWidth />
mt='md' </Center>
rightIcon={<CheckIcon />}
onClick={deleteTotp ? disableTotp : verifyCode} {error && (
> <Text my='sm' size='sm' color='red' align='center'>
Verify{deleteTotp ? ' and Disable' : ''} {error}
</Button> </Text>
</form> )}
{!deleteTotp && (
<Text my='sm' size='sm' color='gray' align='center'>
QR Code not working? Try manually entering the code into your app: {secret}
</Text>
)}
<Button disabled={disabled} size='lg' fullWidth mt='md' rightIcon={<CheckIcon />} type='submit'>
Verify{deleteTotp ? ' and Disable' : ''}
</Button>
</Modal> </Modal>
); );
} }

View File

@@ -413,7 +413,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
<Box my='md'> <Box my='md'>
<Title>Two Factor Authentication</Title> <Title>Two Factor Authentication</Title>
<MutedText size='md'> <MutedText size='md'>
{user.totpSecret {totpEnabled
? 'You have two factor authentication enabled.' ? 'You have two factor authentication enabled.'
: 'You do not have two factor authentication enabled.'} : 'You do not have two factor authentication enabled.'}
</MutedText> </MutedText>

View File

@@ -6,6 +6,8 @@ import {
Modal, Modal,
NumberInput, NumberInput,
PasswordInput, PasswordInput,
PinInput,
Text,
TextInput, TextInput,
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
@@ -23,10 +25,11 @@ export default function Login({ title, user_registration, oauth_registration, oa
// totp modal // totp modal
const [totpOpen, setTotpOpen] = useState(false); const [totpOpen, setTotpOpen] = useState(false);
const [code, setCode] = useState(undefined);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const [loading, setLoading] = useState(false);
const oauth_providers = JSON.parse(unparsed); const oauth_providers = JSON.parse(unparsed);
const icons = { const icons = {
@@ -46,8 +49,10 @@ export default function Login({ title, user_registration, oauth_registration, oa
}, },
}); });
const onSubmit = async (values) => { const onSubmit = async (values, code = null) => {
setLoading(true);
setError(''); setError('');
setDisabled(true);
const username = values.username.trim(); const username = values.username.trim();
const password = values.password.trim(); const password = values.password.trim();
@@ -65,20 +70,31 @@ export default function Login({ title, user_registration, oauth_registration, oa
} else if (res.totp) { } else if (res.totp) {
if (res.code === 400) { if (res.code === 400) {
setError('Invalid code'); setError('Invalid code');
setDisabled(false);
setLoading(false);
} else { } else {
setError(''); setError('');
setDisabled(false);
setLoading(false);
} }
setTotpOpen(true); setTotpOpen(true);
} else { } else {
form.setFieldError('username', 'Invalid username'); form.setFieldError('username', 'Invalid username');
form.setFieldError('password', 'Invalid password'); form.setFieldError('password', 'Invalid password');
setLoading(false);
} }
} else { } else {
await router.push((router.query.url as string) || '/dashboard'); await router.push((router.query.url as string) || '/dashboard');
} }
}; };
const handlePinChange = (value) => {
if (value.length === 6) {
onSubmit(form.values, value);
}
};
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const a = await fetch('/api/user'); const a = await fetch('/api/user');
@@ -98,24 +114,38 @@ export default function Login({ title, user_registration, oauth_registration, oa
title={<Title order={3}>Two-Factor Authentication Required</Title>} title={<Title order={3}>Two-Factor Authentication Required</Title>}
size='lg' size='lg'
> >
<form onSubmit={form.onSubmit(() => onSubmit(form.values))}> <Center my='md'>
<NumberInput <PinInput
placeholder='2FA Code'
label='Verify'
size='xl'
hideControls
maxLength={6}
minLength={6}
value={code}
onChange={(e) => setCode(e)}
data-autofocus data-autofocus
error={error} length={6}
oneTimeCode
type='number'
placeholder=''
onChange={handlePinChange}
autoFocus={true}
error={!!error}
disabled={disabled}
size='xl'
/> />
</Center>
<Button disabled={disabled} size='lg' fullWidth mt='md' rightIcon={<CheckIcon />} type='submit'> {error && (
Verify &amp; Login <Text my='sm' size='sm' color='red' align='center'>
</Button> {error}
</form> </Text>
)}
<Button
loading={loading}
disabled={disabled}
size='lg'
fullWidth
mt='md'
rightIcon={<CheckIcon />}
type='submit'
>
Verify &amp; Login
</Button>
</Modal> </Modal>
<Center sx={{ height: '100vh' }}> <Center sx={{ height: '100vh' }}>
<div> <div>
@@ -133,7 +163,7 @@ export default function Login({ title, user_registration, oauth_registration, oa
{...form.getInputProps('password')} {...form.getInputProps('password')}
/> />
<Button size='lg' my='sm' fullWidth type='submit'> <Button size='lg' my='sm' fullWidth type='submit' loading={loading}>
Login Login
</Button> </Button>
</form> </form>