Files
website/server/src/model/settings/settings.model.js
Claude 7a08546da6
All checks were successful
PR Checks / client-build (pull_request) Successful in 9m24s
PR Checks / server-tests (pull_request) Successful in 10m33s
PR Checks / bot-install (pull_request) Successful in 9m20s
feat(brand): BRAND_* env scheme — instance branding without a rebuild
Replace baked-in UOM/MysticMoon/UOMysticmoon branding with a BRAND_* env
scheme so one prebuilt image runs as any shard; UOMysticmoon becomes the
first tenant that sets these vars rather than a special case in the code.

Architecture (chosen because the app ships as a prebuilt image):
- server/src/config/brand.js + bot/src/brand.js read BRAND_* once at boot,
  with Runic Gateway defaults.
- Text/colors reach the SPA at RUNTIME through the existing public settings
  API (settings.model.getPublic -> SiteContext), so no client rebuild. The
  admin-editable site title + contact email still override BRAND_NAME/email.
- SiteContext applies BRAND_ACCENT_COLOR to the --accent CSS var at runtime.
- Express templates the built index.html <title>/description/OG/favicon at
  serve time from BRAND_* (renderIndexHtml in app.js).
- Server-side consumers read brand directly: emails, TOTP issuer, API docs,
  boot logs, HTML error page. Bot uses it for embed color + logs.

Assets: logo/hero/favicon delivered from a ./brand:/app/brand bind-mount
(BRAND_LOGO/HERO/FAVICON), with neutral defaults baked in; hero falls back
to a built-in image when unset.

Scope: also genericized package.json names (uomysticmoon-* -> runic-gateway-*)
and the DB_NAME/DB_USER/COOKIE_NAME code defaults (runic_gateway/runic/
rg_token). Production keeps its real values by pinning them in .env — see
.env.uomysticmoon.example, which reproduces the exact UOMysticmoon identity
(proof the substitution works). Changing a deployed COOKIE_NAME invalidates
existing sessions, so UOMysticmoon pins uomm_token.

Verified: 193 server tests pass, client builds, app.js loads + templates the
built index.html, brand transform injects title/description/OG/favicon.
2026-07-18 02:20:04 -05:00

127 lines
4.3 KiB
JavaScript

const settingsDb = require('./settings.db')
const brand = require('../../config/brand')
// Keys safe to expose on the public site.
const PUBLIC_KEYS = [
'site_mode',
'maintenance_message',
'status_message',
'homepage_teaser',
'contact_email',
'site_title',
'hero_layout', // portal hero composition (JSON). Draft key stays admin-only.
]
// Player self-registration mode. Stored under the 'player_registration' key.
// NOTE: the raw value is never exposed publicly — getPublic() derives boolean
// availability flags from it instead (see below).
const REGISTRATION_KEY = 'player_registration'
const REGISTRATION_MODES = ['disabled', 'password', 'sso', 'both']
// Resolve the registration mode, defaulting to 'disabled' (and coercing any
// unexpected stored value back to 'disabled' so a bad row can't open sign-up).
async function getRegistrationMode() {
const value = await settingsDb.get(REGISTRATION_KEY)
return REGISTRATION_MODES.includes(value) ? value : 'disabled'
}
// Derived, public-safe availability flags for the register page.
function registrationFlags(mode) {
return {
password: mode === 'password' || mode === 'both',
sso: mode === 'sso' || mode === 'both',
}
}
// Game-account signup (Protocol 2.0). The admin picks who mints game accounts:
// disabled — the site never offers game-account creation (link-only).
// website — the site is the authority (offer creation; pair with the shard in
// website mode + AutoCreateAccounts=false).
// hybrid — either side may create (the site offers creation).
// game — the game server is the authority; the site does NOT offer creation.
// The site OFFERS creation only for 'website'/'hybrid'; the shard's own SignupMode
// (Bridge.cfg) still has the final say and may 403 a call regardless.
const GAME_SIGNUP_KEY = 'game_account_signup'
const GAME_SIGNUP_MODES = ['disabled', 'website', 'hybrid', 'game']
const GAME_SIGNUP_OFFER = ['website', 'hybrid']
async function getGameSignupMode() {
const v = await settingsDb.get(GAME_SIGNUP_KEY)
return GAME_SIGNUP_MODES.includes(v) ? v : 'disabled'
}
async function isGameAccountSignupEnabled() {
return GAME_SIGNUP_OFFER.includes(await getGameSignupMode())
}
async function get(key) {
return settingsDb.get(key)
}
async function set(key, value, updatedBy = null) {
return settingsDb.set(key, value, updatedBy)
}
async function setMany(obj, updatedBy = null) {
for (const [key, value] of Object.entries(obj)) {
await settingsDb.set(key, value, updatedBy)
}
}
async function getAll() {
const rows = await settingsDb.getAll()
return rows.reduce((acc, row) => {
acc[row.key] = row.value
return acc
}, {})
}
async function getPublic() {
const all = await getAll()
const out = PUBLIC_KEYS.reduce((acc, key) => {
if (all[key] !== undefined) acc[key] = all[key]
return acc
}, {})
// Derived registration availability (never the raw mode). Lets the register
// page show/hide the password form and SSO buttons.
const mode = REGISTRATION_MODES.includes(all[REGISTRATION_KEY]) ? all[REGISTRATION_KEY] : 'disabled'
out.registration = registrationFlags(mode)
// Whether the site offers game-account creation (the shard's own mode still has
// the final say when the call is made). Lets the portal show/hide the form.
const gsMode = GAME_SIGNUP_MODES.includes(all[GAME_SIGNUP_KEY]) ? all[GAME_SIGNUP_KEY] : 'disabled'
out.gameAccountSignup = GAME_SIGNUP_OFFER.includes(gsMode)
// Instance branding (BRAND_* env defaults). The two admin-editable settings —
// site title and contact email — override the env value when set, so existing
// installs keep their DB-configured name; everything else comes from env.
out.brand = {
name: out.site_title || brand.name,
shortName: brand.shortName,
tagline: brand.tagline,
description: brand.description,
contactEmail: out.contact_email || brand.contactEmail,
url: brand.url,
accent: brand.accent,
logo: brand.logo,
hero: brand.hero,
favicon: brand.favicon,
}
return out
}
module.exports = {
get,
set,
setMany,
getAll,
getPublic,
PUBLIC_KEYS,
REGISTRATION_KEY,
REGISTRATION_MODES,
getRegistrationMode,
registrationFlags,
GAME_SIGNUP_KEY,
GAME_SIGNUP_MODES,
getGameSignupMode,
isGameAccountSignupEnabled,
}