Files
Jyotisha/frontend/src/lib/account-deletion-worker-core.ts
T
Jesse_ChenandClaude Opus 5.5 96adbce3a9 feat(account): self-service deletion with a 7-day cooling-off period
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
2026-09-30 09:42:11 +08:00

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 };
}