1
Fork 0
mirror of https://github.com/diced/zipline.git synced 2025-04-04 23:21:17 -05:00

feat: use pininput for 2fa

This commit is contained in:
diced 2023-03-04 14:40:54 -08:00
parent df013a52d1
commit 2c24cafab8
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
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 { useForm } from '@mantine/form';
import { CheckIcon, CrossIcon } from 'components/icons';
@ -9,9 +9,7 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
const [secret, setSecret] = useState('');
const [qrCode, setQrCode] = useState('');
const [disabled, setDisabled] = useState(false);
const [code, setCode] = useState(undefined);
const [error, setError] = useState('');
const form = useForm();
useEffect(() => {
(async () => {
@ -34,15 +32,15 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
})();
}, [opened]);
const disableTotp = async () => {
const disableTotp = async (code) => {
setDisabled(true);
const str = code.toString();
if (str.length !== 6) {
if (code.length !== 6) {
setDisabled(false);
return setError('Code must be 6 digits');
}
const resp = await useFetch('/api/user/mfa/totp', 'DELETE', {
code: str,
code,
});
if (resp.error) {
@ -63,16 +61,16 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
setDisabled(false);
};
const verifyCode = async () => {
const verifyCode = async (code) => {
setDisabled(true);
const str = code.toString();
if (str.length !== 6) {
if (code.length !== 6) {
setDisabled(false);
return setError('Code must be 6 digits');
}
const resp = await useFetch('/api/user/mfa/totp', 'POST', {
secret,
code: str,
code,
register: true,
});
@ -94,6 +92,13 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
setDisabled(false);
};
const handlePinChange = (value) => {
if (value.length === 6) {
setDisabled(true);
deleteTotp ? disableTotp(value) : verifyCode(value);
}
};
return (
<Modal
opened={opened}
@ -112,39 +117,39 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
<Center>
<Image height={180} width={180} src={qrCode} alt='QR Code' withPlaceholder />
</Center>
<Text my='sm'>QR Code not working? Try manually entering the code into your app: {secret}</Text>
</>
)}
<form
onSubmit={form.onSubmit(() => {
deleteTotp ? disableTotp() : verifyCode();
})}
>
<NumberInput
placeholder='2FA Code'
label='Verify'
size='xl'
hideControls
maxLength={6}
minLength={6}
value={code}
onChange={(e) => setCode(e)}
<Center my='md'>
<PinInput
data-autofocus
error={error}
/>
<Button
length={6}
oneTimeCode
type='number'
placeholder=''
onChange={handlePinChange}
autoFocus={true}
error={!!error}
disabled={disabled}
size='lg'
fullWidth
mt='md'
rightIcon={<CheckIcon />}
onClick={deleteTotp ? disableTotp : verifyCode}
>
Verify{deleteTotp ? ' and Disable' : ''}
</Button>
</form>
size='xl'
/>
</Center>
{error && (
<Text my='sm' size='sm' color='red' align='center'>
{error}
</Text>
)}
{!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>
);
}

View file

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

View file

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