From bbad3bc0ea6a44950d371c7c44aa256b976bee91 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Fri, 21 Aug 2026 13:43:44 +0200 Subject: [PATCH] feat(account-deletion): delete the account from user settings The privacy policy has always promised deletion on request, with emailing a person as the only route. This is that promise, self-serve. The dialog spells out what is lost before it asks, because the consequences reach past the person clicking: every workspace they own goes, and anyone sharing one loses that work. Confirmation is typing DELETE, the same bar the destructive actions elsewhere use. On success it reloads the page rather than routing, so no cached query outlives the account it belonged to. --- .../components/DeleteAccountDialog.tsx | 89 +++++++++++++++++++ .../components/ProfileContent.tsx | 16 ++++ surfsense_web/lib/apis/user-api.service.ts | 7 ++ 3 files changed, 112 insertions(+) create mode 100644 surfsense_web/app/dashboard/[workspace_id]/user-settings/components/DeleteAccountDialog.tsx 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();