mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: remove 2FA password requirement (#1053)
This commit is contained in:
@ -1,14 +1,14 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { IDENTITY_PROVIDER_NAME } from '@documenso/lib/constants/auth';
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||||
import { AuthenticatorApp } from '~/components/forms/2fa/authenticator-app';
|
import { DisableAuthenticatorAppDialog } from '~/components/forms/2fa/disable-authenticator-app-dialog';
|
||||||
import { RecoveryCodes } from '~/components/forms/2fa/recovery-codes';
|
import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog';
|
||||||
|
import { ViewRecoveryCodesDialog } from '~/components/forms/2fa/view-recovery-codes-dialog';
|
||||||
import { PasswordForm } from '~/components/forms/password';
|
import { PasswordForm } from '~/components/forms/password';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@ -25,57 +25,51 @@ export default async function SecuritySettingsPage() {
|
|||||||
subtitle="Here you can manage your password and security settings."
|
subtitle="Here you can manage your password and security settings."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{user.identityProvider === 'DOCUMENSO' ? (
|
{user.identityProvider === 'DOCUMENSO' && (
|
||||||
<div>
|
<>
|
||||||
<PasswordForm user={user} />
|
<PasswordForm user={user} />
|
||||||
|
|
||||||
<hr className="border-border/50 mt-6" />
|
<hr className="border-border/50 mt-6" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
className="mt-6 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
|
className="mt-6 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
|
||||||
variant="neutral"
|
variant="neutral"
|
||||||
>
|
>
|
||||||
<div className="mb-4 sm:mb-0">
|
<div className="mb-4 sm:mb-0">
|
||||||
<AlertTitle>Two factor authentication</AlertTitle>
|
<AlertTitle>Two factor authentication</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription className="mr-4">
|
<AlertDescription className="mr-4">
|
||||||
Create one-time passwords that serve as a secondary authentication method for
|
Add an authenticator to serve as a secondary authentication method{' '}
|
||||||
confirming your identity when requested during the sign-in process.
|
{user.identityProvider === 'DOCUMENSO'
|
||||||
</AlertDescription>
|
? 'when signing in, or when signing documents.'
|
||||||
</div>
|
: 'for signing documents.'}
|
||||||
|
|
||||||
<AuthenticatorApp isTwoFactorEnabled={user.twoFactorEnabled} />
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{user.twoFactorEnabled && (
|
|
||||||
<Alert
|
|
||||||
className="mt-6 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
|
|
||||||
variant="neutral"
|
|
||||||
>
|
|
||||||
<div className="mb-4 sm:mb-0">
|
|
||||||
<AlertTitle>Recovery codes</AlertTitle>
|
|
||||||
|
|
||||||
<AlertDescription className="mr-4">
|
|
||||||
Two factor authentication recovery codes are used to access your account in the
|
|
||||||
event that you lose access to your authenticator app.
|
|
||||||
</AlertDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RecoveryCodes isTwoFactorEnabled={user.twoFactorEnabled} />
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Alert className="p-6" variant="neutral">
|
|
||||||
<AlertTitle>
|
|
||||||
Your account is managed by {IDENTITY_PROVIDER_NAME[user.identityProvider]}
|
|
||||||
</AlertTitle>
|
|
||||||
|
|
||||||
<AlertDescription>
|
|
||||||
To update your password, enable two-factor authentication, and manage other security
|
|
||||||
settings, please go to your {IDENTITY_PROVIDER_NAME[user.identityProvider]} account
|
|
||||||
settings.
|
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user.twoFactorEnabled ? (
|
||||||
|
<DisableAuthenticatorAppDialog />
|
||||||
|
) : (
|
||||||
|
<EnableAuthenticatorAppDialog />
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{user.twoFactorEnabled && (
|
||||||
|
<Alert
|
||||||
|
className="mt-6 flex flex-col justify-between p-6 sm:flex-row sm:items-center"
|
||||||
|
variant="neutral"
|
||||||
|
>
|
||||||
|
<div className="mb-4 sm:mb-0">
|
||||||
|
<AlertTitle>Recovery codes</AlertTitle>
|
||||||
|
|
||||||
|
<AlertDescription className="mr-4">
|
||||||
|
Two factor authentication recovery codes are used to access your account in the event
|
||||||
|
that you lose access to your authenticator app.
|
||||||
|
</AlertDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ViewRecoveryCodesDialog />
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,43 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import { DisableAuthenticatorAppDialog } from './disable-authenticator-app-dialog';
|
|
||||||
import { EnableAuthenticatorAppDialog } from './enable-authenticator-app-dialog';
|
|
||||||
|
|
||||||
type AuthenticatorAppProps = {
|
|
||||||
isTwoFactorEnabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AuthenticatorApp = ({ isTwoFactorEnabled }: AuthenticatorAppProps) => {
|
|
||||||
const [modalState, setModalState] = useState<'enable' | 'disable' | null>(null);
|
|
||||||
|
|
||||||
const isEnableDialogOpen = modalState === 'enable';
|
|
||||||
const isDisableDialogOpen = modalState === 'disable';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
{isTwoFactorEnabled ? (
|
|
||||||
<Button variant="destructive" onClick={() => setModalState('disable')}>
|
|
||||||
Disable 2FA
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button onClick={() => setModalState('enable')}>Enable 2FA</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EnableAuthenticatorAppDialog
|
|
||||||
open={isEnableDialogOpen}
|
|
||||||
onOpenChange={(open) => !open && setModalState(null)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DisableAuthenticatorAppDialog
|
|
||||||
open={isDisableDialogOpen}
|
|
||||||
onOpenChange={(open) => !open && setModalState(null)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,3 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@ -9,65 +13,51 @@ import { trpc } from '@documenso/trpc/react';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export const ZDisableTwoFactorAuthenticationForm = z.object({
|
export const ZDisable2FAForm = z.object({
|
||||||
password: z.string().min(6).max(72),
|
token: z.string(),
|
||||||
backupCode: z.string(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDisableTwoFactorAuthenticationForm = z.infer<
|
export type TDisable2FAForm = z.infer<typeof ZDisable2FAForm>;
|
||||||
typeof ZDisableTwoFactorAuthenticationForm
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type DisableAuthenticatorAppDialogProps = {
|
export const DisableAuthenticatorAppDialog = () => {
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (_open: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DisableAuthenticatorAppDialog = ({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: DisableAuthenticatorAppDialogProps) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { mutateAsync: disableTwoFactorAuthentication } =
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
trpc.twoFactorAuthentication.disable.useMutation();
|
|
||||||
|
|
||||||
const disableTwoFactorAuthenticationForm = useForm<TDisableTwoFactorAuthenticationForm>({
|
const { mutateAsync: disable2FA } = trpc.twoFactorAuthentication.disable.useMutation();
|
||||||
|
|
||||||
|
const disable2FAForm = useForm<TDisable2FAForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
password: '',
|
token: '',
|
||||||
backupCode: '',
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZDisableTwoFactorAuthenticationForm),
|
resolver: zodResolver(ZDisable2FAForm),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isSubmitting: isDisableTwoFactorAuthenticationSubmitting } =
|
const { isSubmitting: isDisable2FASubmitting } = disable2FAForm.formState;
|
||||||
disableTwoFactorAuthenticationForm.formState;
|
|
||||||
|
|
||||||
const onDisableTwoFactorAuthenticationFormSubmit = async ({
|
const onDisable2FAFormSubmit = async ({ token }: TDisable2FAForm) => {
|
||||||
password,
|
|
||||||
backupCode,
|
|
||||||
}: TDisableTwoFactorAuthenticationForm) => {
|
|
||||||
try {
|
try {
|
||||||
await disableTwoFactorAuthentication({ password, backupCode });
|
await disable2FA({ token });
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Two-factor authentication disabled',
|
title: 'Two-factor authentication disabled',
|
||||||
@ -76,7 +66,7 @@ export const DisableAuthenticatorAppDialog = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
onOpenChange(false);
|
setIsOpen(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
@ -91,74 +81,51 @@ export const DisableAuthenticatorAppDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
|
<DialogTrigger asChild={true}>
|
||||||
|
<Button className="flex-shrink-0" variant="destructive">
|
||||||
|
Disable 2FA
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent position="center">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Disable Authenticator App</DialogTitle>
|
<DialogTitle>Disable 2FA</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
To disable the Authenticator App for your account, please enter your password and a
|
Please provide a token from the authenticator, or a backup code. If you do not have a
|
||||||
backup code. If you do not have a backup code available, please contact support.
|
backup code available, please contact support.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Form {...disableTwoFactorAuthenticationForm}>
|
<Form {...disable2FAForm}>
|
||||||
<form
|
<form onSubmit={disable2FAForm.handleSubmit(onDisable2FAFormSubmit)}>
|
||||||
onSubmit={disableTwoFactorAuthenticationForm.handleSubmit(
|
<fieldset className="flex flex-col gap-y-4" disabled={isDisable2FASubmitting}>
|
||||||
onDisableTwoFactorAuthenticationFormSubmit,
|
|
||||||
)}
|
|
||||||
className="flex flex-col gap-y-4"
|
|
||||||
>
|
|
||||||
<fieldset
|
|
||||||
className="flex flex-col gap-y-4"
|
|
||||||
disabled={isDisableTwoFactorAuthenticationSubmitting}
|
|
||||||
>
|
|
||||||
<FormField
|
<FormField
|
||||||
name="password"
|
name="token"
|
||||||
control={disableTwoFactorAuthenticationForm.control}
|
control={disable2FAForm.control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-muted-foreground">Password</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<PasswordInput
|
<Input {...field} placeholder="Token" />
|
||||||
{...field}
|
|
||||||
autoComplete="current-password"
|
|
||||||
value={field.value ?? ''}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<DialogFooter>
|
||||||
name="backupCode"
|
<DialogClose asChild>
|
||||||
control={disableTwoFactorAuthenticationForm.control}
|
<Button type="button" variant="secondary">
|
||||||
render={({ field }) => (
|
Cancel
|
||||||
<FormItem>
|
</Button>
|
||||||
<FormLabel className="text-muted-foreground">Backup Code</FormLabel>
|
</DialogClose>
|
||||||
<FormControl>
|
|
||||||
<Input {...field} type="text" value={field.value ?? ''} />
|
<Button type="submit" variant="destructive" loading={isDisable2FASubmitting}>
|
||||||
</FormControl>
|
Disable 2FA
|
||||||
<FormMessage />
|
</Button>
|
||||||
</FormItem>
|
</DialogFooter>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="destructive"
|
|
||||||
loading={isDisableTwoFactorAuthenticationSubmitting}
|
|
||||||
>
|
|
||||||
Disable 2FA
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
import { renderSVG } from 'uqr';
|
import { renderSVG } from 'uqr';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@ -11,11 +14,13 @@ import { trpc } from '@documenso/trpc/react';
|
|||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -26,85 +31,60 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
import { RecoveryCodeList } from './recovery-code-list';
|
import { RecoveryCodeList } from './recovery-code-list';
|
||||||
|
|
||||||
export const ZSetupTwoFactorAuthenticationForm = z.object({
|
export const ZEnable2FAForm = z.object({
|
||||||
password: z.string().min(6).max(72),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TSetupTwoFactorAuthenticationForm = z.infer<typeof ZSetupTwoFactorAuthenticationForm>;
|
|
||||||
|
|
||||||
export const ZEnableTwoFactorAuthenticationForm = z.object({
|
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TEnableTwoFactorAuthenticationForm = z.infer<typeof ZEnableTwoFactorAuthenticationForm>;
|
export type TEnable2FAForm = z.infer<typeof ZEnable2FAForm>;
|
||||||
|
|
||||||
export type EnableAuthenticatorAppDialogProps = {
|
export const EnableAuthenticatorAppDialog = () => {
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (_open: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EnableAuthenticatorAppDialog = ({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: EnableAuthenticatorAppDialogProps) => {
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { mutateAsync: setupTwoFactorAuthentication, data: setupTwoFactorAuthenticationData } =
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
trpc.twoFactorAuthentication.setup.useMutation();
|
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
|
||||||
|
|
||||||
|
const { mutateAsync: enable2FA } = trpc.twoFactorAuthentication.enable.useMutation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: enableTwoFactorAuthentication,
|
mutateAsync: setup2FA,
|
||||||
data: enableTwoFactorAuthenticationData,
|
data: setup2FAData,
|
||||||
isLoading: isEnableTwoFactorAuthenticationDataLoading,
|
isLoading: isSettingUp2FA,
|
||||||
} = trpc.twoFactorAuthentication.enable.useMutation();
|
} = trpc.twoFactorAuthentication.setup.useMutation({
|
||||||
|
onError: () => {
|
||||||
const setupTwoFactorAuthenticationForm = useForm<TSetupTwoFactorAuthenticationForm>({
|
toast({
|
||||||
defaultValues: {
|
title: 'Unable to setup two-factor authentication',
|
||||||
password: '',
|
description:
|
||||||
|
'We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZSetupTwoFactorAuthenticationForm),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isSubmitting: isSetupTwoFactorAuthenticationSubmitting } =
|
const enable2FAForm = useForm<TEnable2FAForm>({
|
||||||
setupTwoFactorAuthenticationForm.formState;
|
|
||||||
|
|
||||||
const enableTwoFactorAuthenticationForm = useForm<TEnableTwoFactorAuthenticationForm>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
token: '',
|
token: '',
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZEnableTwoFactorAuthenticationForm),
|
resolver: zodResolver(ZEnable2FAForm),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isSubmitting: isEnableTwoFactorAuthenticationSubmitting } =
|
const { isSubmitting: isEnabling2FA } = enable2FAForm.formState;
|
||||||
enableTwoFactorAuthenticationForm.formState;
|
|
||||||
|
|
||||||
const step = useMemo(() => {
|
const onEnable2FAFormSubmit = async ({ token }: TEnable2FAForm) => {
|
||||||
if (!setupTwoFactorAuthenticationData || isSetupTwoFactorAuthenticationSubmitting) {
|
|
||||||
return 'setup';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableTwoFactorAuthenticationData || isEnableTwoFactorAuthenticationSubmitting) {
|
|
||||||
return 'enable';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'view';
|
|
||||||
}, [
|
|
||||||
setupTwoFactorAuthenticationData,
|
|
||||||
isSetupTwoFactorAuthenticationSubmitting,
|
|
||||||
enableTwoFactorAuthenticationData,
|
|
||||||
isEnableTwoFactorAuthenticationSubmitting,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onSetupTwoFactorAuthenticationFormSubmit = async ({
|
|
||||||
password,
|
|
||||||
}: TSetupTwoFactorAuthenticationForm) => {
|
|
||||||
try {
|
try {
|
||||||
await setupTwoFactorAuthentication({ password });
|
const data = await enable2FA({ code: token });
|
||||||
|
|
||||||
|
setRecoveryCodes(data.recoveryCodes);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Two-factor authentication enabled',
|
||||||
|
description:
|
||||||
|
'You will now be required to enter a code from your authenticator app when signing in.',
|
||||||
|
});
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Unable to setup two-factor authentication',
|
title: 'Unable to setup two-factor authentication',
|
||||||
@ -116,8 +96,8 @@ export const EnableAuthenticatorAppDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const downloadRecoveryCodes = () => {
|
const downloadRecoveryCodes = () => {
|
||||||
if (enableTwoFactorAuthenticationData && enableTwoFactorAuthenticationData.recoveryCodes) {
|
if (recoveryCodes) {
|
||||||
const blob = new Blob([enableTwoFactorAuthenticationData.recoveryCodes.join('\n')], {
|
const blob = new Blob([recoveryCodes.join('\n')], {
|
||||||
type: 'text/plain',
|
type: 'text/plain',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -128,175 +108,126 @@ export const EnableAuthenticatorAppDialog = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEnableTwoFactorAuthenticationFormSubmit = async ({
|
const handleEnable2FA = async () => {
|
||||||
token,
|
if (!setup2FAData) {
|
||||||
}: TEnableTwoFactorAuthenticationForm) => {
|
await setup2FA();
|
||||||
try {
|
|
||||||
await enableTwoFactorAuthentication({ code: token });
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Two-factor authentication enabled',
|
|
||||||
description:
|
|
||||||
'Two-factor authentication has been enabled for your account. You will now be required to enter a code from your authenticator app when signing in.',
|
|
||||||
});
|
|
||||||
} catch (_err) {
|
|
||||||
toast({
|
|
||||||
title: 'Unable to setup two-factor authentication',
|
|
||||||
description:
|
|
||||||
'We were unable to setup two-factor authentication for your account. Please ensure that you have entered your password correctly and try again.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset the form when the Dialog closes
|
enable2FAForm.reset();
|
||||||
if (!open) {
|
|
||||||
setupTwoFactorAuthenticationForm.reset();
|
if (!isOpen && recoveryCodes && recoveryCodes.length > 0) {
|
||||||
|
setRecoveryCodes(null);
|
||||||
|
router.refresh();
|
||||||
}
|
}
|
||||||
}, [open, setupTwoFactorAuthenticationForm]);
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
|
<DialogTrigger asChild={true}>
|
||||||
<DialogHeader>
|
<Button
|
||||||
<DialogTitle>Enable Authenticator App</DialogTitle>
|
className="flex-shrink-0"
|
||||||
|
loading={isSettingUp2FA}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleEnable2FA();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Enable 2FA
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
{step === 'setup' && (
|
<DialogContent position="center">
|
||||||
<DialogDescription>
|
{setup2FAData && (
|
||||||
To enable two-factor authentication, please enter your password below.
|
<>
|
||||||
</DialogDescription>
|
{recoveryCodes ? (
|
||||||
)}
|
<div>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Backup codes</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Your recovery codes are listed below. Please store them in a safe place.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
{step === 'view' && (
|
<div className="mt-4">
|
||||||
<DialogDescription>
|
<RecoveryCodeList recoveryCodes={recoveryCodes} />
|
||||||
Your recovery codes are listed below. Please store them in a safe place.
|
</div>
|
||||||
</DialogDescription>
|
|
||||||
)}
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{match(step)
|
<DialogFooter className="mt-4">
|
||||||
.with('setup', () => {
|
<DialogClose asChild>
|
||||||
return (
|
<Button variant="secondary">Close</Button>
|
||||||
<Form {...setupTwoFactorAuthenticationForm}>
|
</DialogClose>
|
||||||
<form
|
|
||||||
onSubmit={setupTwoFactorAuthenticationForm.handleSubmit(
|
|
||||||
onSetupTwoFactorAuthenticationFormSubmit,
|
|
||||||
)}
|
|
||||||
className="flex flex-col gap-y-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
name="password"
|
|
||||||
control={setupTwoFactorAuthenticationForm.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-muted-foreground">Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<PasswordInput
|
|
||||||
{...field}
|
|
||||||
autoComplete="current-password"
|
|
||||||
value={field.value ?? ''}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogFooter>
|
<Button onClick={downloadRecoveryCodes}>Download</Button>
|
||||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
</DialogFooter>
|
||||||
Cancel
|
</div>
|
||||||
</Button>
|
) : (
|
||||||
|
<Form {...enable2FAForm}>
|
||||||
|
<form onSubmit={enable2FAForm.handleSubmit(onEnable2FAFormSubmit)}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Enable Authenticator App</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
To enable two-factor authentication, scan the following QR code using your
|
||||||
|
authenticator app.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
<Button type="submit" loading={isSetupTwoFactorAuthenticationSubmitting}>
|
<fieldset disabled={isEnabling2FA} className="mt-4 flex flex-col gap-y-4">
|
||||||
Continue
|
<div
|
||||||
</Button>
|
className="flex h-36 justify-center"
|
||||||
</DialogFooter>
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: renderSVG(setup2FAData?.uri ?? ''),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
If your authenticator app does not support QR codes, you can use the following
|
||||||
|
code instead:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="bg-muted/60 text-muted-foreground rounded-lg p-2 text-center font-mono tracking-widest">
|
||||||
|
{setup2FAData?.secret}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Once you have scanned the QR code or entered the code manually, enter the code
|
||||||
|
provided by your authenticator app below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name="token"
|
||||||
|
control={enable2FAForm.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-muted-foreground">Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} type="text" value={field.value ?? ''} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary">Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
<Button type="submit" loading={isEnabling2FA}>
|
||||||
|
Enable 2FA
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
)}
|
||||||
})
|
</>
|
||||||
.with('enable', () => (
|
)}
|
||||||
<Form {...enableTwoFactorAuthenticationForm}>
|
|
||||||
<form
|
|
||||||
onSubmit={enableTwoFactorAuthenticationForm.handleSubmit(
|
|
||||||
onEnableTwoFactorAuthenticationFormSubmit,
|
|
||||||
)}
|
|
||||||
className="flex flex-col gap-y-4"
|
|
||||||
>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
To enable two-factor authentication, scan the following QR code using your
|
|
||||||
authenticator app.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex h-36 justify-center"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: renderSVG(setupTwoFactorAuthenticationData?.uri ?? ''),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
If your authenticator app does not support QR codes, you can use the following
|
|
||||||
code instead:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="bg-muted/60 text-muted-foreground rounded-lg p-2 text-center font-mono tracking-widest">
|
|
||||||
{setupTwoFactorAuthenticationData?.secret}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Once you have scanned the QR code or entered the code manually, enter the code
|
|
||||||
provided by your authenticator app below.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
name="token"
|
|
||||||
control={enableTwoFactorAuthenticationForm.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-muted-foreground">Token</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} type="text" value={field.value ?? ''} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button type="submit" loading={isEnableTwoFactorAuthenticationSubmitting}>
|
|
||||||
Enable 2FA
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
))
|
|
||||||
.with('view', () => (
|
|
||||||
<div>
|
|
||||||
{enableTwoFactorAuthenticationData?.recoveryCodes && (
|
|
||||||
<RecoveryCodeList recoveryCodes={enableTwoFactorAuthenticationData.recoveryCodes} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4 flex flex-row-reverse items-center gap-2">
|
|
||||||
<Button onClick={() => onOpenChange(false)}>Complete</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={downloadRecoveryCodes}
|
|
||||||
disabled={!enableTwoFactorAuthenticationData?.recoveryCodes}
|
|
||||||
loading={isEnableTwoFactorAuthenticationDataLoading}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
.exhaustive()}
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
|
|
||||||
import { ViewRecoveryCodesDialog } from './view-recovery-codes-dialog';
|
|
||||||
|
|
||||||
type RecoveryCodesProps = {
|
|
||||||
isTwoFactorEnabled: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RecoveryCodes = ({ isTwoFactorEnabled }: RecoveryCodesProps) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
className="flex-shrink-0"
|
|
||||||
onClick={() => setIsOpen(true)}
|
|
||||||
disabled={!isTwoFactorEnabled}
|
|
||||||
>
|
|
||||||
View Codes
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<ViewRecoveryCodesDialog
|
|
||||||
key={isOpen ? 'open' : 'closed'}
|
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={setIsOpen}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,4 +1,6 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@ -6,69 +8,61 @@ import { match } from 'ts-pattern';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { PasswordInput } from '@documenso/ui/primitives/password-input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { RecoveryCodeList } from './recovery-code-list';
|
import { RecoveryCodeList } from './recovery-code-list';
|
||||||
|
|
||||||
export const ZViewRecoveryCodesForm = z.object({
|
export const ZViewRecoveryCodesForm = z.object({
|
||||||
password: z.string().min(6).max(72),
|
token: z.string().min(1, { message: 'Token is required' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TViewRecoveryCodesForm = z.infer<typeof ZViewRecoveryCodesForm>;
|
export type TViewRecoveryCodesForm = z.infer<typeof ZViewRecoveryCodesForm>;
|
||||||
|
|
||||||
export type ViewRecoveryCodesDialogProps = {
|
export const ViewRecoveryCodesDialog = () => {
|
||||||
open: boolean;
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
onOpenChange: (_open: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCodesDialogProps) => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: viewRecoveryCodes,
|
data: recoveryCodes,
|
||||||
data: viewRecoveryCodesData,
|
mutate,
|
||||||
isLoading: isViewRecoveryCodesDataLoading,
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
} = trpc.twoFactorAuthentication.viewRecoveryCodes.useMutation();
|
||||||
|
|
||||||
|
// error?.data?.code
|
||||||
|
|
||||||
const viewRecoveryCodesForm = useForm<TViewRecoveryCodesForm>({
|
const viewRecoveryCodesForm = useForm<TViewRecoveryCodesForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
password: '',
|
token: '',
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZViewRecoveryCodesForm),
|
resolver: zodResolver(ZViewRecoveryCodesForm),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isSubmitting: isViewRecoveryCodesSubmitting } = viewRecoveryCodesForm.formState;
|
|
||||||
|
|
||||||
const step = useMemo(() => {
|
|
||||||
if (!viewRecoveryCodesData || isViewRecoveryCodesSubmitting) {
|
|
||||||
return 'authenticate';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'view';
|
|
||||||
}, [viewRecoveryCodesData, isViewRecoveryCodesSubmitting]);
|
|
||||||
|
|
||||||
const downloadRecoveryCodes = () => {
|
const downloadRecoveryCodes = () => {
|
||||||
if (viewRecoveryCodesData && viewRecoveryCodesData.recoveryCodes) {
|
if (recoveryCodes) {
|
||||||
const blob = new Blob([viewRecoveryCodesData.recoveryCodes.join('\n')], {
|
const blob = new Blob([recoveryCodes.join('\n')], {
|
||||||
type: 'text/plain',
|
type: 'text/plain',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,105 +73,88 @@ export const ViewRecoveryCodesDialog = ({ open, onOpenChange }: ViewRecoveryCode
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onViewRecoveryCodesFormSubmit = async ({ password }: TViewRecoveryCodesForm) => {
|
|
||||||
try {
|
|
||||||
await viewRecoveryCodes({ password });
|
|
||||||
} catch (_err) {
|
|
||||||
toast({
|
|
||||||
title: 'Unable to view recovery codes',
|
|
||||||
description:
|
|
||||||
'We were unable to view your recovery codes. Please ensure that you have entered your password correctly and try again.',
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset the form when the Dialog closes
|
|
||||||
if (!open) {
|
|
||||||
viewRecoveryCodesForm.reset();
|
|
||||||
}
|
|
||||||
}, [open, viewRecoveryCodesForm]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="flex-shrink-0">View Codes</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
|
<DialogContent className="w-full max-w-xl md:max-w-xl lg:max-w-xl">
|
||||||
<DialogHeader>
|
{recoveryCodes ? (
|
||||||
<DialogTitle>View Recovery Codes</DialogTitle>
|
<div>
|
||||||
|
<DialogHeader className="mb-4">
|
||||||
|
<DialogTitle>View Recovery Codes</DialogTitle>
|
||||||
|
|
||||||
{step === 'authenticate' && (
|
<DialogDescription>
|
||||||
<DialogDescription>
|
Your recovery codes are listed below. Please store them in a safe place.
|
||||||
To view your recovery codes, please enter your password below.
|
</DialogDescription>
|
||||||
</DialogDescription>
|
</DialogHeader>
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'view' && (
|
<RecoveryCodeList recoveryCodes={recoveryCodes} />
|
||||||
<DialogDescription>
|
|
||||||
Your recovery codes are listed below. Please store them in a safe place.
|
|
||||||
</DialogDescription>
|
|
||||||
)}
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{match(step)
|
<DialogFooter className="mt-4">
|
||||||
.with('authenticate', () => {
|
<DialogClose asChild>
|
||||||
return (
|
<Button variant="secondary">Close</Button>
|
||||||
<Form {...viewRecoveryCodesForm}>
|
</DialogClose>
|
||||||
<form
|
|
||||||
onSubmit={viewRecoveryCodesForm.handleSubmit(onViewRecoveryCodesFormSubmit)}
|
|
||||||
className="flex flex-col gap-y-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
name="password"
|
|
||||||
control={viewRecoveryCodesForm.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="text-muted-foreground">Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<PasswordInput
|
|
||||||
{...field}
|
|
||||||
autoComplete="current-password"
|
|
||||||
value={field.value ?? ''}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogFooter>
|
<Button onClick={downloadRecoveryCodes}>Download</Button>
|
||||||
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Form {...viewRecoveryCodesForm}>
|
||||||
|
<form onSubmit={viewRecoveryCodesForm.handleSubmit((value) => mutate(value))}>
|
||||||
|
<DialogHeader className="mb-4">
|
||||||
|
<DialogTitle>View Recovery Codes</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
Please provide a token from your authenticator, or a backup code.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<fieldset className="flex flex-col space-y-4" disabled={isLoading}>
|
||||||
|
<FormField
|
||||||
|
name="token"
|
||||||
|
control={viewRecoveryCodesForm.control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="Token" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription>
|
||||||
|
{match(AppError.parseError(error).message)
|
||||||
|
.with(
|
||||||
|
ErrorCode.INCORRECT_TWO_FACTOR_CODE,
|
||||||
|
() => 'Invalid code. Please try again.',
|
||||||
|
)
|
||||||
|
.otherwise(
|
||||||
|
() => 'Something went wrong. Please try again or contact support.',
|
||||||
|
)}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button type="button" variant="secondary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
<Button type="submit" loading={isViewRecoveryCodesSubmitting}>
|
<Button type="submit" loading={isLoading}>
|
||||||
Continue
|
View
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</fieldset>
|
||||||
</Form>
|
</form>
|
||||||
);
|
</Form>
|
||||||
})
|
)}
|
||||||
.with('view', () => (
|
|
||||||
<div>
|
|
||||||
{viewRecoveryCodesData?.recoveryCodes && (
|
|
||||||
<RecoveryCodeList recoveryCodes={viewRecoveryCodesData.recoveryCodes} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4 flex flex-row-reverse items-center gap-2">
|
|
||||||
<Button onClick={() => onOpenChange(false)}>Complete</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
disabled={!viewRecoveryCodesData?.recoveryCodes}
|
|
||||||
loading={isViewRecoveryCodesDataLoading}
|
|
||||||
onClick={downloadRecoveryCodes}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
.exhaustive()}
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,40 +1,30 @@
|
|||||||
import { compare } from '@node-rs/bcrypt';
|
|
||||||
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { ErrorCode } from '../../next-auth/error-codes';
|
import { AppError } from '../../errors/app-error';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { validateTwoFactorAuthentication } from './validate-2fa';
|
import { validateTwoFactorAuthentication } from './validate-2fa';
|
||||||
|
|
||||||
type DisableTwoFactorAuthenticationOptions = {
|
type DisableTwoFactorAuthenticationOptions = {
|
||||||
user: User;
|
user: User;
|
||||||
backupCode: string;
|
token: string;
|
||||||
password: string;
|
|
||||||
requestMetadata?: RequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const disableTwoFactorAuthentication = async ({
|
export const disableTwoFactorAuthentication = async ({
|
||||||
backupCode,
|
token,
|
||||||
user,
|
user,
|
||||||
password,
|
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: DisableTwoFactorAuthenticationOptions) => {
|
}: DisableTwoFactorAuthenticationOptions) => {
|
||||||
if (!user.password) {
|
let isValid = await validateTwoFactorAuthentication({ totpCode: token, user });
|
||||||
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCorrectPassword = await compare(password, user.password);
|
|
||||||
|
|
||||||
if (!isCorrectPassword) {
|
|
||||||
throw new Error(ErrorCode.INCORRECT_PASSWORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await validateTwoFactorAuthentication({ backupCode, user });
|
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw new Error(ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE);
|
isValid = await validateTwoFactorAuthentication({ backupCode: token, user });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new AppError('INCORRECT_TWO_FACTOR_CODE');
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { type User, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
import { type User, UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { AppError } from '../../errors/app-error';
|
||||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||||
import { getBackupCodes } from './get-backup-code';
|
import { getBackupCodes } from './get-backup-code';
|
||||||
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
||||||
@ -17,25 +17,38 @@ export const enableTwoFactorAuthentication = async ({
|
|||||||
code,
|
code,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: EnableTwoFactorAuthenticationOptions) => {
|
}: EnableTwoFactorAuthenticationOptions) => {
|
||||||
if (user.identityProvider !== 'DOCUMENSO') {
|
|
||||||
throw new Error(ErrorCode.INCORRECT_IDENTITY_PROVIDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.twoFactorEnabled) {
|
if (user.twoFactorEnabled) {
|
||||||
throw new Error(ErrorCode.TWO_FACTOR_ALREADY_ENABLED);
|
throw new AppError('TWO_FACTOR_ALREADY_ENABLED');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.twoFactorSecret) {
|
if (!user.twoFactorSecret) {
|
||||||
throw new Error(ErrorCode.TWO_FACTOR_SETUP_REQUIRED);
|
throw new AppError('TWO_FACTOR_SETUP_REQUIRED');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidToken = await verifyTwoFactorAuthenticationToken({ user, totpCode: code });
|
const isValidToken = await verifyTwoFactorAuthenticationToken({ user, totpCode: code });
|
||||||
|
|
||||||
if (!isValidToken) {
|
if (!isValidToken) {
|
||||||
throw new Error(ErrorCode.INCORRECT_TWO_FACTOR_CODE);
|
throw new AppError('INCORRECT_TWO_FACTOR_CODE');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await prisma.$transaction(async (tx) => {
|
let recoveryCodes: string[] = [];
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
const updatedUser = await tx.user.update({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
twoFactorEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
recoveryCodes = getBackupCodes({ user: updatedUser }) ?? [];
|
||||||
|
|
||||||
|
if (recoveryCodes.length === 0) {
|
||||||
|
throw new AppError('MISSING_BACKUP_CODE');
|
||||||
|
}
|
||||||
|
|
||||||
await tx.userSecurityAuditLog.create({
|
await tx.userSecurityAuditLog.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -44,18 +57,7 @@ export const enableTwoFactorAuthentication = async ({
|
|||||||
ipAddress: requestMetadata?.ipAddress,
|
ipAddress: requestMetadata?.ipAddress,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return await tx.user.update({
|
|
||||||
where: {
|
|
||||||
id: user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
twoFactorEnabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const recoveryCodes = getBackupCodes({ user: updatedUser });
|
|
||||||
|
|
||||||
return { recoveryCodes };
|
return { recoveryCodes };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { compare } from '@node-rs/bcrypt';
|
|
||||||
import { base32 } from '@scure/base';
|
import { base32 } from '@scure/base';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { createTOTPKeyURI } from 'oslo/otp';
|
import { createTOTPKeyURI } from 'oslo/otp';
|
||||||
@ -12,14 +11,12 @@ import { symmetricEncrypt } from '../../universal/crypto';
|
|||||||
|
|
||||||
type SetupTwoFactorAuthenticationOptions = {
|
type SetupTwoFactorAuthenticationOptions = {
|
||||||
user: User;
|
user: User;
|
||||||
password: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ISSUER = 'Documenso';
|
const ISSUER = 'Documenso';
|
||||||
|
|
||||||
export const setupTwoFactorAuthentication = async ({
|
export const setupTwoFactorAuthentication = async ({
|
||||||
user,
|
user,
|
||||||
password,
|
|
||||||
}: SetupTwoFactorAuthenticationOptions) => {
|
}: SetupTwoFactorAuthenticationOptions) => {
|
||||||
const key = DOCUMENSO_ENCRYPTION_KEY;
|
const key = DOCUMENSO_ENCRYPTION_KEY;
|
||||||
|
|
||||||
@ -27,20 +24,6 @@ export const setupTwoFactorAuthentication = async ({
|
|||||||
throw new Error(ErrorCode.MISSING_ENCRYPTION_KEY);
|
throw new Error(ErrorCode.MISSING_ENCRYPTION_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.identityProvider !== 'DOCUMENSO') {
|
|
||||||
throw new Error(ErrorCode.INCORRECT_IDENTITY_PROVIDER);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.password) {
|
|
||||||
throw new Error(ErrorCode.USER_MISSING_PASSWORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCorrectPassword = await compare(password, user.password);
|
|
||||||
|
|
||||||
if (!isCorrectPassword) {
|
|
||||||
throw new Error(ErrorCode.INCORRECT_PASSWORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
const secret = crypto.randomBytes(10);
|
const secret = crypto.randomBytes(10);
|
||||||
|
|
||||||
const backupCodes = Array.from({ length: 10 })
|
const backupCodes = Array.from({ length: 10 })
|
||||||
|
|||||||
30
packages/lib/server-only/2fa/view-backup-codes.ts
Normal file
30
packages/lib/server-only/2fa/view-backup-codes.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { AppError } from '../../errors/app-error';
|
||||||
|
import { getBackupCodes } from './get-backup-code';
|
||||||
|
import { validateTwoFactorAuthentication } from './validate-2fa';
|
||||||
|
|
||||||
|
type ViewBackupCodesOptions = {
|
||||||
|
user: User;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const viewBackupCodes = async ({ token, user }: ViewBackupCodesOptions) => {
|
||||||
|
let isValid = await validateTwoFactorAuthentication({ totpCode: token, user });
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
isValid = await validateTwoFactorAuthentication({ backupCode: token, user });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new AppError('INCORRECT_TWO_FACTOR_CODE');
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupCodes = getBackupCodes({ user });
|
||||||
|
|
||||||
|
if (!backupCodes) {
|
||||||
|
throw new AppError('MISSING_BACKUP_CODE');
|
||||||
|
}
|
||||||
|
|
||||||
|
return backupCodes;
|
||||||
|
};
|
||||||
@ -1,34 +1,34 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
|
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
|
||||||
import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa';
|
import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa';
|
||||||
import { getBackupCodes } from '@documenso/lib/server-only/2fa/get-backup-code';
|
|
||||||
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
|
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
|
||||||
import { compareSync } from '@documenso/lib/server-only/auth/hash';
|
import { viewBackupCodes } from '@documenso/lib/server-only/2fa/view-backup-codes';
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
|
||||||
import { authenticatedProcedure, router } from '../trpc';
|
import { authenticatedProcedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
ZDisableTwoFactorAuthenticationMutationSchema,
|
ZDisableTwoFactorAuthenticationMutationSchema,
|
||||||
ZEnableTwoFactorAuthenticationMutationSchema,
|
ZEnableTwoFactorAuthenticationMutationSchema,
|
||||||
ZSetupTwoFactorAuthenticationMutationSchema,
|
|
||||||
ZViewRecoveryCodesMutationSchema,
|
ZViewRecoveryCodesMutationSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
|
|
||||||
export const twoFactorAuthenticationRouter = router({
|
export const twoFactorAuthenticationRouter = router({
|
||||||
setup: authenticatedProcedure
|
setup: authenticatedProcedure.mutation(async ({ ctx }) => {
|
||||||
.input(ZSetupTwoFactorAuthenticationMutationSchema)
|
try {
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const user = ctx.user;
|
|
||||||
|
|
||||||
const { password } = input;
|
|
||||||
|
|
||||||
return await setupTwoFactorAuthentication({
|
return await setupTwoFactorAuthentication({
|
||||||
user,
|
user: ctx.user,
|
||||||
password,
|
|
||||||
});
|
});
|
||||||
}),
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'We were unable to setup two-factor authentication. Please try again later.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
enable: authenticatedProcedure
|
enable: authenticatedProcedure
|
||||||
.input(ZEnableTwoFactorAuthenticationMutationSchema)
|
.input(ZEnableTwoFactorAuthenticationMutationSchema)
|
||||||
@ -44,7 +44,11 @@ export const twoFactorAuthenticationRouter = router({
|
|||||||
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
@ -59,16 +63,17 @@ export const twoFactorAuthenticationRouter = router({
|
|||||||
try {
|
try {
|
||||||
const user = ctx.user;
|
const user = ctx.user;
|
||||||
|
|
||||||
const { password, backupCode } = input;
|
|
||||||
|
|
||||||
return await disableTwoFactorAuthentication({
|
return await disableTwoFactorAuthentication({
|
||||||
user,
|
user,
|
||||||
password,
|
token: input.token,
|
||||||
backupCode,
|
|
||||||
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
requestMetadata: extractNextApiRequestMetadata(ctx.req),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
code: 'BAD_REQUEST',
|
||||||
@ -81,38 +86,18 @@ export const twoFactorAuthenticationRouter = router({
|
|||||||
.input(ZViewRecoveryCodesMutationSchema)
|
.input(ZViewRecoveryCodesMutationSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
const user = ctx.user;
|
return await viewBackupCodes({
|
||||||
|
user: ctx.user,
|
||||||
const { password } = input;
|
token: input.token,
|
||||||
|
|
||||||
if (!user.twoFactorEnabled) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message: ErrorCode.TWO_FACTOR_SETUP_REQUIRED,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.password || !compareSync(password, user.password)) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'UNAUTHORIZED',
|
|
||||||
message: ErrorCode.INCORRECT_PASSWORD,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const recoveryCodes = await getBackupCodes({ user });
|
|
||||||
|
|
||||||
return { recoveryCodes };
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
|
|
||||||
if (err instanceof TRPCError) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'BAD_REQUEST',
|
|
||||||
message: 'We were unable to view your recovery codes. Please try again later.',
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw AppError.parseErrorToTRPCError(err);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,13 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const ZSetupTwoFactorAuthenticationMutationSchema = z.object({
|
|
||||||
password: z.string().min(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TSetupTwoFactorAuthenticationMutationSchema = z.infer<
|
|
||||||
typeof ZSetupTwoFactorAuthenticationMutationSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const ZEnableTwoFactorAuthenticationMutationSchema = z.object({
|
export const ZEnableTwoFactorAuthenticationMutationSchema = z.object({
|
||||||
code: z.string().min(6).max(6),
|
code: z.string().min(6).max(6),
|
||||||
});
|
});
|
||||||
@ -17,8 +9,7 @@ export type TEnableTwoFactorAuthenticationMutationSchema = z.infer<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export const ZDisableTwoFactorAuthenticationMutationSchema = z.object({
|
export const ZDisableTwoFactorAuthenticationMutationSchema = z.object({
|
||||||
password: z.string().min(6).max(72),
|
token: z.string().trim().min(1),
|
||||||
backupCode: z.string().trim(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDisableTwoFactorAuthenticationMutationSchema = z.infer<
|
export type TDisableTwoFactorAuthenticationMutationSchema = z.infer<
|
||||||
@ -26,7 +17,7 @@ export type TDisableTwoFactorAuthenticationMutationSchema = z.infer<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export const ZViewRecoveryCodesMutationSchema = z.object({
|
export const ZViewRecoveryCodesMutationSchema = z.object({
|
||||||
password: z.string().min(6).max(72),
|
token: z.string().trim().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TViewRecoveryCodesMutationSchema = z.infer<typeof ZViewRecoveryCodesMutationSchema>;
|
export type TViewRecoveryCodesMutationSchema = z.infer<typeof ZViewRecoveryCodesMutationSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user