mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-02 18:51:52 +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",
|
"addComment": "Add Comment",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"polls": "Polls",
|
"polls": "Polls",
|
||||||
"showMore": "Show more…",
|
|
||||||
"timeZoneSelect__noOption": "No option found",
|
"timeZoneSelect__noOption": "No option found",
|
||||||
"timeZoneSelect__inputPlaceholder": "Search…",
|
"timeZoneSelect__inputPlaceholder": "Search…",
|
||||||
"poweredByRallly": "Powered by <a>{name}</a>",
|
"poweredByRallly": "Powered by <a>{name}</a>",
|
||||||
|
@ -246,5 +245,10 @@
|
||||||
"timeShownIn": "Times shown in {timeZone}",
|
"timeShownIn": "Times shown in {timeZone}",
|
||||||
"pollStatusPausedDescription": "Votes cannot be submitted or edited at this time",
|
"pollStatusPausedDescription": "Votes cannot be submitted or edited at this time",
|
||||||
"eventHostTitle": "Manage Access",
|
"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";
|
"use client";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@rallly/ui/alert";
|
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 { Input } from "@rallly/ui/input";
|
||||||
import { Label } from "@rallly/ui/label";
|
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 Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import { DeleteAccountDialog } from "@/app/[locale]/(admin)/settings/profile/delete-account-dialog";
|
||||||
import { LogoutButton } from "@/app/components/logout-button";
|
import { LogoutButton } from "@/app/components/logout-button";
|
||||||
import { ProfileSettings } from "@/components/settings/profile-settings";
|
import { ProfileSettings } from "@/components/settings/profile-settings";
|
||||||
import {
|
import {
|
||||||
|
@ -91,6 +95,29 @@ export const ProfilePage = () => {
|
||||||
<Trans i18nKey="logout" defaults="Logout" />
|
<Trans i18nKey="logout" defaults="Logout" />
|
||||||
</LogoutButton>
|
</LogoutButton>
|
||||||
</SettingsSection>
|
</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>
|
</SettingsContent>
|
||||||
)}
|
)}
|
||||||
</Settings>
|
</Settings>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Button } from "@rallly/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
@ -8,7 +9,6 @@ import {
|
||||||
import { Input } from "@rallly/ui/input";
|
import { Input } from "@rallly/ui/input";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { LegacyButton } from "@/components/button";
|
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { UserAvatar } from "@/components/user";
|
import { UserAvatar } from "@/components/user";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
|
@ -72,14 +72,14 @@ export const ProfileSettings = () => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4 flex">
|
<div className="mt-4 flex">
|
||||||
<LegacyButton
|
<Button
|
||||||
loading={formState.isSubmitting}
|
loading={formState.isSubmitting}
|
||||||
type="primary"
|
variant="primary"
|
||||||
htmlType="submit"
|
type="submit"
|
||||||
disabled={!formState.isDirty}
|
disabled={!formState.isDirty}
|
||||||
>
|
>
|
||||||
<Trans i18nKey="save" />
|
<Trans i18nKey="save" />
|
||||||
</LegacyButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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(
|
subscription: possiblyPublicProcedure.query(
|
||||||
async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => {
|
async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => {
|
||||||
if (ctx.user.isGuest) {
|
if (ctx.user.isGuest) {
|
||||||
|
|
|
@ -33,7 +33,7 @@ export function Icon({ children, size, variant }: IconProps) {
|
||||||
<Slot
|
<Slot
|
||||||
className={cn(
|
className={cn(
|
||||||
iconVariants({ size, variant }),
|
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}
|
{children}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export type InputProps = Omit<
|
||||||
|
|
||||||
const inputVariants = cva(
|
const inputVariants = cva(
|
||||||
cn(
|
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",
|
"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