refactor(server): split admin shard, uo-link, email, discord-bot, settings and dashboard into capability routers (PR 4) #105

Merged
whitlocktech merged 1 commits from refactor/admin-router-split-4 into main 2026-07-28 01:33:48 +00:00
Member

What & why

PR 4 of the in-place admin router split — the last admin one — see docs/website/API_V2_PLAN.md § Phase 2. Follows #102 (PR 1), #103 (PR 2) and #104 (PR 3). Docs PR: RunicGateway/docs#58.

This moves the entire residual 33 and deletes admin.routes.js. Every one of the 110 admin routes is now declared in a capability router, each mounted at the prefix it already owned, so no URL, gate or handler changes.

Router Routes Prefix Extra gate
shard.router.js 16 /admin/shard none on the 7 self-service routes; modAccess per route on the 9 staff ops
uoLink.router.js 5 /admin/uo-link adminOnly per route
email.router.js 6 /admin/email adminOnly per route
discordBot.router.js 2 /admin/discord-bot adminOnly per route
settings.router.js 2 /admin/settings adminOnly per route
dashboard.router.js 2 group root (/dashboard, /site-mode) adminOnly on /site-mode only
admin.routes.js deleted

16 + 5 + 6 + 2 + 2 + 2 = 33, and 33 + the 77 extracted by PRs 1–3 = the 110 inventoried admin routes.

No gate moved to router level. Every adminOnly in the residual file was per-route, and modAccess on /shard must stay per-route because half that router must not have it. That keeps the per-route handler count intact — the one number routes.guards.json can actually check, since requireRole(...) returns an anonymous arrow that never shows up by name.

Two things worth a reviewer's attention

  1. dashboard.router.js is mounted at the group root, not a prefix — the single relaxation of the split's "always mount at a prefix" rule. GET /dashboard and PUT /site-mode own no common path segment, so a prefix mount would mean two one-route files instead of the one file the target tree calls for. It is safe only because that file declares no router-level middleware: a bare use(gate) in a root-mounted router runs for every request passing through toward another mount and would 403 an editor on an unrelated route (the PR 1 finding). The file carries a comment saying exactly that, because the next person to add a gate there is the one who needs to know.

  2. /shard is the first prefix where two tiers share one router. Seven self-service account-linking routes (link, accounts, roster/:account, vendors/:account, char/:serial, sales, POST /account) carry no gate beyond the shared staffOnly and are served by the same player/shard.controller handlers as /player/shard — staff are a superset of players and the controller keys off req.user.id. The nine in-game ops carry modAccess. They keep their differing swagger tags (Admin · Account vs Admin · Shard), so the tag disagrees with the filename: prefix ownership beats tag grouping, and splitting by tag would put two routers under one prefix for no gain. Retagging is a real spec diff and belongs in a PR about tags — same call as PR 1 and PR 3.

Also in this PR: the comments that referenced admin.routes.js by name are repointed now that the file is gone (botActivity.controller.jsbotActivity.router.js, moderation.controller.jsmoderation.router.js, the town-crier cap mirror in announceJobs.logic.jsadmin/uoLink.router.js), and the "a route's path sits on the line after router.get(" rationale in routeManifest.js, README.md and pr-checks.yml is generalized — it was never about that one file.

Next and last: PR 5 — public/* + player/* (and the residual auth/* grouping).

How it was tested

All four freeze gates came back zero-diff, which is this PR's acceptance criterion:

cd website/server
npm run routes:manifest   # routes.manifest.json unchanged — 200 public + 2 internal
                          # routes.guards.json  unchanged — no route lost or gained a gate
npm run swagger           # swagger-output.json unchanged — 198 operations
npm test                  # 434 tests, 0 failures

docs/website/api-route-inventory.json was already in sync, so it needed no refresh. The generated artifacts are not in this diff — they regenerate byte-identically, which is the point.

Plus the one check no gate can make. The manifest sorts its entries, so declaration order is invisible to all three gates; a /:param route declared ahead of a literal sibling would pass every gate and still break the URL. Introspecting the built stack in declaration order and asking, for each literal path, which layer would match it first:

checked 110 admin routes (59 literal); shadowed: 0

The near-misses worth naming: GET /shard/pages (the in-game help-page queue — unrelated to /admin/pages, the CMS builder) sits alongside POST /shard/pages/:id/respond|close, and POST /uo-link/towncrier alongside DELETE /uo-link/towncrier/:id. Different depths and methods, so neither collides.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

This project requires disclosure of AI tool usage. Please pick one:

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why **PR 4 of the in-place admin router split — the last admin one** — see [`docs/website/API_V2_PLAN.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/API_V2_PLAN.md) § Phase 2. Follows #102 (PR 1), #103 (PR 2) and #104 (PR 3). Docs PR: RunicGateway/docs#58. This moves the **entire residual 33** and **deletes `admin.routes.js`**. Every one of the 110 admin routes is now declared in a capability router, each mounted at the prefix it already owned, so **no URL, gate or handler changes**. | Router | Routes | Prefix | Extra gate | |---|---|---|---| | `shard.router.js` | 16 | `/admin/shard` | none on the 7 self-service routes; `modAccess` per route on the 9 staff ops | | `uoLink.router.js` | 5 | `/admin/uo-link` | `adminOnly` per route | | `email.router.js` | 6 | `/admin/email` | `adminOnly` per route | | `discordBot.router.js` | 2 | `/admin/discord-bot` | `adminOnly` per route | | `settings.router.js` | 2 | `/admin/settings` | `adminOnly` per route | | `dashboard.router.js` | 2 | **group root** (`/dashboard`, `/site-mode`) | `adminOnly` on `/site-mode` only | | ~~`admin.routes.js`~~ | — | **deleted** | — | 16 + 5 + 6 + 2 + 2 + 2 = 33, and 33 + the 77 extracted by PRs 1–3 = the 110 inventoried admin routes. **No gate moved to router level.** Every `adminOnly` in the residual file was per-route, and `modAccess` on `/shard` *must* stay per-route because half that router must not have it. That keeps the per-route handler count intact — the one number `routes.guards.json` can actually check, since `requireRole(...)` returns an anonymous arrow that never shows up by name. ### Two things worth a reviewer's attention 1. **`dashboard.router.js` is mounted at the group root, not a prefix** — the single relaxation of the split's "always mount at a prefix" rule. `GET /dashboard` and `PUT /site-mode` own no common path segment, so a prefix mount would mean two one-route files instead of the one file the target tree calls for. It is safe **only** because that file declares no router-level middleware: a bare `use(gate)` in a root-mounted router runs for every request passing through toward another mount and would 403 an editor on an unrelated route (the PR 1 finding). The file carries a comment saying exactly that, because the next person to add a gate there is the one who needs to know. 2. **`/shard` is the first prefix where two tiers share one router.** Seven self-service account-linking routes (`link`, `accounts`, `roster/:account`, `vendors/:account`, `char/:serial`, `sales`, `POST /account`) carry no gate beyond the shared `staffOnly` and are served by the *same* `player/shard.controller` handlers as `/player/shard` — staff are a superset of players and the controller keys off `req.user.id`. The nine in-game ops carry `modAccess`. They keep their differing swagger tags (`Admin · Account` vs `Admin · Shard`), so the tag disagrees with the filename: prefix ownership beats tag grouping, and splitting by tag would put two routers under one prefix for no gain. Retagging is a real spec diff and belongs in a PR about tags — same call as PR 1 and PR 3. Also in this PR: the comments that referenced `admin.routes.js` by name are repointed now that the file is gone (`botActivity.controller.js` → `botActivity.router.js`, `moderation.controller.js` → `moderation.router.js`, the town-crier cap mirror in `announceJobs.logic.js` → `admin/uoLink.router.js`), and the "a route's path sits on the line *after* `router.get(`" rationale in `routeManifest.js`, `README.md` and `pr-checks.yml` is generalized — it was never about that one file. **Next and last:** PR 5 — `public/*` + `player/*` (and the residual `auth/*` grouping). ## How it was tested All four freeze gates came back **zero-diff**, which is this PR's acceptance criterion: ``` cd website/server npm run routes:manifest # routes.manifest.json unchanged — 200 public + 2 internal # routes.guards.json unchanged — no route lost or gained a gate npm run swagger # swagger-output.json unchanged — 198 operations npm test # 434 tests, 0 failures ``` `docs/website/api-route-inventory.json` was already in sync, so it needed no refresh. The generated artifacts are **not in this diff** — they regenerate byte-identically, which is the point. Plus the one check no gate can make. The manifest sorts its entries, so declaration order is invisible to all three gates; a `/:param` route declared ahead of a literal sibling would pass every gate and still break the URL. Introspecting the built stack in declaration order and asking, for each literal path, which layer would match it first: ``` checked 110 admin routes (59 literal); shadowed: 0 ``` The near-misses worth naming: `GET /shard/pages` (the in-game help-page queue — unrelated to `/admin/pages`, the CMS builder) sits alongside `POST /shard/pages/:id/respond|close`, and `POST /uo-link/towncrier` alongside `DELETE /uo-link/towncrier/:id`. Different depths and methods, so neither collides. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. <!-- docs: RunicGateway/docs#58 --> - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) This project **requires disclosure of AI tool usage**. Please pick one: - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-07-28 01:04:24 +00:00
refactor(server): split admin shard, uo-link, email, discord-bot, settings and dashboard into capability routers
All checks were successful
PR Checks / bot-install (pull_request) Successful in 17s
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / server-tests (pull_request) Successful in 9m21s
8fd0d82580
PR 4 of the in-place admin router split (docs/website/API_V2_PLAN.md § Phase 2),
and the last admin one: it moves the entire residual 33 and DELETES
admin.routes.js. Every one of the 110 admin routes is now declared in a
capability router. No URL, gate or handler changes.

  shard.router.js      (16)  /admin/shard
  uoLink.router.js     ( 5)  /admin/uo-link
  email.router.js      ( 6)  /admin/email
  discordBot.router.js ( 2)  /admin/discord-bot
  settings.router.js   ( 2)  /admin/settings
  dashboard.router.js  ( 2)  GET /dashboard + PUT /site-mode, at the group root
  admin.routes.js            deleted, was 33

No gate moved to router level. Every adminOnly in the residual file was
per-route, and modAccess on /shard must stay per-route because half that router
must not have it — which keeps the per-route handler count intact, the one
number routes.guards.json can actually check.

/shard is the first prefix where two tiers share one router: 7 self-service
account-linking routes (no extra gate, served by the same player/shard
controller handlers, tagged `Admin · Account`) alongside 9 in-game staff ops on
modAccess. Prefix ownership beats tag grouping — splitting by tag would put two
routers under one prefix for no gain. The tag mismatch stays; retagging is a
real spec diff and belongs in a PR about tags.

dashboard.router.js is the one router mounted at the group root rather than a
prefix: GET /dashboard and PUT /site-mode share no path segment. That is safe
only because the file declares no router-level middleware — a bare use(gate) in
a root-mounted router would run for every request passing through toward
another mount. The file carries a comment saying so.

Acceptance — all four gates zero-diff:
  routes.manifest.json    unchanged (200 public + 2 internal)
  routes.guards.json      unchanged (no route lost or gained a gate)
  swagger-output.json     unchanged (198 operations)
  api-route-inventory.json already in sync
plus 434 server tests green.

Verified separately, because no gate can catch it: introspecting the built
stack, all 59 literal admin paths still dispatch to their own layer — nothing
is captured first by a /:param sibling. The manifest sorts its entries, so
declaration order is invisible to it.

Also repoints the comments that referenced admin.routes.js by name
(botActivity/moderation controllers, the town-crier cap mirror in
announceJobs.logic.js) and generalizes the "the path is on the line after
router.get(" rationale in routeManifest.js, README.md and pr-checks.yml, which
was never about that one file.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech approved these changes 2026-07-28 01:33:39 +00:00
whitlocktech merged commit 3fcc64ab96 into main 2026-07-28 01:33:48 +00:00
whitlocktech deleted branch refactor/admin-router-split-4 2026-07-28 01:33:49 +00:00
Sign in to join this conversation.
No description provided.