Request signs out everywhere and freezes paid routes (423 + DB triggers); signing in within 7 days shows the pending gate with 撤销注销. A periodic idempotent worker purges personal content afterwards and keeps finance rows against a tombstoned identity. Read-only admin list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
/**
|
|
* The permanent step of account deletion (2026-09-30). One tick finds pending
|
|
* requests whose 7 days are over and calls `purge_deleted_account` for each.
|
|
* The database function is all-or-nothing, so a failed purge leaves the
|
|
* account exactly as it was; the attempt is counted and the next tick retries
|
|
* until MAX_ATTEMPTS. Logs carry counts and codes only, never ids or emails.
|
|
*/
|
|
|
|
export const ACCOUNT_DELETION_MAX_ATTEMPTS = 5;
|
|
export const ACCOUNT_DELETION_BATCH = 20;
|
|
export const ACCOUNT_DELETION_TICK_MS = 30 * 60 * 1000;
|
|
export const ACCOUNT_DELETION_FIRST_TICK_MS = 60 * 1000;
|
|
|
|
export type AccountDeletionWorkerDeps = Readonly<{
|
|
now: () => Date;
|
|
listDue: (nowIso: string, limit: number, maxAttempts: number) => Promise<readonly string[]>;
|
|
purge: (requestId: string) => Promise<{ ok: true; status: string } | { ok: false; code: string }>;
|
|
markFailed: (requestId: string, code: string) => Promise<void>;
|
|
log: (line: string) => void;
|
|
}>;
|
|
|
|
export type AccountDeletionTickResult = Readonly<{ due: number; completed: number; skipped: number; failed: number }>;
|
|
|
|
export function purgeErrorCode(message: string | undefined): string {
|
|
if (message?.includes("account_deletion_purge_incomplete")) return "purge_incomplete";
|
|
if (message?.includes("account_deletion_request_not_found")) return "request_not_found";
|
|
return "purge_failed";
|
|
}
|
|
|
|
export async function runAccountDeletionTick(deps: AccountDeletionWorkerDeps): Promise<AccountDeletionTickResult> {
|
|
const due = await deps.listDue(deps.now().toISOString(), ACCOUNT_DELETION_BATCH, ACCOUNT_DELETION_MAX_ATTEMPTS);
|
|
let completed = 0;
|
|
let skipped = 0;
|
|
let failed = 0;
|
|
for (const requestId of due) {
|
|
const result = await deps.purge(requestId);
|
|
if (result.ok) {
|
|
if (result.status === "completed") completed += 1; else skipped += 1;
|
|
continue;
|
|
}
|
|
failed += 1;
|
|
await deps.markFailed(requestId, result.code);
|
|
}
|
|
if (due.length > 0) {
|
|
deps.log(`[account-deletion-worker] due=${due.length} completed=${completed} skipped=${skipped} failed=${failed}`);
|
|
}
|
|
return { due: due.length, completed, skipped, failed };
|
|
}
|