feat(account): self-service account deletion, MODULE_API 1.20.0 #232

Merged
whitlocktech merged 1 commits from feature/account-deletion into main 2026-10-29 20:34:55 -05:00
Member

Step 2 of docs/website/ACCOUNT_DELETION.md §6, the core: a member deletes their own account on the website, and the admin route's delete runs the same procedure. Play needs this before the M8 cutover (android/PLAN.md). Docs: RunicGateway/docs PR on docs/account-deletion-core.

The route

DELETE /api/v1/auth/me/account takes { username, password, code?, recoveryCode? }.

  • The typed username must match, ignoring case.
  • The password is always required (AD7). A 2FA or recovery code is required when 2FA is on.
  • Refusals, each with a code:
    • 400: username_mismatch, wrong_password, code_required, wrong_code
    • 409: password_required, last_admin
    • 403: DEMO_LOCKED for the demo's shared logins
  • A wrong password or code is 400, not 401 (AD12, asked today). In the app, a 401 would make TokenAuthenticator refresh and replay the DELETE. Both failures count toward the login backoff and bot score, like a wrong current password does elsewhere.
  • Behind backoffGuard, slowLogin and accountChangeLimiter. Swagger annotations are added and the spec is regenerated.

The procedure (server/src/auth/accountDeletion.js)

  1. Before the delete: every started module's onUserDeleting({ userId, deletedBy }) runs in turn, each with a 10 s limit. A hook that throws or hangs is logged, and the deletion goes ahead (AD10, AD11).
  2. One transaction (a new transaction() in utils/db.js): a staff username goes into the new reserved_usernames table (AD9), and the row is deleted. The foreign keys do the rest.
  3. After commit, fire-and-forget:
    • a Teams reconcile
    • user.self_delete or user.delete in the activity log, naming the id and never the username
    • a notice to the old address, from the new protected template auth.account-deleted

The admin DELETE /admin/users/:id now calls the same service and keeps its 400s.

MODULE_API 1.20.0

api.onUserDeleting(fn) is added on the server, the version is bumped on both halves, and the engagement manifest is regenerated. Module-uo's ^1.10.0 still resolves. With no hook registered, a deletion is just the cascade, as before. The Integration Kit's coreApi check goes red until it's re-pinned, which is intended.

Who sees a deleted account's name (AD5, AD6)

server/src/utils/displayAuthor.js is the one rule:

  • A snapshot with a NULL id is a deleted account.
  • Members, leaders and visitors see "[deleted player]".
  • Admin, editor and moderator see the stored name plus a …Deleted flag. The client shows that as "Ravenna (deleted account)".

It's used by:

  • the forum's threads and posts
  • the Reports queue (reporter, handler, the target's author and uploader)
  • the leader's Forum guests list
  • the Team digest mail
  • the admin Teams ledgers and leader overrides

A deleted account drops off Forum guests (AD13, asked today). Its grant row stays in the staff ledger.

Reserved names (AD9)

users.model refuses a reserved name in createUser and in a username update. isDuplicateUsername reports that refusal as a duplicate. So registration, invite acceptance, SSO provisioning, an admin creating or renaming an account, and a self-service rename all answer it the way they answer a taken name.

Web

  • A Delete my account section at the bottom of the player Account page and the staff Account Security page.
    • It says what goes and what stays. Staff are told their name is reserved.
    • The confirm form asks for the username, the password and the code when 2FA is on. Delete stays disabled until they match.
    • On the demo, the button is disabled for the shared logins.
  • On success, the browser goes home and the site toast says "Your account has been deleted." (lib/notice.js feeds the existing toast).

Checked

  • npm test (server): 2,356 tests, 0 failures. New test files: accountDeletion, displayAuthor, userDeletingHook. Updated: contentReports, demoLocks, emailTemplates, eventModuleContract.
  • Client: npm test 443/443, npm run build passes.
  • routes.manifest.json was regenerated with no modules installed: one route added.
  • Local walk against a throwaway database (acctdel on the dev MariaDB), with no game module:
    • API: 21/21 checks passed.
      • Every refusal code.
      • A deletion on the web, and one over a bearer with 2FA.
      • The other device's cookie and access token got 401 on their next request.
      • A staff name was refused at admin create and at a rename; a player name was free at once.
      • The admin route worked, and the last admin was refused.
      • The DB afterwards: two staff names reserved, every deletion audited with an IP and no username, no orphaned refresh tokens.
    • Browser:
      • Signed in as a player: a wrong password showed its sentence; the right one landed home with the toast; /account then went to sign-in.
      • Signed in as an admin: the section and the staff note render on Account Security.
  • Not walked:
    • the forum's "[deleted player]" (needs a game module for Teams)
    • the demo lock (needs DEMO_MODE)
    • Both are covered by tests and belong to the step-6 walk.

Not in this PR (later steps)

  • Module-uo and Module-Rust hooks (step 3)

  • Android (step 4)

  • runicgateway.com /delete-account/ (step 5)

  • AI-assisted (Claude Code)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Step 2 of `docs/website/ACCOUNT_DELETION.md` §6, the core: a member deletes their own account on the website, and the admin route's delete runs the same procedure. Play needs this before the M8 cutover (`android/PLAN.md`). Docs: RunicGateway/docs PR on `docs/account-deletion-core`. ## The route `DELETE /api/v1/auth/me/account` takes `{ username, password, code?, recoveryCode? }`. - The typed username must match, ignoring case. - The password is always required (AD7). A 2FA or recovery code is required when 2FA is on. - Refusals, each with a `code`: - **400:** `username_mismatch`, `wrong_password`, `code_required`, `wrong_code` - **409:** `password_required`, `last_admin` - **403:** `DEMO_LOCKED` for the demo's shared logins - **A wrong password or code is 400, not 401 (AD12, asked today).** In the app, a 401 would make `TokenAuthenticator` refresh and replay the DELETE. Both failures count toward the login backoff and bot score, like a wrong current password does elsewhere. - Behind `backoffGuard`, `slowLogin` and `accountChangeLimiter`. Swagger annotations are added and the spec is regenerated. ## The procedure (`server/src/auth/accountDeletion.js`) 1. **Before the delete:** every started module's `onUserDeleting({ userId, deletedBy })` runs in turn, each with a 10 s limit. A hook that throws or hangs is logged, and the deletion goes ahead (AD10, AD11). 2. **One transaction** (a new `transaction()` in `utils/db.js`): a staff username goes into the new `reserved_usernames` table (AD9), and the row is deleted. The foreign keys do the rest. 3. **After commit, fire-and-forget:** - a Teams reconcile - `user.self_delete` or `user.delete` in the activity log, naming the id and never the username - a notice to the old address, from the new protected template `auth.account-deleted` The admin `DELETE /admin/users/:id` now calls the same service and keeps its 400s. ## MODULE_API 1.20.0 `api.onUserDeleting(fn)` is added on the server, the version is bumped on both halves, and the engagement manifest is regenerated. Module-uo's `^1.10.0` still resolves. With no hook registered, a deletion is just the cascade, as before. The Integration Kit's `coreApi` check goes red until it's re-pinned, which is intended. ## Who sees a deleted account's name (AD5, AD6) `server/src/utils/displayAuthor.js` is the one rule: - A snapshot with a NULL id is a deleted account. - Members, leaders and visitors see **"[deleted player]"**. - Admin, editor and moderator see the stored name plus a `…Deleted` flag. The client shows that as "Ravenna (deleted account)". It's used by: - the forum's threads and posts - the Reports queue (reporter, handler, the target's author and uploader) - the leader's *Forum guests* list - the Team digest mail - the admin Teams ledgers and leader overrides **A deleted account drops off *Forum guests* (AD13, asked today).** Its grant row stays in the staff ledger. ## Reserved names (AD9) `users.model` refuses a reserved name in `createUser` and in a username `update`. `isDuplicateUsername` reports that refusal as a duplicate. So registration, invite acceptance, SSO provisioning, an admin creating or renaming an account, and a self-service rename all answer it the way they answer a taken name. ## Web - A *Delete my account* section at the bottom of the player Account page and the staff Account Security page. - It says what goes and what stays. Staff are told their name is reserved. - The confirm form asks for the username, the password and the code when 2FA is on. *Delete* stays disabled until they match. - On the demo, the button is disabled for the shared logins. - On success, the browser goes home and the site toast says "Your account has been deleted." (`lib/notice.js` feeds the existing toast). ## Checked - `npm test` (server): 2,356 tests, 0 failures. New test files: `accountDeletion`, `displayAuthor`, `userDeletingHook`. Updated: `contentReports`, `demoLocks`, `emailTemplates`, `eventModuleContract`. - Client: `npm test` 443/443, `npm run build` passes. - `routes.manifest.json` was regenerated with no modules installed: one route added. - **Local walk** against a throwaway database (`acctdel` on the dev MariaDB), with no game module: - **API:** 21/21 checks passed. - Every refusal code. - A deletion on the web, and one over a bearer with 2FA. - The other device's cookie and access token got 401 on their next request. - A staff name was refused at admin create and at a rename; a player name was free at once. - The admin route worked, and the last admin was refused. - The DB afterwards: two staff names reserved, every deletion audited with an IP and no username, no orphaned refresh tokens. - **Browser:** - Signed in as a player: a wrong password showed its sentence; the right one landed home with the toast; `/account` then went to sign-in. - Signed in as an admin: the section and the staff note render on Account Security. - **Not walked:** - the forum's "[deleted player]" (needs a game module for Teams) - the demo lock (needs `DEMO_MODE`) - Both are covered by tests and belong to the step-6 walk. ## Not in this PR (later steps) - Module-uo and Module-Rust hooks (step 3) - Android (step 4) - runicgateway.com `/delete-account/` (step 5) - [x] AI-assisted (Claude Code) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-10-29 18:34:57 -05:00
feat(account): self-service account deletion, MODULE_API 1.20.0
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 37s
PR Checks / client-build (pull_request) Successful in 49s
PR Checks / server-tests (pull_request) Successful in 6m14s
d1ae8f943f
ACCOUNT_DELETION.md phase 2 (core): a member deletes their own account on
the website, and the admin route runs the same procedure.

- DELETE /api/v1/auth/me/account: typed username, password always (AD7),
  and a 2FA or recovery code when 2FA is on. A wrong password or code is
  400, not 401 (AD12), so the app's refresh-on-401 never replays it.
  409 password_required and last_admin; the demo's shared logins are
  locked.
- auth/accountDeletion.js: module hooks first, then one transaction that
  reserves a staff username (reserved_usernames, AD9) and deletes the row,
  then a Teams reconcile, the audit entry (no username) and a notice to
  the old address (new protected template auth.account-deleted).
- MODULE_API 1.20.0: api.onUserDeleting({ userId, deletedBy }), before the
  delete, 10 s limit, cannot veto (AD10, AD11).
- utils/displayAuthor.js: a deleted account's username snapshots read
  "[deleted player]" to members and the stored name, marked, to staff
  (AD5, AD6). Used by the forum, the Reports queue, the guest list and
  the Team digest. A deleted account drops off Forum guests (AD13).
- Reserved names refuse at every username write, answered as "taken".
- Web: Delete my account on the player and staff Account pages; staff
  views mark "(deleted account)".
- OpenAPI regenerated; route manifest and engagement manifest regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech approved these changes 2026-10-29 20:25:27 -05:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-10-29 20:25:33 -05:00
whitlocktech merged commit 7bd93b8bb0 into main 2026-10-29 20:34:55 -05:00
whitlocktech deleted branch feature/account-deletion 2026-10-29 20:34:57 -05:00
Sign in to join this conversation.
No description provided.