feat(account): self-service account deletion, MODULE_API 1.20.0
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
This commit is contained in:
@@ -523,6 +523,15 @@ function buildApi(record) {
|
||||
// never writes a module's tables. Called from the seed script only, never
|
||||
// by the running server, and only with DEMO_MODE on.
|
||||
onDemoSeed: hook('onDemoSeed'),
|
||||
// API 1.20.0 (ACCOUNT_DELETION.md §4.1, AD10, AD11). Core telling the module
|
||||
// an account is about to be deleted, dispatched from auth/accountDeletion.js
|
||||
// BEFORE the row goes, so the module can still read the person's links and
|
||||
// undo them through its own unlink — a cascade is not an unlink, and for UO
|
||||
// the link lives on the shard. It cannot veto: a throw or a hook past core's
|
||||
// time limit is logged and the deletion goes ahead, because a member's
|
||||
// deletion request must be honoured. Whatever the module could not finish is
|
||||
// its own pending work.
|
||||
onUserDeleting: hook('onUserDeleting'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,7 +907,12 @@ function load(tierRouters) {
|
||||
tables: new Set(),
|
||||
called: new Set(),
|
||||
hooks: {
|
||||
onBoot: null, onShutdown: null, onIdentityLinking: null, onIdentityLinked: null, onDemoSeed: null,
|
||||
onBoot: null,
|
||||
onShutdown: null,
|
||||
onIdentityLinking: null,
|
||||
onIdentityLinked: null,
|
||||
onDemoSeed: null,
|
||||
onUserDeleting: null,
|
||||
},
|
||||
client: null,
|
||||
ctx: null,
|
||||
@@ -1007,7 +1021,7 @@ function refuseSecond(id, dir, holder) {
|
||||
staged: null,
|
||||
tables: new Set(),
|
||||
called: new Set(),
|
||||
hooks: { onBoot: null, onShutdown: null, onIdentityLinking: null, onIdentityLinked: null },
|
||||
hooks: { onBoot: null, onShutdown: null, onIdentityLinking: null, onIdentityLinked: null, onUserDeleting: null },
|
||||
client: null,
|
||||
ctx: null,
|
||||
state: 'startup_failed',
|
||||
@@ -1186,6 +1200,23 @@ function identityHooks(name) {
|
||||
.map((r) => ({ id: r.id, hook: r.hooks[name] }))
|
||||
}
|
||||
|
||||
/**
|
||||
* The `onUserDeleting` hooks, in registration order (API 1.20.0,
|
||||
* ACCOUNT_DELETION.md AD10).
|
||||
*
|
||||
* `started` only, like the identity hooks above: a disabled module, or one whose
|
||||
* `onBoot` threw, is not serving and holds no link it could undo now. Empty in a
|
||||
* process that never loaded modules, so a deletion there is core's alone.
|
||||
*
|
||||
* @returns {{id: string, hook: Function}[]}
|
||||
*/
|
||||
function userDeletingHooks() {
|
||||
if (!loaded) return []
|
||||
return [...modules.values()]
|
||||
.filter((r) => r.state === 'started' && r.hooks.onUserDeleting)
|
||||
.map((r) => ({ id: r.id, hook: r.hooks.onUserDeleting }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Every installed module's `demoLocks`, compiled, for the demo lock in
|
||||
* demo/locks.js (API 1.18.0).
|
||||
@@ -1361,6 +1392,7 @@ module.exports = {
|
||||
bootable,
|
||||
shutdownHooks,
|
||||
identityHooks,
|
||||
userDeletingHooks,
|
||||
demoLocks,
|
||||
demoSeedHooks,
|
||||
stopHook,
|
||||
|
||||
Reference in New Issue
Block a user