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:
@@ -10,6 +10,7 @@ const uoLinkConfig = require('../../model/uoLinkConfig/uoLinkConfig.model')
|
||||
const uoLinkClient = require('../../utils/uoLinkClient')
|
||||
const uoLinkSocket = require('../../utils/uoLinkSocket')
|
||||
const shardBroadcast = require('../../utils/shardBroadcast')
|
||||
const gameSignup = require('../../utils/gameSignup')
|
||||
const { activity } = require('../../core')
|
||||
|
||||
const log = require('../../core').logger('admin-uolink')
|
||||
@@ -75,6 +76,34 @@ async function saveConfig(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
// GET /admin/uo-link/signup-mode — whether this site creates game accounts.
|
||||
//
|
||||
// Core's Site Settings carried this field until slice 3, with help text naming
|
||||
// Bridge.cfg. It reads as UO policy because it is: the site's mode and the
|
||||
// shard's own SignupMode have to agree, and only one of those two is core's.
|
||||
async function getSignupMode(req, res) {
|
||||
try {
|
||||
return res.json({ mode: await gameSignup.getMode(), modes: gameSignup.MODES })
|
||||
} catch (err) {
|
||||
log.error('uoLink.getSignupMode', err)
|
||||
return res.status(500).json({ message: 'Internal Server Error' })
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /admin/uo-link/signup-mode
|
||||
async function saveSignupMode(req, res) {
|
||||
const { mode } = req.body
|
||||
try {
|
||||
await gameSignup.setMode(mode, req.user.id)
|
||||
await activity.log({ req, action: 'uoLink.signupMode.update', detail: { mode } })
|
||||
log.info('game-signup mode updated', { by: req.user.username, mode })
|
||||
return res.json({ mode })
|
||||
} catch (err) {
|
||||
log.error('uoLink.saveSignupMode', err)
|
||||
return res.status(500).json({ message: 'Internal Server Error' })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /admin/uo-link/towncrier — publish/replace a town-crier message.
|
||||
async function postTownCrier(req, res) {
|
||||
const { id, lines, durationSec } = req.body
|
||||
@@ -120,4 +149,4 @@ function stream(req, res) {
|
||||
shardBroadcast.subscribe(req, res, 'admin')
|
||||
}
|
||||
|
||||
module.exports = { getConfig, saveConfig, postTownCrier, deleteTownCrier, stream }
|
||||
module.exports = { getConfig, saveConfig, getSignupMode, saveSignupMode, postTownCrier, deleteTownCrier, stream }
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -15,6 +15,7 @@ const shardMarket = require('../../model/shardMarket/shardMarket.model')
|
||||
const uoLinkConfig = require('../../model/uoLinkConfig/uoLinkConfig.model')
|
||||
const broadcast = require('../../utils/shardBroadcast')
|
||||
const visibility = require('../../utils/shardVisibility')
|
||||
const gameSignup = require('../../utils/gameSignup')
|
||||
|
||||
const log = require('../../core').logger('public-shard')
|
||||
|
||||
@@ -386,7 +387,20 @@ async function getFeatures(req, res) {
|
||||
try {
|
||||
const config = await visibility.getConfig()
|
||||
const level = await visibility.viewerLevel(req)
|
||||
return res.json({ level, features: visibility.visibleFeatures(level, config) })
|
||||
return res.json({
|
||||
level,
|
||||
features: visibility.visibleFeatures(level, config),
|
||||
// Whether this site offers game-account creation. Not a visibility flag
|
||||
// and deliberately carried here anyway: it is the same per-viewer,
|
||||
// once-a-session answer, and the alternative is a second endpoint and a
|
||||
// second round-trip for one boolean. It is NOT audience-gated — it says
|
||||
// what the site offers, not what this caller may see, and the portal's
|
||||
// create-account form is behind a session either way.
|
||||
//
|
||||
// Core's public settings carried this until slice 3. It is ours now
|
||||
// (utils/gameSignup.js), because the setting is about a game server.
|
||||
gameAccountSignup: await gameSignup.isEnabled(),
|
||||
})
|
||||
} catch (err) {
|
||||
log.error('shard.getFeatures', err)
|
||||
return res.status(500).json({ message: 'Internal Server Error' })
|
||||
|
||||
Reference in New Issue
Block a user