Files
Module-uo/server/router/admin/uoLink.router.js
wtclaude 044211fd41
Some checks failed
PR Checks / client-build (pull_request) Successful in 14s
PR Checks / frozen-manifest (pull_request) Failing after 34s
PR Checks / server-tests (pull_request) Successful in 8m49s
fix(docs): repair the escaped apostrophes, and document this module's env vars
Nineteen `#swagger` descriptions carried a `\'` inside a single-quoted string.
That is correct JavaScript and wrong here: swagger-autogen does not evaluate the
annotation as JS, so the backslash survives into the spec and Swagger UI renders
"the shard\'s published ruleset" to a reader. Replaced with a typographic
apostrophe, which the same files already use elsewhere.

Found by opening /api/docs in a browser against a real core with this module
installed — the fragment was valid JSON, the paths were right, every test passed,
and it was still wrong on screen. Nothing that reads the artifact can see this;
only reading the rendered page can.

Also documents the four environment variables this module reads
(UOLINK_BASE_URL / _WS_URL / _PROTOCOL, TOWNCRIER_DURATION_SEC). Core's
.env.example is dropping them in the paired website PR: they were never core's,
and a half-copy in two repos goes stale silently.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 22:57:00 -05:00

138 lines
8.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 gameSignup = require('../../utils/gameSignup')
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,
)
// ── Game-account signup mode ───────────────────────────────────────────────
//
// New in slice 3, and new only in the sense that the field moved: core's Site
// Settings has carried `game_account_signup` since long before the extraction,
// and its help text has always been about a game server. The setting key and its
// stored value are unchanged, so an existing instance keeps its configured mode.
uoLinkRouter.get(
'/signup-mode',
// #swagger.tags = ['Admin · Shard']
// #swagger.summary = 'Get the game-account signup mode (admin only)'
// #swagger.description = 'Whether the site offers game-account creation, and in which direction. The shards own SignupMode (Bridge.cfg) must agree: website/hybrid accept site-created accounts, game refuses them.'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.responses[200] = { description: 'The configured mode and the legal values', content: { "application/json": { schema: { type: "object", properties: { mode: { type: "string" }, modes: { type: "array", items: { type: "string" } } } } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
uoLink.getSignupMode,
)
uoLinkRouter.put(
'/signup-mode',
// #swagger.tags = ['Admin · Shard']
// #swagger.summary = 'Set the game-account signup mode (admin only)'
// #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }]
/* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["mode"], properties: { mode: { type: "string", enum: ["disabled","website","hybrid","game"] } } } } } } */
/* #swagger.responses[200] = { description: 'The saved mode', content: { "application/json": { schema: { type: "object", properties: { mode: { type: "string" } } } } } } */
/* #swagger.responses[400] = { description: 'Unknown mode', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
/* #swagger.responses[403] = { description: 'Admin role required', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
adminOnly,
// Validated here as well as in gameSignup.setMode: the list is the same list,
// and the difference is the answer. A rejected value must be a 400 naming the
// field, not a 500 from a thrown Error the controller could only guess about.
body('mode').isIn(gameSignup.MODES),
validate,
uoLink.saveSignupMode,
)
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/UoTownCrierRequest" } } } } */
/* #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