refactor(server): split admin users, account, invites and auth providers into capability routers #102

Merged
whitlocktech merged 1 commits from refactor/admin-router-split-1 into build/swagger-normalize-paths 2026-07-27 21:00:07 +00:00
Member

Stacked on #101. Base is build/swagger-normalize-paths, so this diff shows only the refactor.
Merge #101 first, then retarget this to main.

What & why

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:

Router Routes Prefix Extra gate
account.router.js 6 /admin/account none — self-service; an editor manages their own 2FA
users.router.js 15 /admin/users adminOnly at router level
invites.router.js 3 /admin/invites adminOnly per route
authProviders.router.js 4 /admin/auth (routes are /providers[/:id]) adminOnly per route
admin.routes.js (residual) 82 group root, mounted last unchanged

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.js disappears when PR 5 lands.

Handlers still live in admin.controller.js and usersShard.controller.js — the plan is explicit
that this re-wires routes, not logic. adminOnly moves with the routes that use it.

How it was tested

Three generated gates, all 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
npm run routes:manifest -- --check  → route manifest up to date (202 routes)
npm test                            → 434/434

routes.guards.json coming back clean is the one worth noting: the method+path freeze cannot see a
dropped 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:

  1. The six self-service /shard/* routes stay with the shard capability (PR 4) despite their
    Admin · Account swagger tag. The invariant is prefix ownership, not tag agreement — splitting
    them off would mean two routers mounting under /shard and an ordering hazard for no gain.
  2. usersRouter.use(adminOnly) is exactly equivalent to the old adminRouter.use('/users', adminOnly)
    because the router is mounted at a prefix. Under a pathless mount, a bare use(gate) would run
    for 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.
  3. PROJECT_TREE.md is auto-generated by the sync-project-tree workflow since #98, so it is
    deliberately not touched here; CI opens its own docs PR after merge.

Docs: RunicGateway/docs PR (stacked the same way).

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)

  • 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.
> **Stacked on #101.** Base is `build/swagger-normalize-paths`, so this diff shows only the refactor. > Merge #101 first, then retarget this to `main`. ## What & why 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: | Router | Routes | Prefix | Extra gate | |---|---|---|---| | `account.router.js` | 6 | `/admin/account` | none — self-service; an editor manages their own 2FA | | `users.router.js` | 15 | `/admin/users` | `adminOnly` at router level | | `invites.router.js` | 3 | `/admin/invites` | `adminOnly` per route | | `authProviders.router.js` | 4 | `/admin/auth` (routes are `/providers[/:id]`) | `adminOnly` per route | | `admin.routes.js` (residual) | 82 | group root, mounted last | unchanged | 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.js` disappears when PR 5 lands. Handlers still live in `admin.controller.js` and `usersShard.controller.js` — the plan is explicit that this re-wires routes, not logic. `adminOnly` moves with the routes that use it. ## How it was tested **Three generated gates, all 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 npm run routes:manifest -- --check → route manifest up to date (202 routes) npm test → 434/434 ``` `routes.guards.json` coming back clean is the one worth noting: the method+path freeze cannot see a dropped `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: 1. **The six self-service `/shard/*` routes stay with the `shard` capability (PR 4)** despite their `Admin · Account` swagger tag. The invariant is *prefix ownership*, not tag agreement — splitting them off would mean two routers mounting under `/shard` and an ordering hazard for no gain. 2. **`usersRouter.use(adminOnly)` is exactly equivalent to the old `adminRouter.use('/users', adminOnly)`** *because* the router is mounted at a prefix. Under a pathless mount, a bare `use(gate)` would run for 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. 3. `PROJECT_TREE.md` is auto-generated by the `sync-project-tree` workflow since #98, so it is deliberately not touched here; CI opens its own docs PR after merge. Docs: RunicGateway/docs PR (stacked the same way). ## 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. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] 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-27 20:56:39 +00:00
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>
whitlocktech approved these changes 2026-07-27 20:59:43 +00:00
whitlocktech merged commit f7c98b8ba3 into build/swagger-normalize-paths 2026-07-27 21:00:07 +00:00
whitlocktech deleted branch refactor/admin-router-split-1 2026-07-27 21:00:07 +00:00
Sign in to join this conversation.
No description provided.