docs(website): record the PR 2 admin router split (moderation, bot-activity, activity) #54

Merged
whitlocktech merged 1 commits from docs/admin-router-split-2 into main 2026-07-28 00:11:17 +00:00
2 changed files with 51 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
# Website API — router domain split + CSP hardening # Website API — router domain split + CSP hardening
Status: **in progress** — PR 0 (route manifest), CSP report-only, and split PR 1 of 5 have landed · Status: **in progress** — PR 0 (route manifest), CSP report-only, and split PRs 12 of 5 have landed ·
Target repo: `website/` · Docs owner: this file + `BACKEND_DESIGN.md` Target repo: `website/` · Docs owner: this file + `BACKEND_DESIGN.md`
> **This file replaces the earlier "API v2" plan** (auth merge → CSP → domain split, with a parallel > **This file replaces the earlier "API v2" plan** (auth merge → CSP → domain split, with a parallel
@@ -273,7 +273,8 @@ router/v1/
posts.router.js pages.router.js wiki.router.js posts.router.js pages.router.js wiki.router.js
uploads.router.js shard.router.js uoLink.router.js uploads.router.js shard.router.js uoLink.router.js
email.router.js discordBot.router.js settings.router.js email.router.js discordBot.router.js settings.router.js
dashboard.router.js # + the /activity and /site-mode singletons activity.router.js # the staff audit log — landed in PR 2, not with dashboard
dashboard.router.js # + the /site-mode singleton
auth/ auth/
login.router.js register.router.js password.router.js invite.router.js login.router.js register.router.js password.router.js invite.router.js
sso.router.js mobile.router.js me.routes.js (already split, 23 routes) sso.router.js mobile.router.js me.routes.js (already split, 23 routes)
@@ -341,6 +342,40 @@ Three findings worth carrying into PRs 25:
The OpenAPI spec was also byte-for-byte unchanged, which required a prerequisite fix — see below. The OpenAPI spec was also byte-for-byte unchanged, which required a prerequisite fix — see below.
### PR 2 — as landed
`moderation`, `bot-activity` and `activity` — 18 routes, leaving 64 in the residual file.
| Router | Routes | Prefix | Extra gate |
|---|---|---|---|
| `moderation.router.js` | 15 | `/admin/moderation` | `modAccess` (admin + moderator) at router level |
| `botActivity.router.js` | 2 | `/admin/bot-activity` | `adminOnly` per route |
| `activity.router.js` | 1 | `/admin/activity` | none — staff-wide audit log |
| `admin.routes.js` (residual) | 64 | group root, mounted last | unchanged |
All four gates came back zero-diff: `routes.manifest.json` (200 public + 2 internal),
`routes.guards.json`, `swagger-output.json` (198 operations), and `docs/website/api-route-inventory.json`
was already in sync. 434 server tests green.
Notes:
- **`/activity` gets its own file, deviating from the target tree above**, which parked it as a
singleton inside `dashboard.router.js`. It is folded into PR 2 by the sequencing list, and PR 4 is
where `dashboard` lands — so honouring the tree would have meant leaving one route in the residual
file for two PRs to satisfy a filename. It is also a genuinely separate capability: `/activity` is
the **staff audit log** (`activity.model.js`), while `/dashboard` is a stats overview and
`/bot-activity` is the botScore middleware's in-memory ban state. Three different things that read
alike. **PR 4 mounts `dashboard` and `site-mode` only.**
- **`modAccess` moved to a router-level `use`; `adminOnly` on bot-activity deliberately did not.**
Moderation was already gated by a *prefix* mount (`adminRouter.use('/moderation', modAccess)`), so
`moderationRouter.use(modAccess)` is the exact equivalent (the PR 1 `users` case). Bot-activity's
gate was per-route, and keeping it per-route is what holds the per-route handler count — the one
number in `routes.guards.json` that would catch a dropped `adminOnly`, since `requireRole(...)`
returns an anonymous arrow and never shows up by name. **Rule for PRs 35: move a gate to router
level only where it was already a prefix mount; otherwise leave it on the route.**
- **`modAccess` stays in the residual file** — the `/shard/*` in-game staff operations still use it
and do not move until PR 4. Its comment there was retargeted rather than deleted.
### The swagger path-normalization prerequisite (landed before PR 1) ### The swagger path-normalization prerequisite (landed before PR 1)
swagger-autogen builds a path by string-concatenating the mount prefix with the route argument, so a swagger-autogen builds a path by string-concatenating the mount prefix with the route argument, so a
@@ -449,10 +484,10 @@ deliberate `+1` in the manifest — which is exactly the mechanism working as de
flipping. Also decide there whether `/api/csp-report` is retired with the report-only twin or kept flipping. Also decide there whether `/api/csp-report` is retired with the report-only twin or kept
as a `report-to` group on the enforced policy. as a `report-to` group on the enforced policy.
4. **PR 1 — admin:** `users`, `account`, `invites`, `auth` (providers). ✅ landed 4. **PR 1 — admin:** `users`, `account`, `invites`, `auth` (providers). ✅ landed
5. **PR 2 — admin:** `moderation`, `bot-activity`, `activity`. 5. **PR 2 — admin:** `moderation`, `bot-activity`, `activity`. ✅ landed
6. **PR 3 — admin (content):** `posts`, `pages`, `wiki`, `uploads`. 6. **PR 3 — admin (content):** `posts`, `pages`, `wiki`, `uploads`.
7. **PR 4 — admin (ops/config):** `shard`, `uo-link`, `email`, `discord-bot`, `settings`, `site-mode`, 7. **PR 4 — admin (ops/config):** `shard`, `uo-link`, `email`, `discord-bot`, `settings`, `site-mode`,
`dashboard`. `dashboard`. (`activity` went with PR 2 — see § PR 2 — as landed.)
8. **PR 5 — `public/*` + `player/*`** (and the residual `auth/*` grouping). 8. **PR 5 — `public/*` + `player/*`** (and the residual `auth/*` grouping).
Each PR: **zero-line diff in `routes.manifest.json`**, server tests green Each PR: **zero-line diff in `routes.manifest.json`**, server tests green

View File

@@ -34,9 +34,10 @@ Skeleton from the spec, with a small number of justified additions marked **(+)*
> every URL unchanged**. This section and §4 get updated as each split PR lands. See > every URL unchanged**. This section and §4 get updated as each split PR lands. See
> [API_V2_PLAN.md](./API_V2_PLAN.md) § Phase 2. > [API_V2_PLAN.md](./API_V2_PLAN.md) § Phase 2.
> >
> **Landed so far:** admin `users`, `account`, `invites` and `auth/providers` (28 routes) now live in > **Landed so far:** admin `users`, `account`, `invites`, `auth/providers` (PR 1, 28 routes) and
> their own routers under `admin/`, behind a new `admin/index.js`. The remaining 82 admin routes are > `moderation`, `bot-activity`, `activity` (PR 2, 18 routes) now live in their own routers under
> still in `admin.routes.js`, and `public/` and `player/` are untouched. > `admin/`, behind a new `admin/index.js`. The remaining 64 admin routes are still in
> `admin.routes.js`, and `public/` and `player/` are untouched.
> >
> "Every URL unchanged" is enforced mechanically, not by review: `server/scripts/routeManifest.js` > "Every URL unchanged" is enforced mechanically, not by review: `server/scripts/routeManifest.js`
> (`npm run routes:manifest`) walks the live Express stack and writes the sorted > (`npm run routes:manifest`) walks the live Express stack and writes the sorted
@@ -68,7 +69,12 @@ server/
users.router.js (15) /admin/users — adminOnly users.router.js (15) /admin/users — adminOnly
invites.router.js (3) /admin/invites — adminOnly invites.router.js (3) /admin/invites — adminOnly
authProviders.router.js (4) /admin/auth — adminOnly authProviders.router.js (4) /admin/auth — adminOnly
admin.routes.js (82) everything not yet split, mounted moderation.router.js (15) /admin/moderation — modAccess
(admin+moderator) at router level
botActivity.router.js (2) /admin/bot-activity — adminOnly
activity.router.js (1) /admin/activity — staff-wide
audit log, no extra gate
admin.routes.js (64) everything not yet split, mounted
last at the group root; goes away last at the group root; goes away
when the final split PR lands when the final split PR lands
admin.controller.js + the per-capability controllers admin.controller.js + the per-capability controllers
@@ -460,7 +466,8 @@ Public content GETs pass through the **siteMode** gate (§5).
### /admin (admin/index.js → the capability routers in §2) — all behind `isLoggedIn` + `noindex` + `staffOnly` ### /admin (admin/index.js → the capability routers in §2) — all behind `isLoggedIn` + `noindex` + `staffOnly`
`admin/index.js` applies the shared gate and mounts each capability router at the prefix it owns; `admin/index.js` applies the shared gate and mounts each capability router at the prefix it owns;
`users`, `invites` and `auth/providers` add `adminOnly` on top. Routes not yet extracted still live `users`, `invites`, `auth/providers` and `bot-activity` add `adminOnly` on top, and `moderation` adds
`modAccess` (admin + moderator, so editors are excluded). Routes not yet extracted still live
in `admin.routes.js`, mounted last at the group root. The URLs below are unaffected by which file a in `admin.routes.js`, mounted last at the group root. The URLs below are unaffected by which file a
route currently sits in — that is the property the route manifest freezes. route currently sits in — that is the property the route manifest freezes.
| Method | Path | Purpose | | Method | Path | Purpose |