feat(account): add deletion lifecycle
CI and Deploy / test (pull_request) Successful in 5m18s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 19:03:54 +02:00
parent 1ec9dd037e
commit 842e793f69
28 changed files with 4418 additions and 129 deletions
+60 -1
View File
@@ -1,15 +1,35 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Alert, Box, Button, Divider, Paper, Typography } from "@mui/material";
import { api, getApiErrorMessage } from "../api";
import { clearAuthClientState } from "../auth";
import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
import { usePrompt } from "../prompt";
type AccountLifecycleStatus = {
deletionEnabled: boolean;
deletionStatus: string;
requiredConfirmation: string;
request?: { status: string; stage: string } | null;
};
export default function BackupCard() {
const { toast } = useToast();
const { t } = useI18n();
const { prompt } = usePrompt();
const [downloading, setDownloading] = useState(false);
const [exportingAccount, setExportingAccount] = useState(false);
const [deletingAccount, setDeletingAccount] = useState(false);
const [lifecycle, setLifecycle] = useState<AccountLifecycleStatus | null>(null);
useEffect(() => {
let active = true;
api.get<AccountLifecycleStatus>("/account-lifecycle/status")
.then((response) => { if (active) setLifecycle(response.data); })
.catch(() => { if (active) setLifecycle(null); });
return () => { active = false; };
}, []);
const downloadBlob = (blob: Blob, contentDisposition: string | undefined, fallbackName: string) => {
const url = URL.createObjectURL(blob);
@@ -49,6 +69,31 @@ export default function BackupCard() {
}
};
const deleteAccount = async () => {
if (!lifecycle?.deletionEnabled || lifecycle.deletionStatus !== "active") return;
const confirmation = await prompt({
title: t("accountDeleteTitle"),
message: t("accountDeletePrompt", { confirmation: lifecycle.requiredConfirmation }),
confirmLabel: t("accountDeleteButton"),
cancelLabel: t("cancel"),
});
if (confirmation === null) return;
if (confirmation.trim() !== lifecycle.requiredConfirmation) {
toast(t("accountDeleteConfirmationMismatch"), "error");
return;
}
setDeletingAccount(true);
try {
const response = await api.post("/account-lifecycle/delete", { confirmation });
setLifecycle((current) => current ? { ...current, deletionStatus: "pending", request: response.data } : current);
clearAuthClientState();
window.location.assign("/login");
} catch (error: any) {
toast(getApiErrorMessage(error, t("accountDeleteFailed")), "error");
setDeletingAccount(false);
}
};
return (
<Paper sx={{ mt: 2, p: 2 }}>
<Typography variant="h6" sx={{ mb: 1 }}>
@@ -73,6 +118,20 @@ export default function BackupCard() {
{downloading ? t("backupPreparing") : t("backupDownload")}
</Button>
</Box>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 0.5 }}>{t("accountDeleteTitle")}</Typography>
{lifecycle?.deletionStatus === "pending" ? (
<Alert severity="warning">{t("accountDeletePending", { stage: lifecycle.request?.stage ?? "pending" })}</Alert>
) : lifecycle?.deletionEnabled ? (
<Box>
<Alert severity="error" sx={{ mb: 1.5 }}>{t("accountDeleteWarning")}</Alert>
<Button color="error" variant="outlined" onClick={() => void deleteAccount()} disabled={deletingAccount}>
{deletingAccount ? t("accountDeleteStarting") : t("accountDeleteButton")}
</Button>
</Box>
) : (
<Alert severity="info">{t("accountDeleteDisabled")}</Alert>
)}
</Paper>
);
}