docs(website): record the account-deletion answers (AD4-AD9)
The org lead answered the plan's six questions on 2026-10-29: - delete at once, no grace period - what a member wrote stays, and members see every snapshot of a deleted account as "[deleted player]" - staff keep the name and the IP for moderation, so the stored rows are untouched and the rule is about who sees what (displayAuthor) - the password is always the proof; SSO sits on top of a real account - a UO game account stays on the shard; no protocol change - a deleted staff username is never available again (reserved_usernames) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Account deletion — a member deletes their own account, on the website and in the app
|
||||
|
||||
**Status:** plan of record, 2026-10-29. Nothing built yet. §5 holds the questions for the org lead;
|
||||
each phase in §6 stops for the org lead before the next one starts.
|
||||
**Status:** plan of record, 2026-10-29, with every question answered (AD4-AD9). Nothing built
|
||||
yet. Each phase in §6 stops for the org lead before the next one starts.
|
||||
|
||||
**Why now.** Google Play's Data safety form asks whether the app lets people create an account, and
|
||||
an app that does must let them delete it, both **in the app** and at a **web address** given to Play
|
||||
@@ -15,13 +15,14 @@ hide (`USER_SAFETY.md`), so it takes the next `MODULE_API` minor.
|
||||
|
||||
| Where | What they see | What happens |
|
||||
|---|---|---|
|
||||
| Website, *Account* (`PlayerAccount.jsx`) | a *Delete my account* section at the bottom: what goes, what stays (§4.1), and a confirm step | the account is deleted (or scheduled, §5 Q1), the member is signed out everywhere, and the site says so |
|
||||
| Website, *Account* (`PlayerAccount.jsx`) | a *Delete my account* section at the bottom: what goes, what stays (§4.1), and a confirm step | the account is deleted at once (AD4), the member is signed out everywhere, and the site says so |
|
||||
| App, *Account* (`AccountScreen.kt`) | the same section and the same confirm step | the same, and the app clears what it holds on the phone, as a sign-out does |
|
||||
| runicgateway.com `/delete-account/` | how to do it on the website or in the app, what is deleted and what is kept, and who to ask when you cannot sign in | nothing; it is the page whose address Play is given (AD2) |
|
||||
|
||||
The confirm step asks for the account's password, and for a two-factor code when the account has
|
||||
two-factor sign-in on. An account with neither is §5 Q4. It also asks the member to type their
|
||||
username, so a mis-tap on a phone cannot delete an account.
|
||||
two-factor sign-in on. It also asks the member to type their username, so a mis-tap on a phone
|
||||
cannot delete an account. Signing in with Google, Discord or Steam is a convenience on top of an
|
||||
account that already has its own password (AD7), so the password is always the proof.
|
||||
|
||||
Every member can delete themself, staff included. The one refusal is the **last admin**, as the
|
||||
admin route already refuses. Then a deployment would be left with no one who can run it.
|
||||
@@ -37,6 +38,26 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
- **AD3 — Before report, block and hide.** It takes `MODULE_API` **1.20.0**, and
|
||||
`USER_SAFETY.md`'s `reportTargets` moves to 1.21.0.
|
||||
|
||||
The org lead's answers to the six questions in §5, the same day:
|
||||
|
||||
- **AD4 — Delete now** (Q1). There is no grace period. The typed username and the password are
|
||||
the safeguard.
|
||||
- **AD5 — What they wrote stays, attributed to "[deleted player]"** (Q2). For members, every
|
||||
username snapshot of a deleted account reads "[deleted player]": their threads, posts and
|
||||
uploads, and the records in gap 2. That label replaces today's "[deleted account]".
|
||||
- **AD6 — Staff keep the name and the IP** (Q3). The snapshots and the activity log's IP
|
||||
addresses stay in the database, and the staff views keep showing them, marked as a deleted
|
||||
account, for moderation. So AD5 is a rule about what members see, not a change to the stored
|
||||
rows.
|
||||
- **AD7 — The password is the proof** (Q4). SSO is a convenience. A member must already have a
|
||||
proper account before they can link a sign-in provider, so every account has its own password,
|
||||
and the confirm step always asks for it. An account that somehow has no password is told to set
|
||||
one first, on the same Account page.
|
||||
- **AD8 — A UO game account stays on the shard** (Q5). Only the link goes. The shard's staff
|
||||
handle the shard account like any other. There is no protocol change.
|
||||
- **AD9 — A deleted staff account's username is never available again** (Q6). It can never be
|
||||
registered, or taken by a username change. A player's username is free at once.
|
||||
|
||||
## 3. What exists today (read 2026-10-29, website `main` 222bcb0)
|
||||
|
||||
- **The admin route.** `DELETE /api/v1/admin/users/:id` refuses your own account and the last admin,
|
||||
@@ -79,13 +100,15 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
- `team_leader_overrides.actor_username`
|
||||
- `team_moderation_requests.requested_username` and `decided_username`
|
||||
3. **The activity log keeps IP addresses.** Rows for the deleted member keep `ip`, and `detail`
|
||||
can carry a username, with only `user_id` cleared (§5 Q3).
|
||||
can carry a username, with only `user_id` cleared. They stay, for staff (AD6).
|
||||
4. **A module cannot learn that a user was deleted.** If a module keeps user data without a
|
||||
foreign key, or has a side effect in the game, it has no hook. The Module API has
|
||||
`onIdentityLinked`, but nothing for deletion.
|
||||
5. **A game account the app created stays on the game server.** For UO the link cascades, but the
|
||||
shard account and its characters remain (§5 Q5).
|
||||
shard account and its characters remain, which is intended (AD8).
|
||||
6. The demo's four accounts (`demo/mode.js`) must not be deletable.
|
||||
7. **Nothing stops a deleted name being reused.** Registration and username changes only check
|
||||
the live `users` table (AD9).
|
||||
|
||||
## 4. Design
|
||||
|
||||
@@ -96,25 +119,27 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
by staff and a deletion by the member do the same thing.
|
||||
- **The member's route.** `DELETE /api/v1/auth/me/account`, behind `requireAuth` and
|
||||
`accountChangeLimiter`.
|
||||
- Body: `{ username, password?, code?, recoveryCode? }`.
|
||||
- Body: `{ username, password, code?, recoveryCode? }`.
|
||||
- The typed `username` must match the account, ignoring case.
|
||||
- The password is checked when the account has one.
|
||||
- The password is always checked (AD7).
|
||||
- A two-factor code or a recovery code is checked when two-factor sign-in is on.
|
||||
- Refusals:
|
||||
- 400 for a username that doesn't match
|
||||
- 401 for a wrong password or code
|
||||
- 409 `password_required` for an account with no password: the screen points at the
|
||||
Account page's set-a-password section, which already exists for that case
|
||||
- 409 for the last admin
|
||||
- the demo's own refusal for a demo account (`demo/mode.js`, the same lock other breaking
|
||||
settings use)
|
||||
- `#swagger.*` annotations; the spec is regenerated.
|
||||
- **The procedure, in order:**
|
||||
1. **Before the delete**, read what is needed afterwards: the email address (for the notice
|
||||
below) and the linked game ids.
|
||||
below), the username and the role.
|
||||
2. **In one transaction:**
|
||||
- clear the "what they wrote" snapshots (gap 2), so their threads, posts and uploads show
|
||||
"[deleted account]"; §5 Q2 decides whether the content itself stays
|
||||
- apply §5 Q3's answer to the "records" snapshots and to their activity-log rows
|
||||
- delete the `users` row, so the foreign keys do the rest
|
||||
- when the role is staff (anything but `player`), add the username to a new
|
||||
`reserved_usernames` table (AD9)
|
||||
- delete the `users` row, so the foreign keys do the rest. The snapshot columns, the
|
||||
content and the activity log are **not** touched (AD5, AD6).
|
||||
3. **After commit**, fire-and-forget, each in its own `try`:
|
||||
- every module's `onUserDeleted` hook (below)
|
||||
- `teams.reconcile({ reason: 'account deleted' })`
|
||||
@@ -126,6 +151,22 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
- **Nothing can stop a deletion once it starts.** A module hook that throws is logged and the
|
||||
deletion still stands, because Play requires the request to be honoured. That is also why the
|
||||
hook runs after the commit rather than before it.
|
||||
- **Who sees the name (AD5, AD6).** A snapshot whose id column is now NULL belongs to a deleted
|
||||
account; nothing else clears those ids. Rendering follows the viewer:
|
||||
- **Members**, Team leaders included, and signed-out visitors see `[deleted player]`. That
|
||||
covers forum threads, posts and uploads, and any record a member can read. The constant in
|
||||
`teamForum.model.js` and `contentReports.model.js` changes from `[deleted account]`, and the
|
||||
"id NULL" case now wins over a present snapshot.
|
||||
- **Staff** (admin, editor, moderator) see the stored username with a *deleted account*
|
||||
marker, in the Reports queue, the forum's moderation views, the user and activity screens,
|
||||
and Team moderation requests. The activity log keeps its IP addresses.
|
||||
- The rule lives in one helper, `displayAuthor(row, viewer)`, which every model that renders a
|
||||
snapshot uses, and its tests cover each table in gap 2.
|
||||
- **`reserved_usernames`** (AD9) holds `username_norm` (the primary key, lowercased), `reason`
|
||||
(`deleted_staff`) and `reserved_at`. Registration, an invite's acceptance, an admin creating an
|
||||
account, and a username change all refuse a reserved name, with the same message as a taken
|
||||
one. There is no release path, because the org lead said "completely unavailable". A player's
|
||||
username is not added and is free at once.
|
||||
- **`MODULE_API` 1.20.0: `api.onUserDeleted(async ({ userId, deletedBy }) => {})`**, where
|
||||
`deletedBy` is `'self' | 'staff'`.
|
||||
- It runs after the row is gone, so a module that keeps data without a foreign key cleans it
|
||||
@@ -139,12 +180,17 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
session, trusted device and push registration; linked Google, Discord, OIDC and Steam sign-ins;
|
||||
game-account links and, in Rust, the permissions and groups the site gave those Steam ids;
|
||||
notification settings and the inbox.
|
||||
- **Stays, without your name:** what you wrote where others read it (§5 Q2), shown as
|
||||
"[deleted account]"; reports you filed and appeals you made, as moderation records; event
|
||||
results, with the score kept and the name removed; the delivery log's hashes and the
|
||||
suppression list (`collection.mjs` already says so).
|
||||
- **Stays for security:** §5 Q3's answer.
|
||||
- **Docs:** `BACKEND_DESIGN.md` (route, procedure, the table above), `MODULE_API.md`, OpenAPI.
|
||||
- **Stays, shown to other members as "[deleted player]":** what you wrote where others read it
|
||||
(AD5), and reports you filed and appeals you made, as moderation records.
|
||||
- **Stays, and staff can still see who it was:** the same records with your username, and the
|
||||
activity log with its IP addresses, for moderation (AD6). Kept until the operator removes
|
||||
them.
|
||||
- **Stays, detached:** event results (the score and the in-game name, no site account), the
|
||||
delivery log's hashes and the suppression list (`collection.mjs` already says so).
|
||||
- **If you were staff:** your username can never be used again (AD9).
|
||||
- **Docs:** `BACKEND_DESIGN.md` (the route, the procedure, `reserved_usernames`, the
|
||||
display rule, the list above), `TEAMS.md` (the forum's deleted-author rule), `MODULE_API.md`,
|
||||
OpenAPI.
|
||||
|
||||
### 4.2 Website client
|
||||
|
||||
@@ -176,9 +222,9 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
- The links and grants themselves already cascade.
|
||||
- Core-ref pin.
|
||||
- **Module-uo:**
|
||||
- `shard_account_links` already cascades.
|
||||
- `onUserDeleted` does whatever §5 Q5 decides for the shard account.
|
||||
- Core-ref pin.
|
||||
- `shard_account_links` already cascades, and the shard account stays (AD8), so the module
|
||||
registers no hook.
|
||||
- Core-ref pin only, if its `coreApi` range needs it.
|
||||
- **Integration Kit:** the re-pin, and a paragraph in the module chapter: what `onUserDeleted` is
|
||||
for, and that a module keeping user data with no foreign key must use it.
|
||||
|
||||
@@ -187,7 +233,8 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
- **`/delete-account/`**, the page Play is given (AD2). It covers:
|
||||
- the app's name and ours
|
||||
- the steps on the website and in the app
|
||||
- what is deleted and what is kept, and for how long
|
||||
- what is deleted and what is kept, and for how long: the moderation records that stay, and
|
||||
that other members see "[deleted player]"
|
||||
- that we hold no account, because each community runs its own site
|
||||
- what to do if you cannot sign in: ask that community's operator, who can delete the account
|
||||
from the admin panel
|
||||
@@ -195,65 +242,25 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
`/privacy` and the page agree. `PLAY_DATA_SAFETY.md`'s account-deletion bullet then names the URL
|
||||
and answers "yes" to "can users request deletion".
|
||||
|
||||
## 5. Questions for the org lead
|
||||
## 5. The questions, as they were asked and answered (org lead, 2026-10-29)
|
||||
|
||||
- **Q1 — Delete now, or after a grace period?**
|
||||
- *Example:* a player taps *Delete* at 2 a.m. after a bad raid.
|
||||
- **Now:** the account is gone, and the username is free to register by morning.
|
||||
- **Grace period (14 days):** the account is locked at once and signs them out everywhere.
|
||||
Signing in during the 14 days cancels the deletion, and a nightly sweep deletes it when the
|
||||
period ends. That needs a `pending_deletion` status and a sweep.
|
||||
- *Recommended: now.* It is simpler, it is what the confirm step is for, and Play accepts either.
|
||||
- **Q2 — What happens to what they wrote?**
|
||||
- *Example:* a member wrote a forum guide with 40 replies under it.
|
||||
- **Keep it, shown as "[deleted account]":** the thread still reads, with no name on it.
|
||||
- **Delete their posts and uploads:** the guide is gone, and the replies answer nothing.
|
||||
- *Recommended: keep it, anonymous.* Most forums work this way, and the screens say so before
|
||||
the member confirms.
|
||||
- **Q3 — Records of what was done: the activity log's IP addresses, and the moderation
|
||||
snapshots.**
|
||||
- *Example:* a banned player deletes their account and comes back as someone new. Or a Team
|
||||
leader who hid a post deletes their account, and staff later ask who hid it.
|
||||
- **Keep them as they are:** moderators can still match the old IPs, and a record still says
|
||||
who filed the report or hid the post. Those are security and moderation records, and the
|
||||
privacy notes say so.
|
||||
- **Clear them:** clear `ip` and the username in `detail` in the activity log, and the
|
||||
"records" snapshots in gap 2. Nothing can be matched any more, and those records then read
|
||||
"[deleted account]".
|
||||
- *Recommended: keep them,* as security and moderation records kept until the operator removes
|
||||
them, which is what `collection.mjs` already says about the audit log.
|
||||
- **Q4 — How does an account with no password confirm?**
|
||||
- *Example:* a player who only ever signs in with Discord, with no two-factor sign-in.
|
||||
- **Typing the username on a signed-in session is enough:** simple, but a person holding their
|
||||
unlocked phone can do it.
|
||||
- **Sign in with the provider again first** (within the last 10 minutes): safer, but it needs a
|
||||
re-authentication flow on the web and in the app.
|
||||
- *Recommended: sign in again,* reusing the SSO flow with a `reauth` purpose.
|
||||
- **Q5 — The UO game account the app can create.**
|
||||
- *Example:* a player made `Thorne` on the shard from the app and has three characters on it.
|
||||
- **Leave it on the shard:** the link goes, and the shard account and characters stay. The
|
||||
shard's staff deal with it as they would any account.
|
||||
- **Offer to delete it too:** the confirm step adds "Also delete game account Thorne and its
|
||||
characters". That needs a new bridge command, `account.delete`, and a protocol bump in
|
||||
servuo-plugins, link and module-uo.
|
||||
- *Recommended: leave it.* Play's rule concerns the account a user signs in to the app with,
|
||||
which is the site account, and a shard account belongs to the shard. If Play's review asks,
|
||||
the second option can follow.
|
||||
- **Q6 — Can someone else take the username straight away?**
|
||||
- *Example:* a moderator called `Ravenna` deletes their account, and the next day someone
|
||||
registers `Ravenna` and messages players as staff.
|
||||
- **Free at once:** simple.
|
||||
- **Reserved for 90 days:** a small `reserved_usernames` table that registration and username
|
||||
changes check.
|
||||
- *Recommended: reserve for 90 days*, for staff accounts only.
|
||||
| | Asked, with the example given | Answer |
|
||||
|---|---|---|
|
||||
| Q1 | Delete now, or lock for 14 days and let a sign-in cancel it? (a player taps *Delete* at 2 a.m. after a bad raid) | **Delete now** (AD4) |
|
||||
| Q2 | Keep what they wrote, anonymously, or delete it? (a forum guide with 40 replies) | **Keep it; every username snapshot reads "[deleted player]"** (AD5) |
|
||||
| Q3 | Keep or clear the activity log's IPs and the moderation snapshots? (a banned player returns under a new name) | **Keep the name and the IP on the admin side, for moderation** (AD6) |
|
||||
| Q4 | How does an account with no password confirm? (a Discord-only player) | **Every account already has a password; SSO is a convenience on top of a proper account** (AD7) |
|
||||
| Q5 | What happens to a UO game account the app created? (`Thorne`, with three characters) | **Leave it on the shard** (AD8) |
|
||||
| Q6 | Can a deleted username be taken at once? (a moderator `Ravenna` impersonated the next day) | **Staff names are completely unavailable** (AD9) |
|
||||
|
||||
## 6. Build order
|
||||
|
||||
1. **This plan** (docs), with §5's answers.
|
||||
1. **This plan** (docs), with the answers (AD4-AD9).
|
||||
2. **Core** (`website`): §4.1 and §4.2, `MODULE_API` 1.20.0, tests (the service's procedure, the
|
||||
route's refusals, the demo lock, a module hook that throws), OpenAPI, `BACKEND_DESIGN.md`,
|
||||
`MODULE_API.md`. One PR.
|
||||
3. **Module-Rust** (`edge`) and **Module-uo**: §4.4 plus their core-ref pins, one PR each.
|
||||
route's refusals, the demo lock, a module hook that throws, `displayAuthor` per table, a
|
||||
reserved staff name), OpenAPI, `BACKEND_DESIGN.md`, `TEAMS.md`, `MODULE_API.md`. One PR.
|
||||
3. **Module-Rust** (`edge`): §4.4's hook plus its core-ref pin. One PR. **Module-uo** gets a
|
||||
pin only if its `coreApi` range needs one.
|
||||
**Integration Kit**: the re-pin and the paragraph, one PR.
|
||||
4. **Android** (`edge`): §4.3. One PR.
|
||||
5. **runicgateway.com**: §4.5, plus `PLAY_DATA_SAFETY.md` regenerated. One PR.
|
||||
@@ -261,7 +268,9 @@ admin route already refuses. Then a deployment would be left with no one who can
|
||||
- delete on the website, and in the app on the emulator
|
||||
- the other device's session dies on its next request
|
||||
- a Rust-linked account's grants leave the rig on the next sync
|
||||
- a forum post shows "[deleted account]"
|
||||
- a forum post shows "[deleted player]" to a member and the old username, marked deleted, to
|
||||
staff
|
||||
- the deleted staff username is refused at registration and at a username change
|
||||
- the demo refuses
|
||||
- the last admin is refused
|
||||
7. Then `USER_SAFETY.md` (report, block and hide, now 1.21.0), then `android/PLAN.md` M8's
|
||||
|
||||
Reference in New Issue
Block a user