mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-08-22 15:23:37 +00:00
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.
This commit is contained in:
parent
4349de6d29
commit
bbad3bc0ea
3 changed files with 112 additions and 0 deletions
|
|
@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("delete_account_title")}</DialogTitle>
|
||||
<DialogDescription>{t("delete_account_consequences")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="delete-confirmation">
|
||||
{t("delete_account_confirm_label", { word: CONFIRMATION_WORD })}
|
||||
</Label>
|
||||
<Input
|
||||
id="delete-confirmation"
|
||||
autoComplete="off"
|
||||
value={confirmation}
|
||||
onChange={(e) => setConfirmation(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={deleting}>
|
||||
{t("delete_account_cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={confirmation !== CONFIRMATION_WORD || deleting}
|
||||
className="relative"
|
||||
>
|
||||
<span className={deleting ? "opacity-0" : ""}>{t("delete_account_confirm")}</span>
|
||||
{deleting && <Spinner size="sm" className="absolute" />}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -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() {
|
|||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{!isUserLoading && (
|
||||
<div className="mt-10 space-y-3 rounded-lg border border-destructive/30 p-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-sm font-medium">{t("delete_account_heading")}</h2>
|
||||
<p className="text-xs text-muted-foreground">{t("delete_account_description")}</p>
|
||||
</div>
|
||||
<Button variant="destructive" size="sm" onClick={() => setDeleteOpen(true)}>
|
||||
{t("delete_account_heading")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DeleteAccountDialog open={deleteOpen} onOpenChange={setDeleteOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue