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.
86 lines
2.7 KiB
JavaScript
86 lines
2.7 KiB
JavaScript
const fs = require('fs')
|
|
const path = require('path')
|
|
const mariadb = require('mariadb')
|
|
require('dotenv').config()
|
|
|
|
const log = require('./logger')('db')
|
|
|
|
const pool = mariadb.createPool({
|
|
host: process.env.DB_HOST || '127.0.0.1',
|
|
port: Number(process.env.DB_PORT) || 3306,
|
|
user: process.env.DB_USER || 'root',
|
|
password: process.env.DB_PASSWORD || '',
|
|
database: process.env.DB_NAME || 'runic_gateway',
|
|
connectionLimit: 5,
|
|
// Return plain JS numbers, never BigInt — keeps JSON responses clean.
|
|
insertIdAsNumber: true,
|
|
bigIntAsNumber: true,
|
|
decimalAsNumber: true,
|
|
// The driver defaults to 'local' — silently serializing bound JS Date
|
|
// params using the HOST MACHINE's local offset instead of the DB session's
|
|
// timezone (discovered via the Discord bot's temp_roles.expires_at coming
|
|
// back hours off in dev). 'auto' negotiates the actual session timezone so
|
|
// Date round-trips correctly regardless of host TZ — affects any write of
|
|
// a JS Date param, e.g. botConfig.model.js's last_connected_at.
|
|
timezone: 'auto',
|
|
})
|
|
|
|
/**
|
|
* Run a parameterized query and release the connection.
|
|
* @param {string} sql
|
|
* @param {Array} [params]
|
|
*/
|
|
async function query(sql, params) {
|
|
const conn = await pool.getConnection()
|
|
try {
|
|
return await conn.query(sql, params)
|
|
} finally {
|
|
conn.release()
|
|
}
|
|
}
|
|
|
|
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'db', 'schema.sql')
|
|
|
|
/**
|
|
* Create tables if they do not exist. Idempotent. Retries while the DB is still
|
|
* coming up (important under docker-compose even with a healthcheck).
|
|
*/
|
|
async function ensureSchema({ retries = 10, delayMs = 2000 } = {}) {
|
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
try {
|
|
const conn = await pool.getConnection()
|
|
try {
|
|
const sql = fs.readFileSync(SCHEMA_PATH, 'utf8')
|
|
// Strip full-line comments first, then split — so a leading comment block
|
|
// doesn't get glued onto (and discard) the statement that follows it.
|
|
const statements = sql
|
|
.split('\n')
|
|
.filter((line) => !line.trim().startsWith('--'))
|
|
.join('\n')
|
|
.split(';')
|
|
.map((s) => s.trim())
|
|
.filter((s) => s.length > 0)
|
|
for (const statement of statements) {
|
|
await conn.query(statement)
|
|
}
|
|
log.info('schema ensured')
|
|
return
|
|
} finally {
|
|
conn.release()
|
|
}
|
|
} catch (err) {
|
|
if (attempt === retries) throw err
|
|
log.warn(`database not ready, retrying (attempt ${attempt}/${retries})`, {
|
|
code: err.code || err.message,
|
|
})
|
|
await new Promise((r) => setTimeout(r, delayMs))
|
|
}
|
|
}
|
|
}
|
|
|
|
async function close() {
|
|
await pool.end()
|
|
}
|
|
|
|
module.exports = { pool, query, ensureSchema, close }
|