feat: establish NekoNest Cloud control and relay

This commit is contained in:
2026-08-12 23:25:43 +08:00
commit f27606b709
222 changed files with 71456 additions and 0 deletions
@@ -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>
);
}