From 9e714d607e70fd420378f27d818b39c151fbd5b3 Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Thu, 29 Aug 2024 01:00:57 +0000 Subject: [PATCH] feat: disable 2fa with backup codes (#1314) Allow disabling two-factor authentication (2FA) by using either their authenticator app (TOTP) or a backup code. --- .../2fa/disable-authenticator-app-dialog.tsx | 114 +++++++++++++----- packages/lib/server-only/2fa/disable-2fa.ts | 20 ++- packages/lib/translations/de/web.po | 20 +-- packages/lib/translations/en/web.po | 20 +-- .../router.ts | 3 +- .../schema.ts | 3 +- 6 files changed, 122 insertions(+), 58 deletions(-) diff --git a/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx index eef551aa9..5078a87a0 100644 --- a/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx +++ b/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx @@ -15,7 +15,6 @@ import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, - DialogClose, DialogContent, DialogDescription, DialogFooter, @@ -28,13 +27,16 @@ import { FormControl, FormField, FormItem, + FormLabel, FormMessage, } from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; import { PinInput, PinInputGroup, PinInputSlot } from '@documenso/ui/primitives/pin-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; export const ZDisable2FAForm = z.object({ - token: z.string(), + totpCode: z.string().trim().optional(), + backupCode: z.string().trim().optional(), }); export type TDisable2FAForm = z.infer; @@ -46,21 +48,43 @@ export const DisableAuthenticatorAppDialog = () => { const { toast } = useToast(); const [isOpen, setIsOpen] = useState(false); + const [twoFactorDisableMethod, setTwoFactorDisableMethod] = useState<'totp' | 'backup'>('totp'); const { mutateAsync: disable2FA } = trpc.twoFactorAuthentication.disable.useMutation(); const disable2FAForm = useForm({ defaultValues: { - token: '', + totpCode: '', + backupCode: '', }, resolver: zodResolver(ZDisable2FAForm), }); + const onCloseTwoFactorDisableDialog = () => { + disable2FAForm.reset(); + + setIsOpen(!isOpen); + }; + + const onToggleTwoFactorDisableMethodClick = () => { + const method = twoFactorDisableMethod === 'totp' ? 'backup' : 'totp'; + + if (method === 'totp') { + disable2FAForm.setValue('backupCode', ''); + } + + if (method === 'backup') { + disable2FAForm.setValue('totpCode', ''); + } + + setTwoFactorDisableMethod(method); + }; + const { isSubmitting: isDisable2FASubmitting } = disable2FAForm.formState; - const onDisable2FAFormSubmit = async ({ token }: TDisable2FAForm) => { + const onDisable2FAFormSubmit = async ({ totpCode, backupCode }: TDisable2FAForm) => { try { - await disable2FA({ token }); + await disable2FA({ totpCode, backupCode }); toast({ title: _(msg`Two-factor authentication disabled`), @@ -70,7 +94,7 @@ export const DisableAuthenticatorAppDialog = () => { }); flushSync(() => { - setIsOpen(false); + onCloseTwoFactorDisableDialog(); }); router.refresh(); @@ -86,7 +110,7 @@ export const DisableAuthenticatorAppDialog = () => { }; return ( - + - +