feat(brand): BRAND_* env scheme — instance branding without a rebuild
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

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.
This commit is contained in:
2026-07-18 02:20:04 -05:00
parent 1bb9e3c3c3
commit 7a08546da6
49 changed files with 389 additions and 119 deletions

View File

@@ -13,6 +13,11 @@
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' })
const pkg = require('../package.json')
const brand = require('../src/config/brand')
// Cookie name is env-configurable (COOKIE_NAME); the spec documents whatever this
// build targets. This is a build-time artifact — regenerate with `npm run swagger`.
const COOKIE_NAME = process.env.COOKIE_NAME || 'rg_token'
const outputFile = './swagger/swagger-output.json'
@@ -23,13 +28,13 @@ const routes = ['./src/app.js']
const doc = {
info: {
title: 'UOMysticmoon API',
title: `${brand.name} API`,
version: pkg.version,
description:
'REST API for the UOMysticmoon website, wiki and admin panel — a private ' +
`REST API for the ${brand.name} website, wiki and admin panel — a private ` +
'Ultima Online shard.\n\n' +
'### Authentication\n' +
'- **Web / admin panel** uses an httpOnly session cookie (`uomm_token`) issued by ' +
`- **Web / admin panel** uses an httpOnly session cookie (\`${COOKIE_NAME}\`) issued by ` +
'`POST /api/v1/auth/login` (plus `/login/totp` when 2FA is enabled).\n' +
'- **Native / mobile clients** use bearer access tokens from ' +
'`POST /api/v1/auth/mobile/login`, refreshed via `/auth/mobile/refresh`.\n\n' +
@@ -67,7 +72,7 @@ const doc = {
cookieAuth: {
type: 'apiKey',
in: 'cookie',
name: 'uomm_token',
name: COOKIE_NAME,
description: 'Session JWT set as an httpOnly cookie by POST /api/v1/auth/login.',
},
// Native/mobile clients — Authorization: Bearer <accessToken>.
@@ -450,7 +455,7 @@ const doc = {
type: 'object',
description: 'Enrollment material returned by POST /account/totp/setup.',
properties: {
otpauthUrl: { type: 'string', example: 'otpauth://totp/UOMysticmoon:admin?secret=...' },
otpauthUrl: { type: 'string', example: `otpauth://totp/${brand.name}:admin?secret=...` },
qr: { type: 'string', description: 'QR code as a data: URL.', example: 'data:image/png;base64,iVBORw0KGgo...' },
},
},