mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-10 22:51:47 +02:00
✨ Add option to delete account (#1110)
This commit is contained in:
parent
acbb6e552f
commit
899bb966fa
7 changed files with 207 additions and 10 deletions
|
@ -97,7 +97,6 @@
|
|||
"addComment": "Add Comment",
|
||||
"profile": "Profile",
|
||||
"polls": "Polls",
|
||||
"showMore": "Show more…",
|
||||
"timeZoneSelect__noOption": "No option found",
|
||||
"timeZoneSelect__inputPlaceholder": "Search…",
|
||||
"poweredByRallly": "Powered by <a>{name}</a>",
|
||||
|
@ -246,5 +245,10 @@
|
|||
"timeShownIn": "Times shown in {timeZone}",
|
||||
"pollStatusPausedDescription": "Votes cannot be submitted or edited at this time",
|
||||
"eventHostTitle": "Manage Access",
|
||||
"eventHostDescription": "You are the creator of this poll"
|
||||
"eventHostDescription": "You are the creator of this poll",
|
||||
"deleteAccount": "Delete Account",
|
||||
"deleteAccountDialogTitle": "Delete Account",
|
||||
"deleteAccountDialogDescription": "Are you sure you want to delete your account?",
|
||||
"deleteAccountInstruction": "Please confirm your email address to delete your account",
|
||||
"emailMismatch": "Email does not match the account email"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
"use client";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogProps,
|
||||
DialogTitle,
|
||||
} from "@rallly/ui/dialog";
|
||||
import { Form, FormField, FormItem, FormMessage } from "@rallly/ui/form";
|
||||
import { Input } from "@rallly/ui/input";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useTranslation } from "@/app/i18n/client";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
import { trpc } from "@/utils/trpc/client";
|
||||
|
||||
export function DeleteAccountDialog({
|
||||
email,
|
||||
children,
|
||||
...rest
|
||||
}: DialogProps & {
|
||||
email: string;
|
||||
}) {
|
||||
const form = useForm<{ email: string }>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
});
|
||||
const { t } = useTranslation("app");
|
||||
const trpcUtils = trpc.useUtils();
|
||||
const posthog = usePostHog();
|
||||
const deleteAccount = trpc.user.delete.useMutation({
|
||||
onSuccess() {
|
||||
posthog?.capture("delete account");
|
||||
trpcUtils.invalidate();
|
||||
signOut({
|
||||
callbackUrl: "/login",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<Dialog {...rest}>
|
||||
{children}
|
||||
<DialogContent>
|
||||
<form
|
||||
method="POST"
|
||||
action="/auth/logout"
|
||||
onSubmit={form.handleSubmit(async () => {
|
||||
await deleteAccount.mutateAsync();
|
||||
})}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans
|
||||
i18nKey="deleteAccountDialogTitle"
|
||||
defaults="Delete Account"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans
|
||||
i18nKey="deleteAccountDialogDescription"
|
||||
defaults="Are you sure you want to delete your account?"
|
||||
/>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<p className="text-sm">
|
||||
<Trans
|
||||
i18nKey="deleteAccountInstruction"
|
||||
defaults="Please confirm your email address to delete your account"
|
||||
/>
|
||||
</p>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (value !== email) {
|
||||
return t("emailMismatch", {
|
||||
defaultValue: "Email does not match the account email",
|
||||
});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Input
|
||||
error={!!form.formState.errors.email}
|
||||
placeholder={email}
|
||||
{...field}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button>
|
||||
<Trans i18nKey="cancel" defaults="Cancel" />
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={deleteAccount.isLoading}
|
||||
variant="destructive"
|
||||
>
|
||||
<Trans i18nKey="deleteAccount" defaults="Delete Account" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
);
|
||||
}
|
|
@ -1,12 +1,16 @@
|
|||
"use client";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { DialogTrigger } from "@rallly/ui/dialog";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { Input } from "@rallly/ui/input";
|
||||
import { Label } from "@rallly/ui/label";
|
||||
import { InfoIcon, LogOutIcon, UserXIcon } from "lucide-react";
|
||||
import { InfoIcon, LogOutIcon, TrashIcon, UserXIcon } from "lucide-react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { DeleteAccountDialog } from "@/app/[locale]/(admin)/settings/profile/delete-account-dialog";
|
||||
import { LogoutButton } from "@/app/components/logout-button";
|
||||
import { ProfileSettings } from "@/components/settings/profile-settings";
|
||||
import {
|
||||
|
@ -91,6 +95,29 @@ export const ProfilePage = () => {
|
|||
<Trans i18nKey="logout" defaults="Logout" />
|
||||
</LogoutButton>
|
||||
</SettingsSection>
|
||||
{user.email ? (
|
||||
<>
|
||||
<hr />
|
||||
<SettingsSection
|
||||
title="Danger Zone"
|
||||
description="Delete your account permanently. This action cannot be undone."
|
||||
>
|
||||
<DeleteAccountDialog email={user.email}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
<Icon>
|
||||
<TrashIcon />
|
||||
</Icon>
|
||||
<Trans
|
||||
i18nKey="deleteAccount"
|
||||
defaults="Delete Account"
|
||||
/>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</DeleteAccountDialog>
|
||||
</SettingsSection>
|
||||
</>
|
||||
) : null}
|
||||
</SettingsContent>
|
||||
)}
|
||||
</Settings>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
|
@ -8,7 +9,6 @@ import {
|
|||
import { Input } from "@rallly/ui/input";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { LegacyButton } from "@/components/button";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { UserAvatar } from "@/components/user";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
|
@ -72,14 +72,14 @@ export const ProfileSettings = () => {
|
|||
)}
|
||||
/>
|
||||
<div className="mt-4 flex">
|
||||
<LegacyButton
|
||||
<Button
|
||||
loading={formState.isSubmitting}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
disabled={!formState.isDirty}
|
||||
>
|
||||
<Trans i18nKey="save" />
|
||||
</LegacyButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -38,6 +38,47 @@ export const user = router({
|
|||
},
|
||||
});
|
||||
}),
|
||||
delete: privateProcedure.mutation(async ({ ctx }) => {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const polls = await tx.poll.findMany({
|
||||
select: { id: true },
|
||||
where: { userId: ctx.user.id },
|
||||
});
|
||||
const pollIds = polls.map((poll) => poll.id);
|
||||
await tx.comment.deleteMany({
|
||||
where: { pollId: { in: pollIds } },
|
||||
});
|
||||
await tx.option.deleteMany({
|
||||
where: { pollId: { in: pollIds } },
|
||||
});
|
||||
await tx.participant.deleteMany({
|
||||
where: { OR: [{ pollId: { in: pollIds } }, { userId: ctx.user.id }] },
|
||||
});
|
||||
await tx.watcher.deleteMany({
|
||||
where: { OR: [{ pollId: { in: pollIds } }, { userId: ctx.user.id }] },
|
||||
});
|
||||
await tx.vote.deleteMany({
|
||||
where: { pollId: { in: pollIds } },
|
||||
});
|
||||
await tx.event.deleteMany({
|
||||
where: { userId: ctx.user.id },
|
||||
});
|
||||
await tx.poll.deleteMany({
|
||||
where: { userId: ctx.user.id },
|
||||
});
|
||||
await tx.account.deleteMany({
|
||||
where: { userId: ctx.user.id },
|
||||
});
|
||||
await tx.userPaymentData.deleteMany({
|
||||
where: { userId: ctx.user.id },
|
||||
});
|
||||
await tx.user.delete({
|
||||
where: {
|
||||
id: ctx.user.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
}),
|
||||
subscription: possiblyPublicProcedure.query(
|
||||
async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => {
|
||||
if (ctx.user.isGuest) {
|
||||
|
|
|
@ -33,7 +33,7 @@ export function Icon({ children, size, variant }: IconProps) {
|
|||
<Slot
|
||||
className={cn(
|
||||
iconVariants({ size, variant }),
|
||||
"group-[.bg-primary]:text-primary-100 group shrink-0",
|
||||
"group-[.bg-primary]:text-primary-100 group-[.bg-destructive]:text-destructive-foreground group shrink-0",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -13,7 +13,7 @@ export type InputProps = Omit<
|
|||
|
||||
const inputVariants = cva(
|
||||
cn(
|
||||
"focus-visible:border-primary-400 focus-visible:ring-offset-1 focus-visible:outline-none focus-visible:ring-primary-200 focus-visible:ring-1",
|
||||
"w-full focus-visible:border-primary-400 focus-visible:ring-offset-1 focus-visible:outline-none focus-visible:ring-primary-200 focus-visible:ring-1",
|
||||
"border-input placeholder:text-muted-foreground h-9 rounded border bg-gray-50 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:cursor-not-allowed disabled:opacity-50",
|
||||
),
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue