Files
Module-uo/server/router/admin/uoLink.router.js
wtclaude 740a677f92 feat(server): register the routes, the slot, the leg and the boot hooks
The entry point becomes real: five mount prefixes, the admin.users.detail
extension slot, the shard push catalog, the town-crier announce leg and both
lifecycle hooks. module.json declares all of it and the loader checks the
declaration against what register() actually registers, in both directions.

The URLs are byte-identical to the ones core served before the extraction. That
is the whole point of moving the code and not the paths: the shipped Android app
calls POST /api/v1/admin/shard/kick and the Discord bot reads
/api/v1/public/shard/*, and neither knows a module answers now.

Require order is load-bearing and the requires are inside register() because of
it. Every ported file reaches core through ./core, whose members resolve ctx
when called -- but a router does `const express = core.express` at ITS file
scope, which runs the moment it is required. Hoisting these to the top of the
file breaks the module with an error about ctx being missing, from a file that
never mentions it.

boot.js takes the eight UO call sites out of core's server.js. One behavioural
change, deliberate: uoLinkSocket.start() and the sidecar health probe used to
run AFTER the listener bound and now run before it, because onBoot does. start()
returns as soon as the reconnecting client is armed, but the probe is a real
HTTP call, so it is fired and NOT awaited -- an unreachable sidecar must not
hold the site closed. Reporting that the bridge is down is diagnostics; being up
is not a precondition for serving a page.

router/rateLimits.js builds the market limiter through ctx.middleware.rateLimit,
core's factory. The policy is the module's -- only the module knows what its
endpoints cost -- and the plumbing is core's, so there is one express-rate-limit
in the process and one place a breach is logged.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 12:06:46 -05:00

101 lines
5.6 KiB
JavaScript

// Admin · uo-link — the sidecar connection config, the town crier, and the
// staff SSE stream.
//
// Mounted at /api/v1/admin/uo-link by admin/index.js, which already applied
// `noindex, isLoggedIn, staffOnly`. This is where shard integration is
// configured: base/ws URL, bearer token, protocol version and the enabled
// toggle all live in the DB (uoLinkConfig), never in env. The token is
// write-only over this API (SECURITY note in uoLink.controller.js).
//
// /stream is the ADMIN SSE channel — it carries staff audit, cheat detection
// and login attempts on top of the public event kinds. The public/admin
// allowlist split in utils/shardIngest.js is a security boundary; the adminOnly
// gate below is its other half.
//
// The routes keep their `Admin · Shard` swagger tag: retagging is a real
// OpenAPI diff and does not belong in a route-move PR.
//
// Admin-only, and kept as a per-route gate rather than a router-level `use` so
// the middleware chain each route carries is unchanged by the move.
const core = require('../../core')
const express = core.express
const { body, param } = core.validator
const uoLink = require('./uoLink.controller')
const { requireRole, validate } = core.middleware
const uoLinkRouter = express.Router()
const adminOnly = requireRole('admin')
uoLinkRouter.get(
'/config',
// #swagger.tags = ['Admin · Shard']
// #swagger.summary = 'Get uo-link config + live status + ingestion stats (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'Masked config, health and ingestion stats', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
uoLink.getConfig,
)
uoLinkRouter.put(
'/config',
// #swagger.tags = ['Admin · Shard']
// #swagger.summary = 'Save uo-link connection config (admin only)'
// #swagger.description = 'token is write-only — omit/blank it to keep the existing one. Saving (re)starts the WS ingest client.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", properties: { baseUrl: { type: "string" }, wsUrl: { type: "string" }, token: { type: "string" }, protocol: { type: "integer" }, enabled: { type: "boolean" } } } } } } */
/* #swagger.responses[200] = { description: 'Updated config + live status', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[400] = { description: 'Validation error, or missing token while enabling', 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('baseUrl').optional({ values: 'falsy' }).isString().trim().isURL({ require_tld: false, protocols: ['http', 'https'] }),
body('wsUrl').optional({ values: 'falsy' }).isString().trim().isURL({ require_tld: false, protocols: ['ws', 'wss'] }),
body('token').optional({ values: 'falsy' }).isString().trim(),
body('protocol').optional().isInt({ min: 1, max: 99 }),
body('enabled').optional().isBoolean(),
validate,
uoLink.saveConfig,
)
uoLinkRouter.post(
'/towncrier',
// #swagger.tags = ['Admin · Shard']
// #swagger.summary = 'Publish / replace a town-crier message (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TownCrierRequest" } } } } */
/* #swagger.responses[200] = { description: 'Posted', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[400] = { description: 'Rejected (over caps)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[503] = { description: 'Shard unavailable', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
body('id').isString().trim().isLength({ min: 1, max: 64 }),
body('lines').isArray({ min: 1, max: 8 }),
body('lines.*').isString().isLength({ max: 200 }),
body('durationSec').optional().isInt({ min: 1, max: 86400 }),
validate,
uoLink.postTownCrier,
)
uoLinkRouter.delete(
'/towncrier/:id',
// #swagger.tags = ['Admin · Shard']
// #swagger.summary = 'Remove a town-crier message (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
// #swagger.parameters['id'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Town-crier message id.' }
/* #swagger.responses[200] = { description: 'Removed', content: { "application/json": { schema: { type: "object", additionalProperties: true } } } } */
/* #swagger.responses[404] = { description: 'Unknown id', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
param('id').isString().trim().isLength({ min: 1, max: 64 }),
validate,
uoLink.deleteTownCrier,
)
uoLinkRouter.get(
'/stream',
// #swagger.tags = ['Admin · Shard']
// #swagger.summary = 'Full live shard event stream incl. audit/cheat (SSE, admin only)'
/* #swagger.responses[200] = { description: 'An SSE stream (Content-Type: text/event-stream).' } */
adminOnly,
uoLink.stream,
)
module.exports = uoLinkRouter