feat(account): add deletion lifecycle
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user