refactor(server): split admin shard, uo-link, email, discord-bot, settings and dashboard into capability routers
All checks were successful
PR Checks / bot-install (pull_request) Successful in 17s
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / server-tests (pull_request) Successful in 9m21s

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>
This commit is contained in:
2026-07-27 20:02:28 -05:00
parent 812b895507
commit 8fd0d82580
15 changed files with 617 additions and 486 deletions

View File

@@ -0,0 +1,58 @@
// Admin · Dashboard — the landing summary, plus the /site-mode singleton.
//
// Mounted at the ROOT of /api/v1/admin by admin/index.js (not at a prefix),
// which already applied `noindex, isLoggedIn, staffOnly`. Two singleton URLs
// that share a swagger tag and a screen but not a path segment live together
// here rather than in two one-route files, which is what the target tree in
// docs/website/API_V2_PLAN.md § Phase 2 calls for.
//
// A root mount is the one place the split's "always mount at a prefix" rule is
// relaxed, and it is safe ONLY because this file declares no router-level
// middleware: a bare `use(gate)` here would run for every request passing
// through toward another mount and 403 an editor on an unrelated route. Keep
// gates per-route in this file.
//
// GET /dashboard — stats overview, any staff role.
// PUT /site-mode — live ↔ maintenance, admin only.
//
// Neither is the audit log (/activity) nor the bot-scoring state
// (/bot-activity); those are separate capabilities that read alike. Handlers
// still live in admin.controller.js; this re-wires routes, not logic.
const express = require('express')
const { body } = require('express-validator')
const ctrl = require('./admin.controller')
const { requireRole } = require('../../../utils/auth')
const validate = require('../../../middleware/validate')
const dashboardRouter = express.Router()
const adminOnly = requireRole('admin')
dashboardRouter.get(
'/dashboard',
// #swagger.tags = ['Admin · Dashboard']
// #swagger.summary = 'Dashboard summary counts'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Summary: site mode, last change, post/user counts and recent activity', content: { "application/json": { schema: { type: "object", properties: { site_mode: { type: "string", example: "live" }, last_change: { type: "object", properties: { at: { type: "string", nullable: true }, by: { type: "string", nullable: true } } }, counts: { type: "object", properties: { posts: { type: "object", additionalProperties: true }, users: { type: "integer" } } }, recent_activity: { type: "array", items: { type: "object", additionalProperties: true } } } } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
ctrl.dashboard,
)
dashboardRouter.put(
'/site-mode',
// #swagger.tags = ['Admin · Dashboard']
// #swagger.summary = 'Set site mode (admin only)'
// #swagger.description = 'Switch the site between live and maintenance.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/SiteModeRequest" } } } } */
/* #swagger.responses[200] = { description: 'Updated site mode', content: { "application/json": { schema: { $ref: "#/components/schemas/SiteModeState" } } } } */
/* #swagger.responses[400] = { description: 'Validation error', content: { "application/json": { schema: { $ref: "#/components/schemas/ValidationError" } } } } */
/* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
body('mode').isIn(['live', 'maintenance']),
validate,
ctrl.setSiteMode,
)
module.exports = dashboardRouter