feat: establish NekoNest Cloud control and relay
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { AccountDeletionRequestRecord } from "@/db/repository";
|
||||
|
||||
type SubmitState = { loading: boolean; message: string; error: boolean };
|
||||
const idle: SubmitState = { loading: false, message: "", error: false };
|
||||
|
||||
async function postDeletion(payload: Record<string, unknown>) {
|
||||
const response = await fetch("/api/account/deletion", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
}),
|
||||
});
|
||||
const body = (await response.json()) as { message?: string };
|
||||
if (!response.ok) throw new Error(body.message || "操作失败");
|
||||
}
|
||||
|
||||
function formatRequestDate(value: string) {
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(value));
|
||||
}
|
||||
|
||||
export function AccountDeletionAction({
|
||||
requests,
|
||||
}: {
|
||||
requests: AccountDeletionRequestRecord[];
|
||||
}) {
|
||||
const active = requests.find((request) => request.status === "requested");
|
||||
const processing = requests.find((request) =>
|
||||
request.status === "processing" || request.status === "relay_purged"
|
||||
);
|
||||
const [confirmed, setConfirmed] = useState(false);
|
||||
const [reason, setReason] = useState("");
|
||||
const [state, setState] = useState(idle);
|
||||
|
||||
async function submitRequest(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
setState({ loading: true, message: "", error: false });
|
||||
try {
|
||||
await postDeletion({ action: "request", confirmed, reason });
|
||||
setState({ loading: false, message: "注销申请已记录。", error: false });
|
||||
window.setTimeout(() => window.location.reload(), 700);
|
||||
} catch (error) {
|
||||
setState({
|
||||
loading: false,
|
||||
message: error instanceof Error ? error.message : "操作失败",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelRequest() {
|
||||
if (!active) return;
|
||||
setState({ loading: true, message: "", error: false });
|
||||
try {
|
||||
await postDeletion({ action: "cancel", requestId: active.id });
|
||||
setState({ loading: false, message: "注销申请已撤回。", error: false });
|
||||
window.setTimeout(() => window.location.reload(), 700);
|
||||
} catch (error) {
|
||||
setState({
|
||||
loading: false,
|
||||
message: error instanceof Error ? error.message : "操作失败",
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (active) {
|
||||
return (
|
||||
<div className="deletion-request-state">
|
||||
<span>待人工核对</span>
|
||||
<small>{formatRequestDate(active.requested_at)}</small>
|
||||
{active.reason && <p>{active.reason}</p>}
|
||||
<button
|
||||
className="button button-secondary"
|
||||
type="button"
|
||||
onClick={cancelRequest}
|
||||
disabled={state.loading}
|
||||
>
|
||||
{state.loading ? "正在撤回…" : "撤回注销申请"}
|
||||
</button>
|
||||
{state.message && (
|
||||
<p className={state.error ? "form-error" : "form-success"} role="status">
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (processing) {
|
||||
return (
|
||||
<div className="deletion-request-state">
|
||||
<span>{processing.status === "relay_purged" ? "Relay 数据已逻辑删除" : "正在永久删除 Relay 数据"}</span>
|
||||
<small>{formatRequestDate(processing.requested_at)}</small>
|
||||
<p>
|
||||
{processing.status === "relay_purged"
|
||||
? "实时 Relay 数据、附件与备份已删除;账户身份和依法保留记录仍按最终退出政策处理。"
|
||||
: "访问已经暂停,删除期间不能撤回申请。"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="deletion-request-form" onSubmit={submitRequest}>
|
||||
<label>
|
||||
<span>补充说明(可选)</span>
|
||||
<textarea
|
||||
maxLength={500}
|
||||
value={reason}
|
||||
onChange={(event) => setReason(event.target.value)}
|
||||
placeholder="例如:暂时不再参加公测"
|
||||
/>
|
||||
</label>
|
||||
<label className="deletion-confirmation">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={confirmed}
|
||||
onChange={(event) => setConfirmed(event.target.checked)}
|
||||
required
|
||||
/>
|
||||
<span>我知道申请不会立即删除主机上的原生会话,并可在处理前撤回。</span>
|
||||
</label>
|
||||
<button
|
||||
className="button button-secondary"
|
||||
type="submit"
|
||||
disabled={state.loading || !confirmed}
|
||||
>
|
||||
{state.loading ? "正在提交…" : "提交注销申请"}
|
||||
</button>
|
||||
{state.message && (
|
||||
<p className={state.error ? "form-error" : "form-success"} role="status">
|
||||
{state.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user