refactor(server): split admin posts, uploads, wiki and pages into capability routers (PR 3) #104

Merged
whitlocktech merged 1 commits from refactor/admin-router-split-3 into main 2026-07-28 00:36:29 +00:00
Member

What & why

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

This is the content tier. Four capabilities move out of the residual admin.routes.js into their own router files, each mounted at the prefix it already owned, so no URL, gate or handler changes.

Router Routes Prefix Extra gate
posts.router.js 9 /admin/posts none — editor tier
uploads.router.js 1 /admin/uploads none — editor tier
wiki.router.js 14 /admin/wiki none — editor tier
pages.router.js 7 /admin/pages none — editor tier
admin.routes.js (residual) 33 group root, mounted last unchanged

31 + 33 + the 46 already extracted = the 110 inventoried admin routes.

First split PR where no gate moved at all. All four capabilities are editor-tier, so the shared noindex, isLoggedIn, staffOnly gate in admin/index.js is their whole gate. PR 2's rule ("move a gate to router level only where it was already a prefix mount") had nothing to act on here.

Three things worth a reviewer's attention

  1. admin/imageUpload.js — the first shared module in the split. The multer config (upload dir, mimetype→extension allowlist, 8 MB cap) was inline in admin.routes.js and used by two routes that this PR puts in different files: POST /posts/upload (returns {image_url}) and POST /uploads (returns {url}). It moved to a shared module rather than being duplicated — duplicating a security allowlist is how the two copies drift. It stays in admin/ deliberately: UPLOAD_DIR is resolved __dirname-relative, so relocating the file would silently repoint the upload directory.

  2. POST /uploads keeps its Admin · Posts swagger tag, which now disagrees with its filename. The acceptance criterion here is a byte-identical spec, so retagging is a real OpenAPI diff and doesn't belong in a route-move PR — same call as PR 1 made for the /shard/* tag mismatch.

  3. The wiki router has load-bearing intra-file route order. /categories and /tags are literal paths that must stay ahead of /:slug, or GET /admin/wiki/categories gets dispatched as a page whose slug is "categories". None of the three gates can catch this — the manifest sorts its entries, so a reordering is invisible. Verified separately (below).

Also noted for next time: the residual 33 is exactly PR 4's list (shard 16, email 6, uo-link 5, settings 2, discord-bot 2, dashboard 1, site-mode 1), so admin.routes.js gets deleted by PR 4, and PR 5 touches only public/*, player/* and auth/*.

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 — 169 paths / 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, on the wiki ordering hazard — introspecting the built router stack and asserting the last literal layer precedes the first /:slug layer:

 0  GET /categories        5  GET /              10  GET /:slug/revisions
 1  POST /categories       6  POST /             11  GET /:slug/revisions/:id
 2  PUT /categories/:id    7  GET /:slug         12  POST /:slug/revisions/:id/restore
 3  DELETE /categories/:id 8  PUT /:slug         13  DELETE /:slug
 4  GET /tags              9  PATCH /:slug/publish
---
last static literal at 4 | first /:slug at 7  →  OK: literals precede /:slug

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 3 of the in-place admin router split** — 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) and #103 (PR 2). Docs PR: RunicGateway/docs#56. This is the **content tier**. Four capabilities move out of the residual `admin.routes.js` into their own router files, each mounted at the prefix it already owned, so **no URL, gate or handler changes**. | Router | Routes | Prefix | Extra gate | |---|---|---|---| | `posts.router.js` | 9 | `/admin/posts` | none — editor tier | | `uploads.router.js` | 1 | `/admin/uploads` | none — editor tier | | `wiki.router.js` | 14 | `/admin/wiki` | none — editor tier | | `pages.router.js` | 7 | `/admin/pages` | none — editor tier | | `admin.routes.js` (residual) | 33 | group root, mounted last | unchanged | 31 + 33 + the 46 already extracted = the 110 inventoried admin routes. **First split PR where no gate moved at all.** All four capabilities are editor-tier, so the shared `noindex, isLoggedIn, staffOnly` gate in `admin/index.js` is their whole gate. PR 2's rule ("move a gate to router level only where it was already a prefix mount") had nothing to act on here. ### Three things worth a reviewer's attention 1. **`admin/imageUpload.js` — the first shared module in the split.** The multer config (upload dir, mimetype→extension allowlist, 8 MB cap) was inline in `admin.routes.js` and used by **two** routes that this PR puts in different files: `POST /posts/upload` (returns `{image_url}`) and `POST /uploads` (returns `{url}`). It moved to a shared module rather than being duplicated — duplicating a security allowlist is how the two copies drift. It stays in `admin/` deliberately: `UPLOAD_DIR` is resolved `__dirname`-relative, so relocating the file would silently repoint the upload directory. 2. **`POST /uploads` keeps its `Admin · Posts` swagger tag**, which now disagrees with its filename. The acceptance criterion here is a byte-identical spec, so retagging is a real OpenAPI diff and doesn't belong in a route-move PR — same call as PR 1 made for the `/shard/*` tag mismatch. 3. **The wiki router has load-bearing intra-file route order.** `/categories` and `/tags` are literal paths that must stay ahead of `/:slug`, or `GET /admin/wiki/categories` gets dispatched as a page whose slug is `"categories"`. **None of the three gates can catch this** — the manifest sorts its entries, so a reordering is invisible. Verified separately (below). Also noted for next time: **the residual 33 is exactly PR 4's list** (shard 16, email 6, uo-link 5, settings 2, discord-bot 2, dashboard 1, site-mode 1), so `admin.routes.js` gets deleted by PR 4, and PR 5 touches only `public/*`, `player/*` and `auth/*`. ## 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 — 169 paths / 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, on the wiki ordering hazard — introspecting the built router stack and asserting the last literal layer precedes the first `/:slug` layer: ``` 0 GET /categories 5 GET / 10 GET /:slug/revisions 1 POST /categories 6 POST / 11 GET /:slug/revisions/:id 2 PUT /categories/:id 7 GET /:slug 12 POST /:slug/revisions/:id/restore 3 DELETE /categories/:id 8 PUT /:slug 13 DELETE /:slug 4 GET /tags 9 PATCH /:slug/publish --- last static literal at 4 | first /:slug at 7 → OK: literals precede /:slug ``` ## 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#56 --> - [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 00:26:30 +00:00
refactor(server): split admin posts, uploads, wiki and pages into capability routers
All checks were successful
PR Checks / bot-install (pull_request) Successful in 16s
PR Checks / server-tests (pull_request) Successful in 37s
PR Checks / client-build (pull_request) Successful in 9m15s
00ad16858a
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>
whitlocktech approved these changes 2026-07-28 00:30:21 +00:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-07-28 00:30:25 +00:00
whitlocktech merged commit 812b895507 into main 2026-07-28 00:36:29 +00:00
whitlocktech deleted branch refactor/admin-router-split-3 2026-07-28 00:36:30 +00:00
Sign in to join this conversation.
No description provided.