refactor(server): split admin users, account, invites and auth providers into capability routers #102
Reference in New Issue
Block a user
No description provided.
Delete Branch "refactor/admin-router-split-1"
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
First of the five domain-split PRs in
docs/website/API_V2_PLAN.md§ Phase 2. Pure mechanicalre-wiring — routes move between files; no handler, gate, validator or annotation changes, and not
one URL moves.
New
src/router/v1/admin/index.jsowns the two things the group shares — thenoindex, isLoggedIn, staffOnlygate and the mount table — and declares no routes itself. The gatesits 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/admin/accountusers.router.js/admin/usersadminOnlyat router levelinvites.router.js/admin/invitesadminOnlyper routeauthProviders.router.js/admin/auth(routes are/providers[/:id])adminOnlyper routeadmin.routes.js(residual)6 + 15 + 3 + 4 + 82 = the 110 inventoried admin routes. None of the four prefixes appears in the
residual file, so nothing depends on mount ordering.
admin.routes.jsdisappears when PR 5 lands.Handlers still live in
admin.controller.jsandusersShard.controller.js— the plan is explicitthat this re-wires routes, not logic.
adminOnlymoves with the routes that use it.How it was tested
Three generated gates, all zero-diff:
routes.guards.jsoncoming back clean is the one worth noting: the method+path freeze cannot see adropped
adminOnly, but the guards file can.The spec staying byte-identical depends on #101; without it the four collection routes would have
documented as
/api/v1/admin/{users,invites,account}/with a trailing slash.Pre-existing test flakiness, unrelated to this PR. The suite intermittently fails one file
(~840 ms, a different file each time —
shardState.model.test.js,shardControllerPublic.test.js),and passes on re-run and in isolation. One of the flakes occurred on the #101 branch, which contains
zero router changes, so it is not caused by this work. Looks like a file-level timeout under
parallel load. Flagging it as worth its own look; not blocking here.
Review notes
Three things found while doing this, carried into the plan doc for PRs 2–5:
/shard/*routes stay with theshardcapability (PR 4) despite theirAdmin · Accountswagger tag. The invariant is prefix ownership, not tag agreement — splittingthem off would mean two routers mounting under
/shardand an ordering hazard for no gain.usersRouter.use(adminOnly)is exactly equivalent to the oldadminRouter.use('/users', adminOnly)because the router is mounted at a prefix. Under a pathless mount, a bare
use(gate)would runfor every request passing through en route to a later mount — 403-ing an editor on
/admin/posts.Worth knowing before anyone "simplifies" a prefix mount away in PR 2–5.
PROJECT_TREE.mdis auto-generated by thesync-project-treeworkflow since #98, so it isdeliberately not touched here; CI opens its own docs PR after merge.
Docs: RunicGateway/docs PR (stacked the same way).
Checklist
AI-assisted contributions (required)
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.
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>