mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 15:50:11 -08:00
feat: use pininput for 2fa
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 & 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 & 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user