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>
PR 3 of the in-place admin router split (docs/website/API_V2_PLAN.md § Phase 2).
Moves the content tier out of the residual admin.routes.js into one router file
per capability, each mounted at the prefix it already owned. No URL, gate or
handler changes.
posts.router.js ( 9) /admin/posts
uploads.router.js ( 1) /admin/uploads
wiki.router.js (14) /admin/wiki
pages.router.js ( 7) /admin/pages
admin.routes.js (33) residual, was 64
All four capabilities are editor tier, so no gate moved: the shared
`noindex, isLoggedIn, staffOnly` in admin/index.js is their whole gate.
The multer config moved to admin/imageUpload.js because the two routes that
share it (POST /posts/upload and POST /uploads) now live in different files;
duplicating a mimetype allowlist is how the two copies drift. It stays in
admin/ because UPLOAD_DIR is resolved relative to __dirname.
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: the wiki router's literal
/categories and /tags paths still precede /:slug in declaration order. The
manifest sorts its entries, so a reordering there would be invisible.
Co-Authored-By: Claude <noreply@anthropic.com>
PR 2 of the domain split (docs/website/API_V2_PLAN.md § Phase 2). Carves 18 more
routes out of admin.routes.js into one router file per business capability,
in place, with every URL unchanged:
moderation.router.js (15) /admin/moderation modAccess at router level
botActivity.router.js (2) /admin/bot-activity adminOnly per route
activity.router.js (1) /admin/activity staff-wide, no extra gate
The residual admin.routes.js drops from 82 routes to 64.
Moderation was already gated by a prefix mount (adminRouter.use('/moderation',
modAccess)), so moderationRouter.use(modAccess) is the exact equivalent now that
the router is mounted at a prefix. Bot-activity's adminOnly was per-route and is
deliberately kept per-route: that is what holds the per-route handler count in
routes.guards.json, the only signal that would catch a dropped gate, since
requireRole(...) returns an anonymous arrow and never appears by name.
/activity gets its own file rather than waiting for dashboard.router.js in PR 4
— it is the staff audit log, a different capability from the dashboard's stats
overview and from the botScore middleware's in-memory ban state.
Acceptance:
- routes.manifest.json zero-diff (200 public + 2 internal)
- routes.guards.json zero-diff
- swagger-output.json zero-diff (198 operations)
- api-route-inventory.json already in sync
- 434 server tests green
- role gates verified identical to main by reading the requireRole role sets
off the live Express stack for every moved route plus untouched controls
Co-Authored-By: Claude <noreply@anthropic.com>
First of the five domain-split PRs in docs/website/API_V2_PLAN.md § Phase 2. Pure
mechanical re-wiring: routes move between files, no handler, gate, validator or
annotation changes, and not one URL moves.
New src/router/v1/admin/index.js owns the two things the group shares — the
`noindex, isLoggedIn, staffOnly` gate and the mount table — and declares no routes
itself. The gate sits ahead of every mount so a capability router extracted in a
later PR cannot silently ship without it. Four capability routers mount at the
prefix they already owned inside the monolith:
account.router.js 6 routes -> /admin/account (self-service, no adminOnly)
users.router.js 15 routes -> /admin/users (adminOnly, router-level)
invites.router.js 3 routes -> /admin/invites (adminOnly, per-route)
authProviders.router.js 4 routes -> /admin/auth (adminOnly, per-route)
admin.routes.js keeps the other 82 (6+15+3+4+82 = the 110 inventoried admin
routes) and is mounted last at the group root; none of the four prefixes appears
in it, so nothing depends on mount ordering. It disappears when PR 5 lands.
Handlers still live in admin.controller.js and usersShard.controller.js — this
re-wires routes, not logic. `adminOnly` moves with the routes that use it, and
`usersRouter.use(adminOnly)` is exactly equivalent to the old
`adminRouter.use('/users', adminOnly)` now that the router is mounted at /users.
All three generated gates are 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, byte-for-byte
The spec staying byte-identical depends on the path normalization landed in the
preceding commit; without it the four collection routes would have documented as
/api/v1/admin/{users,invites,account}/ with a trailing slash.
Server tests green (434/434).
Co-Authored-By: Claude <noreply@anthropic.com>