feat(account): GDPR self-service account + data deletion (5d)
Add DELETE /api/me/account: permanently erases the signed-in account and all its personal data — OAuth tokens, subscriptions, tags, video states, playlists, notifications all cascade on the users row (FK ON DELETE CASCADE); quota-audit events are kept but anonymised (SET NULL); the email's invite/whitelist row is removed too. Guards: the shared demo account can't be deleted, and the last remaining admin can't delete itself. Frontend: a Danger zone in Settings -> Account (non-demo) with a danger-confirm; on success the session clears and the app lands on the welcome page. EN/HU/DE. Verified via curl: self-delete + session clear + demo 403.
This commit is contained in:
parent
08b3c47876
commit
84b55c0567
6 changed files with 90 additions and 3 deletions
|
|
@ -1,11 +1,12 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Bell, Check, Monitor, RotateCcw, Save, User } from "lucide-react";
|
||||
import { Bell, Check, Monitor, RotateCcw, Save, Trash2, User } from "lucide-react";
|
||||
import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
|
||||
import { type Me } from "../lib/api";
|
||||
import { api, type Me } from "../lib/api";
|
||||
import Avatar from "./Avatar";
|
||||
import { notify, type NotifSettings } from "../lib/notifications";
|
||||
import Tooltip from "./Tooltip";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
|
||||
// The Settings page edits server-persisted preferences as a draft: changes apply locally
|
||||
// for instant preview but reach the server only on an explicit Save (or revert on Discard).
|
||||
|
|
@ -354,6 +355,22 @@ function AccessRow({
|
|||
|
||||
function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const confirm = useConfirm();
|
||||
const onDeleteAccount = async () => {
|
||||
const ok = await confirm({
|
||||
title: t("settings.account.deleteTitle"),
|
||||
message: t("settings.account.deleteConfirm"),
|
||||
confirmLabel: t("settings.account.deleteConfirmButton"),
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await api.deleteAccount();
|
||||
window.location.reload(); // session cleared server-side → lands on the welcome page
|
||||
} catch {
|
||||
/* the global error dialog surfaces the reason (e.g. last admin) */
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Section title={t("settings.account.title")}>
|
||||
|
|
@ -410,6 +427,19 @@ function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
|
|||
</button>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{!me.is_demo && (
|
||||
<Section title={t("settings.account.dangerZone")}>
|
||||
<p className="text-xs text-muted leading-relaxed mb-2">{t("settings.account.deleteHint")}</p>
|
||||
<button
|
||||
onClick={onDeleteAccount}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-xl text-sm border border-red-500/40 text-red-400 hover:bg-red-500/10 transition"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
{t("settings.account.deleteAccount")}
|
||||
</button>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue