diff --git a/surfsense_web/app/dashboard/[workspace_id]/user-settings/components/DeleteAccountDialog.tsx b/surfsense_web/app/dashboard/[workspace_id]/user-settings/components/DeleteAccountDialog.tsx new file mode 100644 index 000000000..c80d4b58a --- /dev/null +++ b/surfsense_web/app/dashboard/[workspace_id]/user-settings/components/DeleteAccountDialog.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Spinner } from "@/components/ui/spinner"; +import { userApiService } from "@/lib/apis/user-api.service"; +import { logout } from "@/lib/auth-utils"; + +// Not translated: the label interpolates this exact word. +const CONFIRMATION_WORD = "DELETE"; + +interface DeleteAccountDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +/** Spell out what leaving costs, then erase the account. */ +export function DeleteAccountDialog({ open, onOpenChange }: DeleteAccountDialogProps) { + const t = useTranslations("userSettings"); + const [confirmation, setConfirmation] = useState(""); + const [deleting, setDeleting] = useState(false); + + useEffect(() => { + if (open) setConfirmation(""); + }, [open]); + + const handleDelete = async () => { + setDeleting(true); + try { + await userApiService.deleteMe(); + await logout(); + // Reload, not a route push: no cache should outlive the account. + window.location.href = "/"; + } catch { + toast.error(t("delete_account_error")); + setDeleting(false); + } + }; + + return ( + + + + {t("delete_account_title")} + {t("delete_account_consequences")} + + +
+ + setConfirmation(e.target.value)} + /> +
+ + + + + +
+
+ ); +} diff --git a/surfsense_web/app/dashboard/[workspace_id]/user-settings/components/ProfileContent.tsx b/surfsense_web/app/dashboard/[workspace_id]/user-settings/components/ProfileContent.tsx index 89bc362eb..01a91b4de 100644 --- a/surfsense_web/app/dashboard/[workspace_id]/user-settings/components/ProfileContent.tsx +++ b/surfsense_web/app/dashboard/[workspace_id]/user-settings/components/ProfileContent.tsx @@ -12,6 +12,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Spinner } from "@/components/ui/spinner"; import { getUserAvatarColor, getUserInitials } from "@/lib/user-avatar"; +import { DeleteAccountDialog } from "./DeleteAccountDialog"; function AvatarDisplay({ url, @@ -56,6 +57,7 @@ export function ProfileContent() { const { mutateAsync: updateUser, isPending } = useAtomValue(updateUserMutationAtom); const [displayName, setDisplayName] = useState(""); + const [deleteOpen, setDeleteOpen] = useState(false); useEffect(() => { if (user) { @@ -131,6 +133,20 @@ export function ProfileContent() { )} + + {!isUserLoading && ( +
+
+

{t("delete_account_heading")}

+

{t("delete_account_description")}

+
+ +
+ )} + + ); } diff --git a/surfsense_web/lib/apis/user-api.service.ts b/surfsense_web/lib/apis/user-api.service.ts index 083fd8dee..b8a7f41d0 100644 --- a/surfsense_web/lib/apis/user-api.service.ts +++ b/surfsense_web/lib/apis/user-api.service.ts @@ -21,6 +21,13 @@ class UserApiService { body: request, }); }; + + /** + * Delete the current account. Locks it out immediately; the erase follows. + */ + deleteMe = async () => { + return baseApiService.delete(`/users/me`); + }; } export const userApiService = new UserApiService();