refactor(api): collapse /admin/account and /player/account onto /auth/me/account
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 26s
PR Checks / client-build (pull_request) Successful in 27s
PR Checks / server-tests (pull_request) Successful in 10m32s

Self-service account security had three URL surfaces onto one controller. All
three mounted the same `admin/account.controller.js` handlers; each of the three
router files carried a header comment apologising for the arrangement.

`/auth/me/account` was already a strict superset, which settles which to keep:

  /admin/account   6 routes  noindex, isLoggedIn, staffOnly
  /player/account  8 routes  noindex, requireAuth
  /auth/me/account 10 routes noindex, requireAuth

Neither of the deleted surfaces carried recovery codes, and /admin/account
carried no username or password change at all — so client.js already called
/auth/me/account/recovery-codes/* for two operations on a screen it otherwise
served from /admin/account. The split was leaking before this change.

Gating is equivalent where it overlapped: /player and /auth/me apply identical
`noindex, requireAuth`, and `staffOnly` on /admin/account was strictly narrower
while buying nothing, since every handler is self-scoped to req.user.id. There
is no CSRF layer to differ.

  - 14 routes deleted, 0 added, no handler changed.
  - account.controller.js moves router/v1/admin/ -> router/v1/auth/, beside the
    one router that still reaches it.
  - Web client: 14 call sites move onto a root-level api.myAccount /
    api.changeUsername / ... group, matching the /auth/me methods already there.
  - Android app: no change. MeApi.kt was already 100% /auth/me/account/*.
  - Two swagger tags, `Admin · Account` and `Player`, were declared only by the
    deleted routes and go with them. The orphaned `AccountStatus` schema goes
    too; `PlayerAccount` is re-described as the any-role /auth/me/account shape
    (the name is kept so existing $refs resolve).

Breaking to the published OpenAPI surface, accepted deliberately: both consumers
are in this org, and deprecate-then-delete would leave the next phase deciding
whether to add routes to surfaces already marked for removal.

Verification: routes.manifest.json shows exactly 14 deletions and 0 additions.
The OpenAPI spec loses the same 14 paths with zero surviving path definitions
changed; its large textual diff is pure reordering, because removing the
first-mounted router shifts every later path. 1203 server tests, 288 client
tests, 53 bot tests green; check:modules, check:hosts and routes:manifest
--check all pass.

Design of record: docs/website/ENGAGEMENT.md Phase 1a. This lands ahead of
engagement Phase 1b, which adds a self-service email field — written once here
rather than three times.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-29 00:49:25 -05:00
parent f5aa32e0ed
commit 6e61146678
20 changed files with 89 additions and 1428 deletions

View File

@@ -58,12 +58,10 @@ const doc = {
tags: [
{ name: 'Health', description: 'Liveness probe' },
{ name: 'Auth', description: 'Web session login/logout (cookie + TOTP)' },
{ name: 'Auth · Me', description: 'The signed-in account: profile, notification streams and devices' },
{ name: 'Auth · Me', description: 'The signed-in account: profile, account security (credentials, 2FA, linked identities, recovery codes), notification streams and devices' },
{ name: 'Auth · Mobile', description: 'Native bearer-token login, refresh and logout' },
{ name: 'Auth · SSO', description: 'OAuth2 / OIDC provider discovery and redirect flow' },
{ name: 'Public', description: 'Unauthenticated site content (settings, posts, wiki, contact)' },
{ name: 'Admin · Account', description: 'Self-service account security (2FA, linked identities)' },
{ name: 'Player', description: 'Self-service player accounts (register, credentials, 2FA, linked identities)' },
{ name: 'Player · Appeals', description: 'Player-submitted moderation appeals' },
{ name: 'Settings', description: 'Site-wide settings any authenticated account may read (nav overrides)' },
{ name: 'Admin · Dashboard', description: 'Dashboard summary and site mode' },
@@ -490,13 +488,16 @@ const doc = {
},
},
},
// The self account as GET /auth/me/account returns it, for EVERY role — the
// name predates the collapse of /player/account and /admin/account onto
// /auth/me and is kept so existing $refs and generated clients resolve.
PlayerAccount: {
type: 'object',
description: 'Self-service player account (GET /player/account).',
description: 'The signed-in account (GET /auth/me/account). Same shape for every role.',
properties: {
id: { type: 'integer', example: 42 },
username: { type: 'string', example: 'newplayer' },
role: { type: 'string', enum: ['player'], example: 'player' },
role: { type: 'string', enum: ['admin', 'editor', 'moderator', 'player'], example: 'player' },
email: { type: 'string', format: 'email', nullable: true, example: 'player@example.com' },
status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
totp_enabled: { type: 'boolean', example: false },
@@ -749,16 +750,6 @@ const doc = {
// the affected resource id/slug or a boolean flag. Documented here as-is so
// the spec matches the controllers. (The shapes are intentionally recorded
// rather than normalized — see the audit note if standardizing later.)
AccountStatus: {
type: 'object',
description: 'Self-service account security status (GET /admin/account).',
properties: {
id: { type: 'integer', example: 1 },
username: { type: 'string', example: 'admin' },
role: { type: 'string', enum: ['admin', 'editor'], example: 'admin' },
totp_enabled: { type: 'boolean', example: true },
},
},
TotpSetup: {
type: 'object',
description: 'Enrollment material returned by POST /account/totp/setup.',