diff --git a/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx b/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx new file mode 100644 index 000000000..933b37f31 --- /dev/null +++ b/apps/web/src/app/(dashboard)/settings/profile/delete-account-dialog.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { signOut } from 'next-auth/react'; + +import type { User } from '@documenso/prisma/client'; +import { TRPCClientError } from '@documenso/trpc/client'; +import { trpc } from '@documenso/trpc/react'; +import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +export type DeleteAccountDialogProps = { + className?: string; + user: User; +}; + +export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProps) => { + const { toast } = useToast(); + + const hasTwoFactorAuthentication = user.twoFactorEnabled; + + const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } = + trpc.profile.deleteAccount.useMutation(); + + const onDeleteAccount = async () => { + try { + await deleteAccount(); + + toast({ + title: 'Account deleted', + description: 'Your account has been deleted successfully.', + duration: 5000, + }); + + return await signOut({ callbackUrl: '/' }); + } catch (err) { + if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { + toast({ + title: 'An error occurred', + description: err.message, + variant: 'destructive', + }); + } else { + toast({ + title: 'An unknown error occurred', + variant: 'destructive', + description: + err.message ?? + 'We encountered an unknown error while attempting to delete your account. Please try again later.', + }); + } + } + }; + + return ( +
+ +
+ Delete Account + + Delete your account and all its contents, including completed documents. This action is + irreversible and will cancel your subscription, so proceed with caution. + +
+ +
+ + + + + + + Delete Account + + + + This action is not reversible. Please be certain. + + + + {hasTwoFactorAuthentication && ( + + + Disable Two Factor Authentication before deleting your account. + + + )} + + + Documenso will delete all of your documents + , along with all of your completed documents, signatures, and all other resources + belonging to your Account. + + + + + + + + +
+
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/settings/profile/page.tsx b/apps/web/src/app/(dashboard)/settings/profile/page.tsx index 2890eb5d5..11cfc8515 100644 --- a/apps/web/src/app/(dashboard)/settings/profile/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/profile/page.tsx @@ -5,6 +5,8 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get- import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header'; import { ProfileForm } from '~/components/forms/profile'; +import { DeleteAccountDialog } from './delete-account-dialog'; + export const metadata: Metadata = { title: 'Profile', }; @@ -16,7 +18,9 @@ export default async function ProfileSettingsPage() {
- + + +
); } diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index 8a7e2ff3f..c3f8eca37 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -3,7 +3,6 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; -import { signOut } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -11,18 +10,7 @@ import type { User } from '@documenso/prisma/client'; import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; -import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; -import { Card, CardContent, CardFooter } from '@documenso/ui/primitives/card'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@documenso/ui/primitives/dialog'; import { Form, FormControl, @@ -105,36 +93,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { } }; - const onDeleteAccount = async () => { - try { - await deleteAccount(); - - toast({ - title: 'Account deleted', - description: 'Your account has been deleted successfully.', - duration: 5000, - }); - - return await signOut({ callbackUrl: '/' }); - } catch (err) { - if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') { - toast({ - title: 'An error occurred', - description: err.message, - variant: 'destructive', - }); - } else { - toast({ - title: 'An unknown error occurred', - variant: 'destructive', - description: - err.message ?? - 'We encountered an unknown error while attempting to delete your account. Please try again later.', - }); - } - } - }; - return (
{ {isSubmitting ? 'Updating profile...' : 'Update profile'}
- -
- - - - Delete your account and all its contents, including completed documents. This action is - irreversible and will cancel your subscription, so proceed with caution. - - - - - - - - - Delete Account - - Documenso will delete{' '} - all of your documents, along with all of - your completed documents, signatures, and all other resources belonging to your - Account. - - - - - - This action is not reversible. Please be certain. - - - - {hasTwoFactorAuthentication && ( - - - Disable Two Factor Authentication before deleting your account. - - - )} - - - - - - - - -
); }; diff --git a/packages/lib/server-only/user/delete-user.ts b/packages/lib/server-only/user/delete-user.ts index 65a74ac42..d6d4284b4 100644 --- a/packages/lib/server-only/user/delete-user.ts +++ b/packages/lib/server-only/user/delete-user.ts @@ -1,11 +1,13 @@ import { prisma } from '@documenso/prisma'; import { DocumentStatus } from '@documenso/prisma/client'; +import { deletedAccountServiceAccount } from './service-accounts/deleted-account'; + export type DeleteUserOptions = { email: string; }; -export const deletedServiceAccount = async ({ email }: DeleteUserOptions) => { +export const deleteUser = async ({ email }: DeleteUserOptions) => { const user = await prisma.user.findFirst({ where: { email: { @@ -14,20 +16,13 @@ export const deletedServiceAccount = async ({ email }: DeleteUserOptions) => { }, }); - const defaultDeleteUser = await prisma.user.findFirst({ - where: { - email: 'deleted@documenso.com', - }, - }); - if (!user) { throw new Error(`User with email ${email} not found`); } - if (!defaultDeleteUser) { - throw new Error(`Default delete account not found`); - } + const serviceAccount = await deletedAccountServiceAccount(); + // TODO: Send out cancellations for all pending docs await prisma.document.updateMany({ where: { userId: user.id, @@ -36,7 +31,7 @@ export const deletedServiceAccount = async ({ email }: DeleteUserOptions) => { }, }, data: { - userId: defaultDeleteUser.id, + userId: serviceAccount.id, deletedAt: new Date(), }, }); diff --git a/packages/lib/server-only/user/service-accounts/deleted-account.ts b/packages/lib/server-only/user/service-accounts/deleted-account.ts new file mode 100644 index 000000000..6bfd6d25f --- /dev/null +++ b/packages/lib/server-only/user/service-accounts/deleted-account.ts @@ -0,0 +1,17 @@ +import { prisma } from '@documenso/prisma'; + +export const deletedAccountServiceAccount = async () => { + const serviceAccount = await prisma.user.findFirst({ + where: { + email: 'deleted-account@documenso.com', + }, + }); + + if (!serviceAccount) { + throw new Error( + 'Deleted account service account not found, have you ran the appropriate migrations?', + ); + } + + return serviceAccount; +}; diff --git a/packages/prisma/migrations/20240205120648_create_delete_account/migration.sql b/packages/prisma/migrations/20240205120648_create_delete_account/migration.sql index bfb9c2c83..d001bc4ae 100644 --- a/packages/prisma/migrations/20240205120648_create_delete_account/migration.sql +++ b/packages/prisma/migrations/20240205120648_create_delete_account/migration.sql @@ -1,7 +1,7 @@ -- Create deleted@documenso.com DO $$ BEGIN - IF NOT EXISTS (SELECT 1 FROM "public"."User" WHERE "email" = 'deleted@documenso.com') THEN + IF NOT EXISTS (SELECT 1 FROM "public"."User" WHERE "email" = 'deleted-account@documenso.com') THEN INSERT INTO "public"."User" ( "email", @@ -16,7 +16,7 @@ BEGIN ) VALUES ( - 'deleted@documenso.com', + 'deleted-account@documenso.com', NOW(), NULL, NOW(), diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts index 56a6eea29..2f636d87d 100644 --- a/packages/trpc/server/profile-router/router.ts +++ b/packages/trpc/server/profile-router/router.ts @@ -1,6 +1,6 @@ import { TRPCError } from '@trpc/server'; -import { deletedServiceAccount } from '@documenso/lib/server-only/user/delete-user'; +import { deleteUser } from '@documenso/lib/server-only/user/delete-user'; import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs'; import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'; import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id'; @@ -161,7 +161,7 @@ export const profileRouter = router({ try { const user = ctx.user; - return await deletedServiceAccount(user); + return await deleteUser(user); } catch (err) { let message = 'We were unable to delete your account. Please try again.';