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
70 lines
3.3 KiB
JavaScript
70 lines
3.3 KiB
JavaScript
// What the Account page's "Delete my account" section says and decides
|
|
// (docs/website/ACCOUNT_DELETION.md §4.1, §4.2). Plain JS with tests, so the
|
|
// words and the confirm rule can be checked without a browser.
|
|
//
|
|
// The server is the guard: it checks the username, the password and the code
|
|
// again whatever this file decides. What is here only keeps a mis-tap from
|
|
// reaching it and turns its refusal codes into sentences.
|
|
|
|
/** What goes with the account. The same list as BACKEND_DESIGN.md's. */
|
|
export const WHAT_GOES = [
|
|
'your sign-in, password, email address, two-factor settings and recovery codes',
|
|
'every signed-in device, trusted device and push registration',
|
|
'your linked sign-ins, such as Google, Discord or Steam',
|
|
'your game-account links, and anything this site granted in the game through them',
|
|
'your notification settings and inbox',
|
|
]
|
|
|
|
/** What stays, and who sees what of it. */
|
|
export const WHAT_STAYS = [
|
|
'What you wrote where others read it, such as forum posts, stays. Other members see it as written by “[deleted player]”.',
|
|
'Reports you filed and appeals you made stay as moderation records. Staff can still see who it was, and the activity log keeps its IP addresses.',
|
|
'Event results stay with the score and the in-game name, with no site account behind them.',
|
|
'A game account stays on the game server. Only its link to this site goes.',
|
|
]
|
|
|
|
/** Said only to staff: their name is never free again (AD9). */
|
|
export const STAFF_NAME_NOTE =
|
|
'You are staff, so your username can never be used again, by anyone, once this account is deleted.'
|
|
|
|
/** True for every role but player. */
|
|
export const isStaffRole = (role) => Boolean(role) && role !== 'player'
|
|
|
|
/** The confirm button is live once the typed name matches, ignoring case. */
|
|
export function usernameMatches(typed, username) {
|
|
return typeof typed === 'string'
|
|
&& typeof username === 'string'
|
|
&& typed.trim().length > 0
|
|
&& typed.trim().toLowerCase() === username.toLowerCase()
|
|
}
|
|
|
|
/**
|
|
* True when the signed-in account is one of the demo's shared logins, which the
|
|
* server refuses to delete. The section still shows (Play's reviewer must find
|
|
* it); only its button is off.
|
|
*/
|
|
export function isSharedDemoLogin(settings, username) {
|
|
const logins = settings?.demo?.logins
|
|
if (!Array.isArray(logins) || typeof username !== 'string') return false
|
|
const name = username.toLowerCase()
|
|
return logins.some((l) => typeof l?.username === 'string' && l.username.toLowerCase() === name)
|
|
}
|
|
|
|
const BY_CODE = {
|
|
username_mismatch: 'Type your username exactly as it is shown.',
|
|
wrong_password: 'Your password is incorrect.',
|
|
code_required: 'Enter a code from your authenticator app, or a recovery code.',
|
|
wrong_code: 'That code is not valid. Try again.',
|
|
password_required: 'This account has no password yet. Set one in the Password section above, then delete the account.',
|
|
last_admin: 'You are the only admin. Make someone else an admin before you delete your account.',
|
|
DEMO_LOCKED: 'This is turned off on the demo site.',
|
|
}
|
|
|
|
/** A refusal from DELETE /auth/me/account, as one sentence. */
|
|
export function refusalMessage(err) {
|
|
const code = err?.body?.code
|
|
if (code && BY_CODE[code]) return BY_CODE[code]
|
|
if (err?.status === 429) return 'Too many attempts. Wait a few minutes and try again.'
|
|
return err?.message || 'Could not delete your account. Try again.'
|
|
}
|