docs(website): rebrand website-README to Runic Gateway #2

Merged
whitlocktech merged 1 commits from feature/branding into main 2026-07-18 21:56:45 +00:00

View File

@@ -1,12 +1,15 @@
# UOMysticmoon Website # Runic Gateway Website
Public site, wiki, and protected admin panel for the **UOMysticmoon** private Ultima Online Public site, wiki, and protected admin panel for a private Ultima Online shard — a
shard — a full-stack app in one repo: full-stack app in one repo. Branding is instance-configurable via `BRAND_*` (see
[Branding](#branding)); **UOMysticmoon** is the first instance.
A full-stack app in one repo:
- **Backend** — Node.js + Express REST API (layered `router → controller → model → db`), MariaDB, a provider-agnostic session layer (JWT cookie for web, bearer tokens for mobile, pluggable SSO). - **Backend** — Node.js + Express REST API (layered `router → controller → model → db`), MariaDB, a provider-agnostic session layer (JWT cookie for web, bearer tokens for mobile, pluggable SSO).
- **Frontend** — React + Vite single-page app (public site, wiki, and the admin panel), dark "gothic" theme (Cinzel + Georgia). - **Frontend** — React + Vite single-page app (public site, wiki, and the admin panel), dark "gothic" theme (Cinzel + Georgia).
- **Deploy** — Docker Compose (app + MariaDB) behind a Pangolin reverse proxy. Express serves the built SPA in production. - **Deploy** — Docker Compose (app + MariaDB) behind a Pangolin reverse proxy. Express serves the built SPA in production.
- **Shard link** — a live bridge to the in-game ServUO shard through the **uo-link** sidecar ([UOM/link](https://gitea.whitlocktech.com/UOM/link)): the site ingests a live event feed and makes server-side REST calls to show shard status, economy, staff presence, IDOCs, live activity, and per-character sheets. See [Shard integration (uo-link)](#shard-integration-uo-link). - **Shard link** — a live bridge to the in-game ServUO shard through the **uo-link** sidecar ([RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link)): the site ingests a live event feed and makes server-side REST calls to show shard status, economy, staff presence, IDOCs, live activity, and per-character sheets. See [Shard integration (uo-link)](#shard-integration-uo-link).
The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, schema, security). The design reference is [BACKEND_DESIGN.md](BACKEND_DESIGN.md) (API contract, schema, security).
@@ -133,14 +136,14 @@ to the backend, so the SPA stays same-origin (cookies work).
**1. Start a MariaDB the backend can reach** (published on `localhost:3306`): **1. Start a MariaDB the backend can reach** (published on `localhost:3306`):
```bash ```bash
docker run -d --name uomm-db -p 3306:3306 -e MARIADB_DATABASE=uomysticmoon -e MARIADB_USER=uomm -e MARIADB_PASSWORD=devpass -e MARIADB_ROOT_PASSWORD=rootpass mariadb:11 docker run -d --name rg-db -p 3306:3306 -e MARIADB_DATABASE=runic_gateway -e MARIADB_USER=runic -e MARIADB_PASSWORD=devpass -e MARIADB_ROOT_PASSWORD=rootpass mariadb:11
``` ```
**2. Configure + start the backend** (terminal 1): **2. Configure + start the backend** (terminal 1):
```bash ```bash
cp server/.env.example server/.env cp server/.env.example server/.env
# Set DB_HOST=127.0.0.1, DB_PORT=3306, DB_USER=uomm, DB_PASSWORD=devpass, # Set DB_HOST=127.0.0.1, DB_PORT=3306, DB_USER=runic, DB_PASSWORD=devpass,
# JWT_SECRET=<anything>, ADMIN_USERNAME=admin, ADMIN_PASSWORD=<your password> # JWT_SECRET=<anything>, ADMIN_USERNAME=admin, ADMIN_PASSWORD=<your password>
npm run install-server npm run install-server
npm run server # nodemon → http://localhost:3000 npm run server # nodemon → http://localhost:3000
@@ -249,7 +252,7 @@ actually returns (`400` validation, `401`/`403` auth, `404`, `409` conflicts, `4
**Authentication in the UI** — click **Authorize** and provide either: **Authentication in the UI** — click **Authorize** and provide either:
- `cookieAuth` — the `uomm_token` session cookie (set automatically in the browser after - `cookieAuth` — the session cookie (name `rg_token`, configurable via `COOKIE_NAME`; set automatically in the browser after
`POST /api/v1/auth/login`), or `POST /api/v1/auth/login`), or
- `bearerAuth` — a mobile access token from `POST /api/v1/auth/mobile/login` (sent as - `bearerAuth` — a mobile access token from `POST /api/v1/auth/mobile/login` (sent as
`Authorization: Bearer <token>`). `Authorization: Bearer <token>`).
@@ -274,7 +277,7 @@ not crash).
The site is wired to the live in-game world through **uo-link**, a standalone sidecar service that The site is wired to the live in-game world through **uo-link**, a standalone sidecar service that
runs next to the ServUO shard. Its source lives in a separate repo: runs next to the ServUO shard. Its source lives in a separate repo:
**[UOM/link](https://gitea.whitlocktech.com/UOM/link)**. uo-link speaks the shard's internals and **[RunicGateway/link](https://gitea.whitlocktech.com/RunicGateway/link)**. uo-link speaks the shard's internals and
exposes a small, authenticated HTTP + WebSocket API; this website is a *client* of it. The shard exposes a small, authenticated HTTP + WebSocket API; this website is a *client* of it. The shard
itself is never exposed to the internet — only the sidecar is, and only the website's backend talks itself is never exposed to the internet — only the sidecar is, and only the website's backend talks
to it. to it.
@@ -282,7 +285,7 @@ to it.
### How it works ### How it works
``` ```
ServUO shard ──▶ uo-link sidecar (UOM/link) ──▶ website backend ──▶ browser ServUO shard ──▶ uo-link sidecar (RunicGateway/link) ──▶ website backend ──▶ browser
REST + WebSocket, bearer-auth ingest + REST same-origin JSON/SSE REST + WebSocket, bearer-auth ingest + REST same-origin JSON/SSE
``` ```
@@ -348,19 +351,20 @@ Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.
| `PORT` | `3000` | server listens on `0.0.0.0:PORT` | | `PORT` | `3000` | server listens on `0.0.0.0:PORT` |
| `UPLOAD_DIR` | `<server>/uploads` | where post images are written (`/app/uploads`, volume-mounted, in Compose) | | `UPLOAD_DIR` | `<server>/uploads` | where post images are written (`/app/uploads`, volume-mounted, in Compose) |
| `DB_HOST` / `DB_PORT` | `db` / `3306` | `db` in Compose; `127.0.0.1` for local dev | | `DB_HOST` / `DB_PORT` | `db` / `3306` | `db` in Compose; `127.0.0.1` for local dev |
| `DB_NAME` / `DB_USER` / `DB_PASSWORD` | `uomysticmoon` / `uomm` / — | app database credentials | | `DB_NAME` / `DB_USER` / `DB_PASSWORD` | `runic_gateway` / `runic` / — | app database credentials |
| `DB_ROOT_PASSWORD` | — | MariaDB root (Compose only) | | `DB_ROOT_PASSWORD` | — | MariaDB root (Compose only) |
| `JWT_SECRET` | — | **required** — long random string; signs session, mobile, and SSO-flow tokens | | `JWT_SECRET` | — | **required** — long random string; signs session, mobile, and SSO-flow tokens |
| `JWT_EXPIRES_IN` | `1d` | web session token + cookie lifetime | | `JWT_EXPIRES_IN` | `1d` | web session token + cookie lifetime |
| `COOKIE_SECURE` | `auto` | `auto` = Secure only over HTTPS (works on LAN HTTP + Pangolin HTTPS) | | `COOKIE_SECURE` | `auto` | `auto` = Secure only over HTTPS (works on LAN HTTP + Pangolin HTTPS) |
| `COOKIE_NAME` | `uomm_token` | | | `COOKIE_NAME` | `rg_token` | changing it on a live instance invalidates existing sessions |
| `BRAND_*` | Runic Gateway | instance branding (name, tagline, colors, logo/hero/favicon) — see [Branding](#branding) |
| `SECRET_ENC_KEY` | — | **required in prod** — key for AES-256-GCM encryption of stored OAuth client secrets. Dev falls back to a key derived from `JWT_SECRET` (with a warning) | | `SECRET_ENC_KEY` | — | **required in prod** — key for AES-256-GCM encryption of stored OAuth client secrets. Dev falls back to a key derived from `JWT_SECRET` (with a warning) |
| `APP_BASE_URL` | — | public base URL, used to build the SSO OAuth `redirect_uri` (`${APP_BASE_URL}/api/v1/auth/sso/:provider/callback`). Set in prod to match what you register with Google/Discord; if unset it is derived from the request (fine for local dev) | | `APP_BASE_URL` | — | public base URL, used to build the SSO OAuth `redirect_uri` (`${APP_BASE_URL}/api/v1/auth/sso/:provider/callback`). Set in prod to match what you register with Google/Discord; if unset it is derived from the request (fine for local dev) |
| `MOBILE_ACCESS_TTL` | `15m` | mobile bearer **access** token lifetime (short-lived) | | `MOBILE_ACCESS_TTL` | `15m` | mobile bearer **access** token lifetime (short-lived) |
| `MOBILE_REFRESH_TTL_DAYS` | `30` | mobile **refresh** token lifetime (long-lived, rotated on use) | | `MOBILE_REFRESH_TTL_DAYS` | `30` | mobile **refresh** token lifetime (long-lived, rotated on use) |
| `TRUST_PROXY` | `1` | reverse-proxy trust for correct `req.ip` / `req.secure` (rate limiting, backoff, bot-ban). Pin to the proxy hop's LAN IP in prod. A blanket `true` is rejected (coerced to `1`) to block `X-Forwarded-For` spoofing | | `TRUST_PROXY` | `1` | reverse-proxy trust for correct `req.ip` / `req.secure` (rate limiting, backoff, bot-ban). Pin to the proxy hop's LAN IP in prod. A blanket `true` is rejected (coerced to `1`) to block `X-Forwarded-For` spoofing |
| `DEBUG_TRUST_PROXY` | `0` | `1` logs raw peer address + `X-Forwarded-For` + resolved `req.ip` per request (to verify/refresh the proxy IP). Noisy — leave off | | `DEBUG_TRUST_PROXY` | `0` | `1` logs raw peer address + `X-Forwarded-For` + resolved `req.ip` per request (to verify/refresh the proxy IP). Noisy — leave off |
| `TOTP_ISSUER` | `UOMysticmoon` | label shown in authenticator apps for optional per-user 2FA | | `TOTP_ISSUER` | `BRAND_NAME` | label shown in authenticator apps for optional per-user 2FA |
| `TOTP_CHALLENGE_TTL` | `5m` | lifetime of the short-lived post-password "awaiting code" step | | `TOTP_CHALLENGE_TTL` | `5m` | lifetime of the short-lived post-password "awaiting code" step |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | — | first-admin bootstrap (first boot only) | | `ADMIN_USERNAME` / `ADMIN_PASSWORD` | — | first-admin bootstrap (first boot only) |
| _Email_ | — | configured in Admin → Settings → Email (Gmail OAuth2), not via env; recipient = `contact_email` setting | | _Email_ | — | configured in Admin → Settings → Email (Gmail OAuth2), not via env; recipient = `contact_email` setting |
@@ -372,6 +376,32 @@ Copy `.env.example` (Compose) or `server/.env.example` (local) and fill in. **`.
--- ---
## Branding
Instance identity is data, not code — set via `BRAND_*` env vars, so one prebuilt
image can run as any shard. With none set, everything renders as **Runic Gateway**.
| Var | What |
|---|---|
| `BRAND_NAME` / `BRAND_SHORT_NAME` | display name (full / short-in-prose) |
| `BRAND_TAGLINE` / `BRAND_DESCRIPTION` | tagline + meta/OG description |
| `BRAND_CONTACT_EMAIL` / `BRAND_URL` | contact + canonical URL (for OG/absolute links) |
| `BRAND_ACCENT_COLOR` | theme `--accent` (web) + Discord embed color |
| `BRAND_LOGO` / `BRAND_HERO` / `BRAND_FAVICON` | image paths under the `/brand` mount, or absolute URLs |
**How it flows:** text/colors reach the SPA at runtime through the public settings
API (`SiteContext`), so no rebuild is needed; the server templates `index.html`
`<title>`/meta/OG/favicon at boot; emails, TOTP issuer, and the Discord bot read
`BRAND_*` directly. The admin-editable **site title** and **contact email**
settings override `BRAND_NAME` / `BRAND_CONTACT_EMAIL` when set. Image assets are
delivered from the `./brand` bind-mount (see `brand/README.md`).
**UOMysticmoon** is the first instance — [`.env.uomysticmoon.example`](https://gitea.whitlocktech.com/RunicGateway/website/src/branch/main/.env.uomysticmoon.example)
holds the exact `BRAND_*` + infra (`DB_NAME`/`DB_USER`/`COOKIE_NAME`) pinning to
run this repo as UOMysticmoon.
---
## Security ## Security
**Session & authorization** **Session & authorization**