mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
chore: tidy code and extract alert-dialog
This commit is contained in:
@ -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 (
|
||||||
|
<div className={className}>
|
||||||
|
<Alert
|
||||||
|
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
|
||||||
|
variant="neutral"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<AlertTitle>Delete Account</AlertTitle>
|
||||||
|
<AlertDescription className="mr-2">
|
||||||
|
Delete your account and all its contents, including completed documents. This action is
|
||||||
|
irreversible and will cancel your subscription, so proceed with caution.
|
||||||
|
</AlertDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="destructive">Delete Account</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader className="space-y-4">
|
||||||
|
<DialogTitle>Delete Account</DialogTitle>
|
||||||
|
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription className="selection:bg-red-100">
|
||||||
|
This action is not reversible. Please be certain.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{hasTwoFactorAuthentication && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription className="selection:bg-red-100">
|
||||||
|
Disable Two Factor Authentication before deleting your account.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={onDeleteAccount}
|
||||||
|
loading={isDeletingAccount}
|
||||||
|
variant="destructive"
|
||||||
|
disabled={hasTwoFactorAuthentication}
|
||||||
|
>
|
||||||
|
{isDeletingAccount ? 'Deleting account...' : 'Delete Account'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -5,6 +5,8 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
|
||||||
import { ProfileForm } from '~/components/forms/profile';
|
import { ProfileForm } from '~/components/forms/profile';
|
||||||
|
|
||||||
|
import { DeleteAccountDialog } from './delete-account-dialog';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Profile',
|
title: 'Profile',
|
||||||
};
|
};
|
||||||
@ -16,7 +18,9 @@ export default async function ProfileSettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<SettingsHeader title="Profile" subtitle="Here you can edit your personal details." />
|
<SettingsHeader title="Profile" subtitle="Here you can edit your personal details." />
|
||||||
|
|
||||||
<ProfileForm user={user} className="max-w-xl" />
|
<ProfileForm className="max-w-xl" user={user} />
|
||||||
|
|
||||||
|
<DeleteAccountDialog className="mt-8 max-w-xl" user={user} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
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';
|
||||||
|
|
||||||
@ -11,18 +10,7 @@ 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';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
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 {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
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 (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@ -187,59 +145,6 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
|
|||||||
{isSubmitting ? 'Updating profile...' : 'Update profile'}
|
{isSubmitting ? 'Updating profile...' : 'Update profile'}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-8 max-w-xl">
|
|
||||||
<Label>Delete Account</Label>
|
|
||||||
<Card className="border-destructive mt-2 pb-0">
|
|
||||||
<CardContent className="p-4">
|
|
||||||
Delete your account and all its contents, including completed documents. This action is
|
|
||||||
irreversible and will cancel your subscription, so proceed with caution.
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="justify-end pb-4 pr-4">
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button 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">
|
|
||||||
This action is not reversible. Please be certain.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
{hasTwoFactorAuthentication && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription className="selection:bg-red-100">
|
|
||||||
Disable Two Factor Authentication before deleting your account.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
onClick={onDeleteAccount}
|
|
||||||
loading={isDeletingAccount}
|
|
||||||
variant="destructive"
|
|
||||||
disabled={hasTwoFactorAuthentication}
|
|
||||||
>
|
|
||||||
{isDeletingAccount ? 'Deleting account...' : 'Delete Account'}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
|
import { deletedAccountServiceAccount } from './service-accounts/deleted-account';
|
||||||
|
|
||||||
export type DeleteUserOptions = {
|
export type DeleteUserOptions = {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deletedServiceAccount = async ({ email }: DeleteUserOptions) => {
|
export const deleteUser = async ({ email }: DeleteUserOptions) => {
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
email: {
|
email: {
|
||||||
@ -14,20 +16,13 @@ export const deletedServiceAccount = async ({ email }: DeleteUserOptions) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultDeleteUser = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
email: 'deleted@documenso.com',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`User with email ${email} not found`);
|
throw new Error(`User with email ${email} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!defaultDeleteUser) {
|
const serviceAccount = await deletedAccountServiceAccount();
|
||||||
throw new Error(`Default delete account not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO: Send out cancellations for all pending docs
|
||||||
await prisma.document.updateMany({
|
await prisma.document.updateMany({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -36,7 +31,7 @@ export const deletedServiceAccount = async ({ email }: DeleteUserOptions) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
userId: defaultDeleteUser.id,
|
userId: serviceAccount.id,
|
||||||
deletedAt: new Date(),
|
deletedAt: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
-- Create deleted@documenso.com
|
-- Create deleted@documenso.com
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
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
|
INSERT INTO
|
||||||
"public"."User" (
|
"public"."User" (
|
||||||
"email",
|
"email",
|
||||||
@ -16,7 +16,7 @@ BEGIN
|
|||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
'deleted@documenso.com',
|
'deleted-account@documenso.com',
|
||||||
NOW(),
|
NOW(),
|
||||||
NULL,
|
NULL,
|
||||||
NOW(),
|
NOW(),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
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 { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
|
||||||
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
|
||||||
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
|
||||||
@ -161,7 +161,7 @@ export const profileRouter = router({
|
|||||||
try {
|
try {
|
||||||
const user = ctx.user;
|
const user = ctx.user;
|
||||||
|
|
||||||
return await deletedServiceAccount(user);
|
return await deleteUser(user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let message = 'We were unable to delete your account. Please try again.';
|
let message = 'We were unable to delete your account. Please try again.';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user