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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# ─── UOMysticmoon server — local dev environment ───
|
||||
# ─── Runic Gateway server — local dev environment ───
|
||||
# Copy to server/.env for running `npm run dev` outside Docker.
|
||||
# (In Docker, the root .env / docker-compose provides these instead.)
|
||||
|
||||
@@ -18,14 +18,14 @@ LOG_TO_FILE=true # set false for console-only
|
||||
# Point at a local or Dockerized MariaDB
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_NAME=uomysticmoon
|
||||
DB_USER=uomm
|
||||
DB_NAME=runic_gateway
|
||||
DB_USER=runic
|
||||
DB_PASSWORD=change-me-db-password
|
||||
|
||||
JWT_SECRET=dev-only-change-me
|
||||
JWT_EXPIRES_IN=1d
|
||||
COOKIE_SECURE=auto
|
||||
COOKIE_NAME=uomm_token
|
||||
COOKIE_NAME=rg_token
|
||||
|
||||
# Encryption key for secrets stored at rest (OAuth client secrets in auth_providers).
|
||||
# Any string — hashed to a 256-bit AES-GCM key. REQUIRED in production; in dev an
|
||||
@@ -62,7 +62,8 @@ TRUST_PROXY=1
|
||||
DEBUG_TRUST_PROXY=0
|
||||
|
||||
# Optional TOTP two-factor (opt-in per user).
|
||||
TOTP_ISSUER=UOMysticmoon
|
||||
# TOTP_ISSUER defaults to BRAND_NAME; BRAND_* live in the root .env (see root .env.example)
|
||||
TOTP_ISSUER=Runic Gateway
|
||||
# How long the "password verified, awaiting code" step stays valid.
|
||||
TOTP_CHALLENGE_TTL=5m
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- UOMysticmoon database schema (MariaDB)
|
||||
-- Runic Gateway database schema (MariaDB)
|
||||
-- Run automatically by the MariaDB container (docker-entrypoint-initdb.d) on a
|
||||
-- fresh volume, and idempotently by ensureSchema() on every server boot.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const settingsDb = require('../src/model/settings/settings.db')
|
||||
const wikiDb = require('../src/model/wiki/wiki.db')
|
||||
const users = require('../src/model/users/users.model')
|
||||
const { ensureSchema, close } = require('../src/utils/db')
|
||||
const brand = require('../src/config/brand')
|
||||
|
||||
const log = require('../src/utils/logger')('seed')
|
||||
|
||||
@@ -13,20 +14,20 @@ const DEFAULT_SETTINGS = {
|
||||
site_mode_changed_at: '',
|
||||
site_mode_changed_by: '',
|
||||
maintenance_message:
|
||||
'Mysticmoon is being shaped beneath a midnight sky. The site will return soon.',
|
||||
`${brand.shortName} is being shaped beneath a midnight sky. The site will return soon.`,
|
||||
status_message: 'In progress.',
|
||||
homepage_teaser:
|
||||
'Mysticmoon is still being shaped beneath a midnight sky. A quiet preview for ' +
|
||||
`${brand.shortName} is still being shaped beneath a midnight sky. A quiet preview for ` +
|
||||
'future news, screenshots, guides, and community notes as the world comes online.',
|
||||
contact_email: process.env.CONTACT_TO || 'UOMysticmoon@gmail.com',
|
||||
site_title: 'UOMysticmoon',
|
||||
contact_email: brand.contactEmail,
|
||||
site_title: brand.name,
|
||||
}
|
||||
|
||||
// Starter wiki sections (editable later via the admin panel).
|
||||
// [slug, title, description, sort_order]
|
||||
const WIKI_CATEGORIES = [
|
||||
['guides', 'Guides', 'Getting started and how-to guides.', 10],
|
||||
['world', 'World & Lore', 'Regions, maps, and the story of Mysticmoon.', 20],
|
||||
['world', 'World & Lore', `Regions, maps, and the story of ${brand.shortName}.`, 20],
|
||||
['gameplay', 'Systems & Gameplay', 'Mechanics, items, monsters, and crafting.', 30],
|
||||
['community', 'Community & Rules', 'Player conduct and shard policies.', 40],
|
||||
]
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "uomysticmoon-server",
|
||||
"name": "runic-gateway-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "uomysticmoon-server",
|
||||
"name": "runic-gateway-server",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "uomysticmoon-server",
|
||||
"name": "runic-gateway-server",
|
||||
"version": "1.0.0",
|
||||
"description": "REST API for the UOMysticmoon website and admin panel",
|
||||
"description": "REST API for the Runic Gateway website and admin panel",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
|
||||
@@ -10,6 +10,7 @@ require('dotenv').config()
|
||||
const swaggerUi = require('swagger-ui-express')
|
||||
|
||||
const apiRouter = require('./router/api.router')
|
||||
const brand = require('./config/brand')
|
||||
const createLogger = require('./utils/logger')
|
||||
const { applyTrustProxy, trustProxyDebug } = require('./utils/trustProxy')
|
||||
const botScore = require('./middleware/botScore')
|
||||
@@ -64,8 +65,41 @@ const SERVER_ROOT = path.join(__dirname, '..')
|
||||
const REPO_ROOT = path.join(SERVER_ROOT, '..')
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(SERVER_ROOT, 'uploads')
|
||||
const CLIENT_DIST = path.join(REPO_ROOT, 'client', 'dist')
|
||||
const BRAND_DIR = process.env.BRAND_DIR || path.join(REPO_ROOT, 'brand')
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true })
|
||||
|
||||
// Escape user/brand text for safe interpolation into the HTML shell.
|
||||
const htmlEscape = (s) =>
|
||||
String(s).replace(
|
||||
/[&<>"']/g,
|
||||
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]),
|
||||
)
|
||||
|
||||
// Template the built index.html <head> with instance branding (title, meta
|
||||
// description, Open Graph/Twitter, favicon). Done once at boot from BRAND_* env,
|
||||
// so the prebuilt SPA image serves per-instance metadata without a rebuild.
|
||||
function renderIndexHtml(html) {
|
||||
const title = htmlEscape(brand.name)
|
||||
const desc = htmlEscape(brand.description)
|
||||
const tags = [
|
||||
`<meta property="og:title" content="${title}" />`,
|
||||
`<meta property="og:description" content="${desc}" />`,
|
||||
'<meta property="og:type" content="website" />',
|
||||
brand.url ? `<meta property="og:url" content="${htmlEscape(brand.url)}" />` : '',
|
||||
brand.logo ? `<meta property="og:image" content="${htmlEscape(brand.logo)}" />` : '',
|
||||
'<meta name="twitter:card" content="summary_large_image" />',
|
||||
`<meta name="twitter:title" content="${title}" />`,
|
||||
`<meta name="twitter:description" content="${desc}" />`,
|
||||
brand.favicon ? `<link rel="icon" href="${htmlEscape(brand.favicon)}" />` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n ')
|
||||
return html
|
||||
.replace(/<title>[\s\S]*?<\/title>/i, `<title>${title}</title>`)
|
||||
.replace(/(<meta\s+name="description"\s+content=")[\s\S]*?("\s*\/?>)/i, `$1${desc}$2`)
|
||||
.replace(/<\/head>/i, ` ${tags}\n </head>`)
|
||||
}
|
||||
|
||||
// Uploaded images — always served, even during maintenance. Force nosniff so a
|
||||
// stored file is never interpreted as anything other than its declared type
|
||||
// (defense in depth alongside helmet's global X-Content-Type-Options, and in
|
||||
@@ -89,7 +123,7 @@ try {
|
||||
res.json(swaggerSpec)
|
||||
})
|
||||
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
|
||||
customSiteTitle: 'UOMysticmoon API docs',
|
||||
customSiteTitle: `${brand.name} API docs`,
|
||||
swaggerOptions: { persistAuthorization: true },
|
||||
}))
|
||||
} catch (err) {
|
||||
@@ -112,15 +146,30 @@ app.use('/api', (req, res) => res.status(404).json({ message: 'Not found' }))
|
||||
// ── Client SPA ────────────────────────────────────────────────────────
|
||||
// Serve the built React app if present; otherwise show a placeholder so the
|
||||
// server is usable API-only before the frontend phase.
|
||||
// Brand assets (logo/hero/favicon) from a mounted directory, used when BRAND_*
|
||||
// paths point at /brand/*. Optional — the defaults live under the SPA's /assets,
|
||||
// so this only matters for a custom mount.
|
||||
if (fs.existsSync(BRAND_DIR)) {
|
||||
app.use(
|
||||
'/brand',
|
||||
express.static(BRAND_DIR, {
|
||||
setHeaders: (res) => res.set('X-Content-Type-Options', 'nosniff'),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (fs.existsSync(path.join(CLIENT_DIST, 'index.html'))) {
|
||||
app.use(express.static(CLIENT_DIST))
|
||||
app.get('*', (req, res) => res.sendFile(path.join(CLIENT_DIST, 'index.html')))
|
||||
// Serve a branded copy of the index.html shell for every SPA route; assets keep
|
||||
// their own cache-friendly static handler.
|
||||
const indexHtml = renderIndexHtml(fs.readFileSync(path.join(CLIENT_DIST, 'index.html'), 'utf8'))
|
||||
app.use(express.static(CLIENT_DIST, { index: false }))
|
||||
app.get('*', (req, res) => res.type('html').send(indexHtml))
|
||||
} else {
|
||||
app.get('*', (req, res) =>
|
||||
res
|
||||
.type('html')
|
||||
.send(
|
||||
'<h1>UOMysticmoon API</h1><p>The web client has not been built yet. ' +
|
||||
`<h1>${htmlEscape(brand.name)} API</h1><p>The web client has not been built yet. ` +
|
||||
'The API is available under <code>/api/v1</code>.</p>',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ require('dotenv').config()
|
||||
const log = require('../utils/logger')('auth')
|
||||
|
||||
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1d'
|
||||
const COOKIE_NAME = process.env.COOKIE_NAME || 'uomm_token'
|
||||
const COOKIE_NAME = process.env.COOKIE_NAME || 'rg_token'
|
||||
// Lifetime of the short-lived "password verified, awaiting TOTP" token.
|
||||
const TOTP_CHALLENGE_TTL = process.env.TOTP_CHALLENGE_TTL || '5m'
|
||||
|
||||
|
||||
46
server/src/config/brand.js
Normal file
46
server/src/config/brand.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// ── Branding (BRAND_*) ─────────────────────────────────────────────────────
|
||||
//
|
||||
// Single source of instance branding. Reads BRAND_* env vars once at startup,
|
||||
// with Runic Gateway defaults, so any instance substitutes its own identity
|
||||
// without a rebuild (the app ships as one prebuilt image).
|
||||
//
|
||||
// How it reaches the UI:
|
||||
// • Text + colors + asset paths are surfaced to the SPA through the public
|
||||
// settings API (settings.model.getPublic → SiteContext). The two fields the
|
||||
// admin can edit (site title, contact email) override these defaults.
|
||||
// • The static index.html shell (title/description/OG/favicon) is templated by
|
||||
// Express at serve time (see src/app.js).
|
||||
// • Server-side consumers (emails, TOTP issuer, API docs) read this directly.
|
||||
//
|
||||
// Image assets are delivered from the /brand mount (BRAND_LOGO/HERO/FAVICON), or
|
||||
// any absolute URL. Runic Gateway ships neutral defaults baked into the image so
|
||||
// an instance with no BRAND_* set still renders.
|
||||
require('dotenv').config()
|
||||
|
||||
const name = process.env.BRAND_NAME || 'Runic Gateway'
|
||||
|
||||
const brand = {
|
||||
name,
|
||||
shortName: process.env.BRAND_SHORT_NAME || name,
|
||||
tagline: process.env.BRAND_TAGLINE || 'an independent private Ultima Online shard',
|
||||
description:
|
||||
process.env.BRAND_DESCRIPTION ||
|
||||
`${name} — an independent private Ultima Online shard. News, screenshots, guides, and community notes.`,
|
||||
contactEmail: process.env.BRAND_CONTACT_EMAIL || process.env.CONTACT_TO || '',
|
||||
url: process.env.BRAND_URL || '',
|
||||
// Visual
|
||||
accent: process.env.BRAND_ACCENT_COLOR || '#7f99bd',
|
||||
logo: process.env.BRAND_LOGO || '', // empty → no logo image rendered
|
||||
hero: process.env.BRAND_HERO || '/assets/img/hero-moon.png',
|
||||
favicon: process.env.BRAND_FAVICON || '', // empty → no favicon link injected
|
||||
}
|
||||
|
||||
// Discord embeds want an int (0xRRGGBB). Parse the accent hex once; fall back to
|
||||
// the default accent if it's malformed.
|
||||
brand.accentInt = (() => {
|
||||
const hex = String(brand.accent).replace('#', '')
|
||||
const n = parseInt(hex, 16)
|
||||
return Number.isNaN(n) ? 0x7f99bd : n
|
||||
})()
|
||||
|
||||
module.exports = brand
|
||||
@@ -1,4 +1,5 @@
|
||||
const settingsDb = require('./settings.db')
|
||||
const brand = require('../../config/brand')
|
||||
|
||||
// Keys safe to expose on the public site.
|
||||
const PUBLIC_KEYS = [
|
||||
@@ -89,6 +90,21 @@ async function getPublic() {
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ const settings = require('./model/settings/settings.model')
|
||||
const revokedSessions = require('./model/revokedSessions/revokedSessions.model')
|
||||
const createLogger = require('./utils/logger')
|
||||
const { evaluateBotInternalKey } = require('./utils/botInternalKey')
|
||||
const brand = require('./config/brand')
|
||||
const pkg = require('../package.json')
|
||||
|
||||
const log = createLogger('server')
|
||||
@@ -27,12 +28,12 @@ const INTERNAL_PORT = Number(process.env.INTERNAL_PORT) || 3001
|
||||
const HOST = '0.0.0.0' // bind all interfaces so Pangolin / the LAN can reach it
|
||||
|
||||
async function start() {
|
||||
log.info(`starting UOMysticmoon server v${pkg.version}`, {
|
||||
log.info(`starting ${brand.name} server v${pkg.version}`, {
|
||||
node: process.version,
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
logFile: createLogger.logFilePath || 'disabled (console only)',
|
||||
db: `${process.env.DB_HOST || '127.0.0.1'}:${process.env.DB_PORT || 3306}/${process.env.DB_NAME || 'uomysticmoon'}`,
|
||||
db: `${process.env.DB_HOST || '127.0.0.1'}:${process.env.DB_PORT || 3306}/${process.env.DB_NAME || 'runic_gateway'}`,
|
||||
cookieSecure: process.env.COOKIE_SECURE || 'auto',
|
||||
email: 'gmail-oauth2 (configured in admin → settings)',
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ const pool = mariadb.createPool({
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'uomysticmoon',
|
||||
database: process.env.DB_NAME || 'runic_gateway',
|
||||
connectionLimit: 5,
|
||||
// Return plain JS numbers, never BigInt — keeps JSON responses clean.
|
||||
insertIdAsNumber: true,
|
||||
|
||||
@@ -14,6 +14,7 @@ const nodemailer = require('nodemailer')
|
||||
const emailConfig = require('../model/emailConfig/emailConfig.model')
|
||||
const authProviders = require('../model/authProviders/authProviders.model')
|
||||
const settings = require('../model/settings/settings.model')
|
||||
const brand = require('../config/brand')
|
||||
const log = require('./logger')('mailer')
|
||||
|
||||
// Ready to send only when enabled, connected (has a refresh token), and we know
|
||||
@@ -76,7 +77,7 @@ async function sendContactMessage({ name, email, message }) {
|
||||
from: fromHeader(config),
|
||||
to,
|
||||
replyTo: email,
|
||||
subject: `UOMysticmoon contact from ${name || 'a visitor'}`,
|
||||
subject: `${brand.name} contact from ${name || 'a visitor'}`,
|
||||
text: `From: ${name || 'unknown'} <${email || 'no email'}>\n\n${message}`,
|
||||
})
|
||||
await emailConfig.recordStatus({ status: 'connected', statusDetail: 'Last send OK', lastVerifiedAt: new Date() })
|
||||
@@ -110,7 +111,7 @@ async function sendTest(to) {
|
||||
await transport.sendMail({
|
||||
from: fromHeader(config),
|
||||
to: recipient,
|
||||
subject: 'UOMysticmoon email test',
|
||||
subject: `${brand.name} email test`,
|
||||
text: 'This is a test message confirming Gmail OAuth2 email delivery is working.',
|
||||
})
|
||||
await emailConfig.recordStatus({ status: 'connected', statusDetail: 'Test send OK', lastVerifiedAt: new Date() })
|
||||
@@ -139,9 +140,9 @@ async function sendInvite({ to, acceptUrl, role, invitedByName }) {
|
||||
await transport.sendMail({
|
||||
from: fromHeader(config),
|
||||
to,
|
||||
subject: 'Your UOMysticmoon invitation',
|
||||
subject: `Your ${brand.name} invitation`,
|
||||
text:
|
||||
`You have been invited${by} to join UOMysticmoon${roleLabel}.\n\n` +
|
||||
`You have been invited${by} to join ${brand.name}${roleLabel}.\n\n` +
|
||||
`Accept your invitation and set up your account here:\n${acceptUrl}\n\n` +
|
||||
`This link is single-use and will expire. If you weren't expecting this, you can ignore it.`,
|
||||
})
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
const speakeasy = require('speakeasy')
|
||||
const QRCode = require('qrcode')
|
||||
|
||||
const ISSUER = process.env.TOTP_ISSUER || 'UOMysticmoon'
|
||||
const brand = require('../config/brand')
|
||||
|
||||
const ISSUER = process.env.TOTP_ISSUER || brand.name
|
||||
|
||||
// Generate a new secret. Returns the base32 secret to persist plus the otpauth
|
||||
// URL to encode in a QR code.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "UOMysticmoon API",
|
||||
"title": "Runic Gateway API",
|
||||
"version": "1.0.0",
|
||||
"description": "REST API for the UOMysticmoon 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 `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\nEndpoints under `/api/v1/admin/**` require a valid session; some are further restricted to the `admin` role (editors are limited to content)."
|
||||
"description": "REST API for the Runic Gateway website, wiki and admin panel — a private Ultima Online shard.\n\n### Authentication\n- **Web / admin panel** uses an httpOnly session cookie (`rg_token`) 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\nEndpoints under `/api/v1/admin/**` require a valid session; some are further restricted to the `admin` role (editors are limited to content)."
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@@ -8861,7 +8861,7 @@
|
||||
"cookieAuth": {
|
||||
"type": "apiKey",
|
||||
"in": "cookie",
|
||||
"name": "uomm_token",
|
||||
"name": "rg_token",
|
||||
"description": "Session JWT set as an httpOnly cookie by POST /api/v1/auth/login."
|
||||
},
|
||||
"bearerAuth": {
|
||||
@@ -11718,7 +11718,7 @@
|
||||
},
|
||||
"example": {
|
||||
"type": "string",
|
||||
"example": "otpauth://totp/UOMysticmoon:admin?secret=..."
|
||||
"example": "otpauth://totp/Runic Gateway:admin?secret=..."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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...' },
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user