fix(server): own game-account signup, and repair the gate slice 1 broke

`POST /player/shard/account` and its staff twin have answered 500 for every
caller since slice 1: the ported controller called
`settings.isGameAccountSignupEnabled()`, which is a member of core's settings
model and not of `ctx.settings` — three functions, deliberately. The call was
`undefined(...)`, the TypeError landed in the catch, and no test reached the
branch.

The gate now lives on the side that uses it (`utils/gameSignup.js`), which is
also where the policy belongs: the setting's own help text names Bridge.cfg and
says the shard's SignupMode must agree, and core cannot own a sentence about a
UO shard. The admin field moves to this module's Shard page and the derived
flag onto `/public/shard/features`, beside the visibility flags the same
callers already read.

The setting KEY is unchanged. Renaming `game_account_signup` would silently
reset every configured instance to `disabled` on upgrade, with players
reporting broken signup as the only clue — the same grandfathering as
`spawn_atlas_servuo_path` and the seven stream ids.

Both regression tests were shown to fail against the bug before it was fixed.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:00:37 -05:00
parent 28f4b9afe2
commit 493cf296ab
5 changed files with 305 additions and 2 deletions

View File

@@ -24,6 +24,7 @@ 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()
@@ -58,6 +59,42 @@ uoLinkRouter.put(
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 shard\'s 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']