refactor(server): split admin shard, uo-link, email, discord-bot, settings and dashboard into capability routers (PR 4) #105
Reference in New Issue
Block a user
No description provided.
Delete Branch "refactor/admin-router-split-4"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.shard.router.js/admin/shardmodAccessper route on the 9 staff opsuoLink.router.js/admin/uo-linkadminOnlyper routeemail.router.js/admin/emailadminOnlyper routediscordBot.router.js/admin/discord-botadminOnlyper routesettings.router.js/admin/settingsadminOnlyper routedashboard.router.js/dashboard,/site-mode)adminOnlyon/site-modeonlyadmin.routes.js16 + 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
adminOnlyin the residual file was per-route, andmodAccesson/shardmust stay per-route because half that router must not have it. That keeps the per-route handler count intact — the one numberroutes.guards.jsoncan actually check, sincerequireRole(...)returns an anonymous arrow that never shows up by name.Two things worth a reviewer's attention
dashboard.router.jsis mounted at the group root, not a prefix — the single relaxation of the split's "always mount at a prefix" rule.GET /dashboardandPUT /site-modeown 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 bareuse(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./shardis 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 sharedstaffOnlyand are served by the sameplayer/shard.controllerhandlers as/player/shard— staff are a superset of players and the controller keys offreq.user.id. The nine in-game ops carrymodAccess. They keep their differing swagger tags (Admin · AccountvsAdmin · 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.jsby 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 inannounceJobs.logic.js→admin/uoLink.router.js), and the "a route's path sits on the line afterrouter.get(" rationale inrouteManifest.js,README.mdandpr-checks.ymlis generalized — it was never about that one file.Next and last: PR 5 —
public/*+player/*(and the residualauth/*grouping).How it was tested
All four freeze gates came back zero-diff, which is this PR's acceptance criterion:
docs/website/api-route-inventory.jsonwas 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
/:paramroute 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:The near-misses worth naming:
GET /shard/pages(the in-game help-page queue — unrelated to/admin/pages, the CMS builder) sits alongsidePOST /shard/pages/:id/respond|close, andPOST /uo-link/towncrieralongsideDELETE /uo-link/towncrier/:id. Different depths and methods, so neither collides.Checklist
AI-assisted contributions (required)
This project requires disclosure of AI tool usage. Please pick one:
Claude Code (Opus 5). I have reviewed and understandevery change, and take responsibility for it. AI-authored commits are
marked with a
Co-Authored-By/Assisted-Bytrailer.License
(GNU GPL v3.0 or later), and I have the right to contribute it.
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>