mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
feat: require 2fa code before account is deleted
This commit is contained in:
@ -7,6 +7,7 @@ import { signOut } from 'next-auth/react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { validateTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/validate-2fa';
|
||||||
import type { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
import { TRPCClientError } from '@documenso/trpc/client';
|
import { TRPCClientError } from '@documenso/trpc/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@ -41,6 +42,11 @@ export const ZProfileFormSchema = z.object({
|
|||||||
signature: z.string().min(1, 'Signature Pad cannot be empty'),
|
signature: z.string().min(1, 'Signature Pad cannot be empty'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZTwoFactorAuthTokenSchema = z.object({
|
||||||
|
token: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TTwoFactorAuthTokenSchema = z.infer<typeof ZTwoFactorAuthTokenSchema>;
|
||||||
export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;
|
export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;
|
||||||
|
|
||||||
export type ProfileFormProps = {
|
export type ProfileFormProps = {
|
||||||
@ -61,7 +67,15 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
resolver: zodResolver(ZProfileFormSchema),
|
resolver: zodResolver(ZProfileFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteAccountTwoFactorTokenForm = useForm<TTwoFactorAuthTokenSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
token: '',
|
||||||
|
},
|
||||||
|
resolver: zodResolver(ZTwoFactorAuthTokenSchema),
|
||||||
|
});
|
||||||
|
|
||||||
const isSubmitting = form.formState.isSubmitting;
|
const isSubmitting = form.formState.isSubmitting;
|
||||||
|
const hasTwoFactorAuthentication = user.twoFactorEnabled;
|
||||||
|
|
||||||
const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
|
const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
|
||||||
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
|
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
|
||||||
@ -101,9 +115,20 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
|
|
||||||
const onDeleteAccount = async () => {
|
const onDeleteAccount = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteAccount();
|
const { token } = deleteAccountTwoFactorTokenForm.getValues();
|
||||||
|
|
||||||
await signOut({ callbackUrl: '/' });
|
if (!token) {
|
||||||
|
throw new Error('Please enter your Two Factor Authentication token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await validateTwoFactorAuthentication({
|
||||||
|
totpCode: token,
|
||||||
|
user,
|
||||||
|
}).catch(() => {
|
||||||
|
throw new Error('We were unable to validate your Two Factor Authentication token.');
|
||||||
|
});
|
||||||
|
|
||||||
|
await deleteAccount();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Account deleted',
|
title: 'Account deleted',
|
||||||
@ -111,9 +136,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// logout after deleting account
|
await signOut({ callbackUrl: '/' });
|
||||||
|
|
||||||
router.push('/');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
|
||||||
toast({
|
toast({
|
||||||
@ -126,6 +149,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
title: 'An unknown error occurred',
|
title: 'An unknown error occurred',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
description:
|
description:
|
||||||
|
err.message ??
|
||||||
'We encountered an unknown error while attempting to delete your account. Please try again later.',
|
'We encountered an unknown error while attempting to delete your account. Please try again later.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -193,36 +217,73 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
irreversible and will cancel your subscription, so proceed with caution.
|
irreversible and will cancel your subscription, so proceed with caution.
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="justify-end pb-4 pr-4">
|
<CardFooter className="justify-end pb-4 pr-4">
|
||||||
<Dialog>
|
<Form {...deleteAccountTwoFactorTokenForm}>
|
||||||
<DialogTrigger asChild>
|
<form
|
||||||
<Button variant="destructive">Delete Account</Button>
|
onSubmit={deleteAccountTwoFactorTokenForm.handleSubmit(() => {
|
||||||
</DialogTrigger>
|
console.log('delete account');
|
||||||
<DialogContent>
|
})}
|
||||||
<DialogHeader>
|
>
|
||||||
<DialogTitle>Delete Account</DialogTitle>
|
<Dialog>
|
||||||
<DialogDescription>
|
<DialogTrigger asChild>
|
||||||
Documenso will delete{' '}
|
<Button
|
||||||
<span className="font-semibold">all of your documents</span>, along with all of
|
onClick={() => {
|
||||||
your completed documents, signatures, and all other resources belonging to your
|
console.log(user);
|
||||||
Account.
|
}}
|
||||||
<Alert variant="destructive" className="mt-5">
|
variant="destructive"
|
||||||
|
>
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Account</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Documenso will delete{' '}
|
||||||
|
<span className="font-semibold">all of your documents</span>, along with all
|
||||||
|
of your completed documents, signatures, and all other resources belonging
|
||||||
|
to your Account.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<Alert variant="destructive">
|
||||||
<AlertDescription className="selection:bg-red-100">
|
<AlertDescription className="selection:bg-red-100">
|
||||||
This action is not reversible. Please be certain.
|
This action is not reversible. Please be certain.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
{hasTwoFactorAuthentication && (
|
||||||
<DialogFooter>
|
<div className="flex flex-col gap-y-4">
|
||||||
<Button
|
<FormField
|
||||||
onClick={onDeleteAccount}
|
name="token"
|
||||||
loading={isDeletingAccount}
|
control={deleteAccountTwoFactorTokenForm.control}
|
||||||
variant="destructive"
|
render={({ field }) => (
|
||||||
>
|
<FormItem>
|
||||||
{isDeletingAccount ? 'Deleting account...' : 'Delete Account'}
|
<FormLabel className="text-muted-foreground">
|
||||||
</Button>
|
Two Factor Authentication Token
|
||||||
</DialogFooter>
|
</FormLabel>
|
||||||
</DialogContent>
|
<FormControl>
|
||||||
</Dialog>
|
<Input {...field} value={field.value ?? ''} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={onDeleteAccount}
|
||||||
|
loading={isDeletingAccount}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
{isDeletingAccount ? 'Deleting account...' : 'Delete Account'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { createTOTPKeyURI } from 'oslo/otp';
|
|||||||
|
|
||||||
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||||
import { symmetricEncrypt } from '../../universal/crypto';
|
import { symmetricEncrypt } from '../../universal/crypto';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { ErrorCode } from '../../next-auth/error-codes';
|
import { ErrorCode } from '../../next-auth/error-codes';
|
||||||
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { base32 } from '@scure/base';
|
import { base32 } from '@scure/base';
|
||||||
import { TOTPController } from 'oslo/otp';
|
import { TOTPController } from 'oslo/otp';
|
||||||
|
|
||||||
import { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||||
import { symmetricDecrypt } from '../../universal/crypto';
|
import { symmetricDecrypt } from '../../universal/crypto';
|
||||||
@ -17,6 +17,7 @@ export const verifyTwoFactorAuthenticationToken = async ({
|
|||||||
user,
|
user,
|
||||||
totpCode,
|
totpCode,
|
||||||
}: VerifyTwoFactorAuthenticationTokenOptions) => {
|
}: VerifyTwoFactorAuthenticationTokenOptions) => {
|
||||||
|
// TODO: This is undefined and I can't figure out why.
|
||||||
const key = DOCUMENSO_ENCRYPTION_KEY;
|
const key = DOCUMENSO_ENCRYPTION_KEY;
|
||||||
|
|
||||||
if (!user.twoFactorSecret) {
|
if (!user.twoFactorSecret) {
|
||||||
|
|||||||
@ -13,7 +13,8 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
destructive:
|
||||||
|
'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive',
|
||||||
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
|
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
|||||||
Reference in New Issue
Block a user