docs(website): one self-service surface — /auth/me/account

Companion to RunicGateway/website's collapse of /admin/account (6 routes) and
/player/account (8 routes) onto /auth/me/account, which was already a strict
superset of both.

  - BACKEND_DESIGN.md: the two router-tree entries go; the /auth/me prose is
    rewritten from "additive, the older routes stay for web back-compat" to the
    single surface it now is, recording why /auth/me was the one to keep and
    that gating was equivalent. account.controller.js moved to router/v1/auth/.
    The /player-group paragraph loses account.router.js from its mount list.
  - ENGAGEMENT.md: new Phase 1a records the collapse as built, and §0.6 finding 4
    is corrected — it named router/v1/player/account.router.js, which is gone.
    Phase 1b's change-and-verify flow now lands on /auth/me/account and nowhere
    else, which was the reason to do this first: a self-service field would
    otherwise have been written three times, in 1b and again in Phase 3.
  - android/PLAN.md §6.4/§8: the "routes stay for web back-compat" note is now
    false. The app needed no change — MeApi.kt was already 100% /auth/me/*.
  - API_V2_PLAN.md: a forward pointer only. Its router inventories are a record
    of the domain split as it landed and are deliberately left as written.

api-route-inventory.json is regenerated wholesale, not partially updated. It is
a generated mirror of server/routes.manifest.json with no CI gate, and it had
drifted on its own: 168 entries against the manifest's 203, missing every Teams
route and still listing the two Gmail connect routes Phase 1 deleted. It is now
byte-identical to the manifest. A gate for it is flagged in ENGAGEMENT.md
Phase 1a as later work — a mirror nothing checks will drift again.

PROJECT_TREE.md is deliberately untouched: it is auto-generated by the
sync-project-tree workflow in the website repo, which regenerates it from
tracked files on main.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-29 00:49:40 -05:00
parent cde362e588
commit 35abf61277
5 changed files with 308 additions and 95 deletions

View File

@@ -87,7 +87,15 @@ server/
/me sub-routers below also match the
bare /me and supply its noindex header
me.routes.js (23) /auth/me/account*, sessions, trusted
devices — router-level requireAuth
devices — router-level requireAuth.
The ONLY self-service account surface
(see below); account.controller.js
sits beside it and is reached from
nowhere else
account.controller.js the self-service handlers: username,
password, TOTP, identities, device
sessions, trusted devices, recovery
codes
notifications.routes.js (3) /auth/me/devices*, notifications/*
mobile.routes.js + /auth/mobile/* — native bearer login
mobileSso.routes.js (5)
@@ -121,9 +129,6 @@ server/
player/ index.js owns the shared `noindex, requireAuth` gate
(authenticated, ANY role — staff are a superset
of players) and the mount table
account.router.js (8) /player/account — credentials, TOTP,
linked identities; handlers shared
with /admin/account and /auth/me
appeals.router.js (4) /player/appeals
appeals.controller.js
(/player/shard is module-uo's)
@@ -145,7 +150,6 @@ server/
own prefixes; owns the shared
`noindex, isLoggedIn, staffOnly` gate and
declares no routes itself
account.router.js (6) /admin/account — self-service, no adminOnly
users.router.js (9) /admin/users — adminOnly. The six
/users/:id/shard/* routes are a
MODULE's, reached through the
@@ -786,21 +790,29 @@ their own router level, and `/sso/:provider/link` carries `requireAuth` per rout
| GET · PUT | `/me/notifications/subscriptions` | cookie / bearer | `{streams:[id]}` on PUT | get / replace own opted-in streams (unknown ids dropped) |
| GET · PUT | `/me/notifications/teams` | cookie / bearer | `{teams:[{teamId,muted,emailMode}]}` on PUT | get / replace own **per-Team** preferences (phase 6, [`TEAMS.md`](TEAMS.md) §6.3). One entry per Team the caller could be notified about — active membership or an active forum grant — plus any Team they already hold a preference for; server-side defaults applied. An entry naming a Team the caller has no access to is **dropped, not refused**: a Team left between loading the screen and saving it is a race, not a client bug. The array is required even when empty (`../android/PLAN.md` §11) |
**Role-agnostic self-service (`/auth/me/*`).** The canonical "me" surface for **every** authenticated
role. It reuses the exact `account.controller` handlers as `/player/account/*` and `/admin/account/*`
(no logic duplication) behind `requireAuth` **only** — any active account, never a specific role. This
lets a client (the Android app) manage its own account through one surface without ever touching
`/admin` (docs/android/PLAN.md §6.4). The older `/player/account/*` + `/admin/account/*` routes stay
for web back-compat.
**Role-agnostic self-service (`/auth/me/*`).** The **only** self-service account surface, for every
authenticated role, behind `requireAuth` **only** — any active account, never a specific role. A
client (the Android app) manages its own account through it without ever touching `/admin`
(docs/android/PLAN.md §6.4).
It used to be the third of three URL surfaces onto `account.controller`, beside `/player/account/*`
and `/admin/account/*`. **Those 14 routes were deleted.** Both were strictly *smaller* than this one —
neither carried recovery codes, and `/admin/account` carried no username or password change — so the
web client already reached in here for part of a single screen. Gating was equivalent where it
overlapped (`/player` and `/auth/me` are byte-identical `noindex, requireAuth`; `staffOnly` on
`/admin/account` was strictly narrower and bought nothing, since every handler is self-scoped to
`req.user.id`). The controller moved to `router/v1/auth/account.controller.js` beside its one
remaining router. **New self-service fields go here and only here.**
**The `/player/*` group is self-service, not player-only.** Staff are a **superset** of players — every
player ability plus their staff tools on top — so the whole group (`account.router.js`,
`appeals.router.js`, mounted by `player/index.js`, plus whatever a module mounts here) sits behind
player ability plus their staff tools on top — so the whole group (`appeals.router.js`, mounted by
`player/index.js`, plus whatever a module mounts here) sits behind
the shared `noindex, requireAuth` gate **only**, never `requireRole('player')`. Every handler is self-scoped to the caller by `req.user.id`, so an admin/editor/
moderator using it sees only their **own** linked accounts and characters (with the pre-existing
`isAdmin` bypass still letting a genuine admin read *any* character). `module-uo` inherits the rule
and relies on it: its `/player/shard/*` handlers are the identical self-scoped ones it also serves
under `/admin/shard/*`, so the two are interchangeable. This is why a staff account with linked game characters gets its "My characters" and
under `/admin/shard/*`, so the two are interchangeable. (Core no longer does this for account
security — see `/auth/me/*` above — but the rule the module depends on is unchanged.) This is why a staff account with linked game characters gets its "My characters" and
personal notification streams on the mobile client — the group no longer 403s a non-`player` role.
`teams.router.js` joins the group in Teams phase 2, and relies on exactly that rule: a moderator is in