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:
@@ -40,6 +40,37 @@ async function query(sql, params) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `fn(conn)` inside one transaction on one connection: committed when it
|
||||
* resolves, rolled back when it throws (and the error rethrown).
|
||||
*
|
||||
* Every statement that must stand or fall together goes through the `conn` it is
|
||||
* handed, never through `query()` above, which takes a different connection from
|
||||
* the pool and so sits outside the transaction.
|
||||
*
|
||||
* @template T
|
||||
* @param {(conn: import('mariadb').PoolConnection) => Promise<T>} fn
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async function transaction(fn) {
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
await conn.beginTransaction()
|
||||
const out = await fn(conn)
|
||||
await conn.commit()
|
||||
return out
|
||||
} catch (err) {
|
||||
try {
|
||||
await conn.rollback()
|
||||
} catch (rollbackErr) {
|
||||
log.error('rollback failed', { message: rollbackErr.message })
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
conn.release()
|
||||
}
|
||||
}
|
||||
|
||||
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'db', 'schema.sql')
|
||||
|
||||
/**
|
||||
@@ -106,4 +137,4 @@ async function close() {
|
||||
await pool.end()
|
||||
}
|
||||
|
||||
module.exports = { pool, query, ensureSchema, close }
|
||||
module.exports = { pool, query, transaction, ensureSchema, close }
|
||||
|
||||
Reference in New Issue
Block a user