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:
@@ -65,6 +65,22 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
UNIQUE KEY uq_users_email_norm (email_norm)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Usernames that can never be taken again (ACCOUNT_DELETION.md AD9). A deleted
|
||||
-- STAFF account's name lands here in the same transaction that deletes the row,
|
||||
-- so nobody can register "Ravenna" the day after the moderator Ravenna left and
|
||||
-- speak with her name. A player's name is never added and is free at once.
|
||||
--
|
||||
-- There is no release path, by decision ("completely unavailable"). The column
|
||||
-- carries the users.username collation so "reserved" means exactly what "taken"
|
||||
-- means: the same case folding the UNIQUE index above applies. Registration, an
|
||||
-- invite's acceptance, an admin creating or renaming an account and a username
|
||||
-- change all refuse a reserved name with the answer they give a taken one.
|
||||
CREATE TABLE IF NOT EXISTS reserved_usernames (
|
||||
username_norm VARCHAR(32) NOT NULL COLLATE utf8mb4_general_ci PRIMARY KEY,
|
||||
reason ENUM('deleted_staff') NOT NULL,
|
||||
reserved_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
category ENUM('news','five_on_friday','newsletter','screenshot') NOT NULL,
|
||||
@@ -1153,7 +1169,7 @@ CREATE TABLE IF NOT EXISTS team_forum_posts (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
thread_id INT NOT NULL,
|
||||
author_user_id INT NULL,
|
||||
author_username VARCHAR(32) NULL, -- snapshot; renders as "[deleted account]" when both are gone
|
||||
author_username VARCHAR(32) NULL, -- snapshot; with the id NULL it is a deleted account: "[deleted player]" to members, the name to staff (utils/displayAuthor.js)
|
||||
body_html MEDIUMTEXT NOT NULL, -- sanitised on write via utils/forumHtml.js
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
edited_at DATETIME NULL,
|
||||
|
||||
Reference in New Issue
Block a user