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:
@@ -12861,6 +12861,102 @@
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Auth · Me"
|
||||
],
|
||||
"summary": "Delete the current account (self, any role)",
|
||||
"description": "Deletes the account at once (no grace period). Requires the typed username (case-insensitive), the account password, and — when two-factor sign-in is on — an authenticator code or a recovery code. Before the row goes, every installed game module unlinks the account’s game links through its own unlink (MODULE_API 1.20.0 onUserDeleting); a module that cannot finish never stops the deletion. Every session, trusted device, push registration, linked sign-in and game link goes with the account. What the member wrote stays and other members see it as \"[deleted player]\"; staff keep the name and the activity log for moderation. A deleted staff account’s username can never be registered again. A wrong password or code is a 400 (not 401), so a native client’s refresh-on-401 never replays it. Refused with 403 DEMO_LOCKED for the demo’s shared logins.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Deleted; this browser’s session and trust cookies are cleared",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deleted": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "username_mismatch, wrong_password, code_required or wrong_code",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteAccountRefusal"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Not authenticated",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Demo login (DEMO_LOCKED)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DemoLocked"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "password_required (the account has no password yet) or last_admin",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteAccountRefusal"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"429": {
|
||||
"description": "Too many attempts (rate limited)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"cookieAuth": []
|
||||
},
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeleteAccountRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/me/account/email": {
|
||||
@@ -21590,6 +21686,148 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeleteAccountRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "object"
|
||||
},
|
||||
"required": {
|
||||
"type": "array",
|
||||
"example": [
|
||||
"username",
|
||||
"password"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "The confirm step of deleting your own account (ACCOUNT_DELETION.md §4.1). The password is always required (AD7); `code` or `recoveryCode` only when two-factor sign-in is on."
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"example": {
|
||||
"type": "string",
|
||||
"example": "wanderer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "The account's username, typed to confirm; compared ignoring case."
|
||||
}
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"example": "password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"code": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"example": {
|
||||
"type": "string",
|
||||
"example": "123456"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "A current authenticator code."
|
||||
}
|
||||
}
|
||||
},
|
||||
"recoveryCode": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"example": {
|
||||
"type": "string",
|
||||
"example": "ABCD-EFGH"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "A single-use recovery code, instead of `code`."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeleteAccountRefusal": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "object"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"enum": {
|
||||
"type": "array",
|
||||
"example": [
|
||||
"username_mismatch",
|
||||
"wrong_password",
|
||||
"code_required",
|
||||
"wrong_code",
|
||||
"password_required",
|
||||
"last_admin"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "string"
|
||||
},
|
||||
"example": {
|
||||
"type": "string",
|
||||
"example": "Your password is incorrect."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ChangePasswordRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23811,7 +24049,7 @@
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "Username snapshot; \"[deleted account]\" once the account is gone."
|
||||
"example": "Username snapshot. Staff see it even after the account is deleted, with `reporterDeleted` true; anyone else would see \"[deleted player]\" (ACCOUNT_DELETION.md AD5, AD6)."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -23918,6 +24156,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"handledByDeleted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "boolean"
|
||||
},
|
||||
"example": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"handledNote": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -24124,6 +24375,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"authorDeleted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "boolean"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"example": "The author's account was deleted; `author` is the stored name, shown to staff (ACCOUNT_DELETION.md AD6)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"uploader": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -24137,6 +24405,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"uploaderDeleted": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "boolean"
|
||||
},
|
||||
"nullable": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"excerpt": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -493,6 +493,27 @@ const doc = {
|
||||
username: { type: 'string', minLength: 3, maxLength: 32, example: 'newname' },
|
||||
},
|
||||
},
|
||||
DeleteAccountRequest: {
|
||||
type: 'object',
|
||||
required: ['username', 'password'],
|
||||
description: 'The confirm step of deleting your own account (ACCOUNT_DELETION.md §4.1). The password is always required (AD7); `code` or `recoveryCode` only when two-factor sign-in is on.',
|
||||
properties: {
|
||||
username: { type: 'string', example: 'wanderer', description: 'The account\'s username, typed to confirm; compared ignoring case.' },
|
||||
password: { type: 'string', format: 'password' },
|
||||
code: { type: 'string', example: '123456', description: 'A current authenticator code.' },
|
||||
recoveryCode: { type: 'string', example: 'ABCD-EFGH', description: 'A single-use recovery code, instead of `code`.' },
|
||||
},
|
||||
},
|
||||
DeleteAccountRefusal: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
enum: ['username_mismatch', 'wrong_password', 'code_required', 'wrong_code', 'password_required', 'last_admin'],
|
||||
},
|
||||
message: { type: 'string', example: 'Your password is incorrect.' },
|
||||
},
|
||||
},
|
||||
ChangePasswordRequest: {
|
||||
type: 'object',
|
||||
required: ['newPassword'],
|
||||
@@ -844,12 +865,13 @@ const doc = {
|
||||
targetType: { type: 'string', example: 'team_forum_post', description: 'team_forum_thread | team_forum_post | team_forum_upload' },
|
||||
targetId: { type: 'integer', example: 812 },
|
||||
teamId: { type: 'integer', nullable: true, example: 7, description: 'Denormalised so the queue can filter by Team.' },
|
||||
reporter: { type: 'string', example: 'wanderer', description: 'Username snapshot; "[deleted account]" once the account is gone.' },
|
||||
reporter: { type: 'string', example: 'wanderer', description: 'Username snapshot. Staff see it even after the account is deleted, with `reporterDeleted` true; anyone else would see "[deleted player]" (ACCOUNT_DELETION.md AD5, AD6).' },
|
||||
reporterDeleted: { type: 'boolean', example: false },
|
||||
reason: { type: 'string', enum: ['spam', 'abuse', 'sexual', 'illegal', 'impersonation', 'other'], example: 'abuse' },
|
||||
detail: { type: 'string', nullable: true, maxLength: 500, example: 'Personal attacks in the third paragraph.' },
|
||||
status: { type: 'string', enum: ['open', 'reviewing', 'actioned', 'dismissed'], example: 'open' },
|
||||
handledBy: { type: 'string', nullable: true, example: 'moderator1' },
|
||||
handledByDeleted: { type: 'boolean', example: false },
|
||||
handledNote: { type: 'string', nullable: true, example: 'Post hidden, author warned.' },
|
||||
handledAt: { type: 'string', format: 'date-time', nullable: true },
|
||||
createdAt: { type: 'string', format: 'date-time' },
|
||||
@@ -869,7 +891,9 @@ const doc = {
|
||||
title: { type: 'string', nullable: true },
|
||||
type: { type: 'string', nullable: true, enum: ['announcement', 'discussion'] },
|
||||
author: { type: 'string', nullable: true, example: 'someone' },
|
||||
authorDeleted: { type: 'boolean', nullable: true, description: 'The author\'s account was deleted; `author` is the stored name, shown to staff (ACCOUNT_DELETION.md AD6).' },
|
||||
uploader: { type: 'string', nullable: true },
|
||||
uploaderDeleted: { type: 'boolean', nullable: true },
|
||||
excerpt: { type: 'string', nullable: true, description: 'Plain-text excerpt of the post body, capped at 300 characters.' },
|
||||
status: { type: 'string', nullable: true, enum: ['visible', 'hidden', 'deleted'] },
|
||||
filename: { type: 'string', nullable: true },
|
||||
|
||||
Reference in New Issue
Block a user