diff options
| author | Adam Soltys <[email protected]> | 2026-11-04 14:29:34 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-11-04 14:31:20 -0700 |
| commit | 3e98c1b1026be994350741d2adbc63472d2a9770 (patch) | |
| tree | 94a8fe07e01a680f5a5a1d5f2c261bf479bbf80b /routes | |
| parent | 10218ae86b464b8e3d29c27513eb5cc0554d044b (diff) | |
| download | coinos-server-3e98c1b1026be994350741d2adbc63472d2a9770.tar.gz coinos-server-3e98c1b1026be994350741d2adbc63472d2a9770.zip | |
self-delete: use the full delall sweep, require the PIN
POST /delete now runs lib/deluser, the same sweep as scripts/delall.ts:
it also disables the account's bolt12 offers on the node, removes the
invoice pointer keys (invoice:<hash|text|offer_id>), covers sub-account
invoices and deletes from the archive too. Offers are disabled before any
key is deleted, so an unreachable node aborts with the account intact.
Accounts with a PIN must now send it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'routes')
| -rw-r--r-- | routes/users.ts | 90 |
1 files changed, 11 insertions, 79 deletions
diff --git a/routes/users.ts b/routes/users.ts index 9f730b6f..400b554b 100644 --- a/routes/users.ts +++ b/routes/users.ts @@ -9,6 +9,7 @@ import { isDefaultName } from "$lib/default-names"; import { upgradeLegacyNsec } from "$lib/legacynsec"; import { mail, templates } from "$lib/mail"; import { getNostrUser, getProfile, serverPubkey2 } from "$lib/nostr"; +import deleteUser from "$lib/deluser"; import register from "$lib/register"; import { assertNameFree } from "$lib/names"; import { isReserved } from "$lib/reserved"; @@ -880,22 +881,24 @@ export default { res.send({}); }, - // Authenticated self-delete: wipes every key tied to the calling user, - // mirroring scripts/delall-legacy.ts. Gated by typing the username (works for - // password and nostr accounts alike) and refuses while a balance remains so a - // user can't accidentally destroy funds. + // Authenticated self-delete: wipes every key tied to the calling user via + // lib/deluser (the same sweep as scripts/delall.ts, offers and archive + // included). Gated by typing the username (works for password and nostr + // accounts alike) plus the PIN when one is set, and refuses while a balance + // remains so a user can't accidentally destroy funds. async deleteSelf(req, res) { try { const { user } = req; - const { id, username, pubkey } = user; + const { id, username } = user; const confirm = (req.body?.confirm || "").toString().trim().toLowerCase(); if (confirm !== username.toLowerCase()) fail("Type your username to confirm account deletion"); + await requirePin(req); // Allow deletion through dust (often too small to withdraw); only block // a meaningful balance so nobody destroys real funds by accident. Sum - // EVERY account (main + sub-accounts) — the deletion loop below wipes each + // EVERY account (main + sub-accounts) — the sweep wipes each // sub-account's balance:/pending: keys unconditionally, so a guard that // only checked the main account let a user with funds parked in a // sub-account pass and silently destroy them. @@ -909,80 +912,9 @@ export default { if (total > 10000) fail("Withdraw your balance before deleting your account"); - const keys = [ - `user:${id}`, - // username->uid mapping is stored lowercased+despaced (see register.ts); - // include both forms so a mixed-case username can't leave a dangling alias. - `user:${username.toLowerCase().replace(/\s/g, "")}`, - `user:${username}`, - `${id}:payments`, - `${id}:payments:last`, - `${id}:apps`, - `${id}:lastlen`, - `${id}:accounts`, - `${id}:contacts`, - `${id}:invoices`, - `${id}:items`, - `${id}:pins`, - `${id}:subscriptions`, - `${id}:trust`, - `${id}:square`, - `${id}:codeVerifier`, - `account:${id}`, - `balance:${id}`, - `pending:${id}`, - `credit:bitcoin:${id}`, - `credit:lightning:${id}`, - `credit:liquid:${id}`, - ]; - - if (pubkey) { - keys.push( - `user:${pubkey}`, - `${pubkey}:follows`, - `${pubkey}:follows:n`, - `${pubkey}:followers`, - `${pubkey}:followers:n`, - `${pubkey}:pubkeys`, - ); - if (user.nip5) await db.sRem("nip5", `${username}:${pubkey}`); - } - - // Connected NWC apps: drop each app record + its payment index. - // `${id}:apps` is a SET (sAdd in register.ts), so use sMembers, not lRange. - for (const ap of await db.sMembers(`${id}:apps`)) - keys.push(`app:${ap}`, `${ap}:payments`); - - // Sub-accounts and their payment records. - for (const aid of await db.lRange(`${id}:accounts`, 0, -1)) { - if (aid === id) continue; - for (const pid of await db.lRange(`${aid}:payments`, 0, -1)) - keys.push(`payment:${pid}`); - keys.push( - `account:${aid}`, - `${aid}:payments`, - `${aid}:payments:last`, - `${aid}:invoices`, - `balance:${aid}`, - `pending:${aid}`, - ); - } - - // Main-account payment + invoice records. - for (const pid of await db.lRange(`${id}:payments`, 0, -1)) - keys.push(`payment:${pid}`); - for (const iid of await db.lRange(`${id}:invoices`, 0, -1)) - keys.push(`invoice:${iid}`); - - let deleted = 0; - for (const k of keys) { - if (await db.exists(k)) { - await db.del(k); - deleted++; - } - } + const { deleted, disabled } = await deleteUser(user); - l("user self-deleted", username, id, `${deleted} keys`); + l("user self-deleted", username, id, `${deleted} keys, ${disabled} offers`); try { res.clearCookie("token"); } catch {} |
