whitlocktech 30c2a30c80 Add hero canvas editor spec (corrected to current code)
Build contract for the WYSIWYG portal-hero editor on hero-feature, derived
from the design doc and corrected against the codebase:
- public settings is a whitelist (getPublic/PUBLIC_KEYS), so hero_layout must
  be added there — the doc's "no backend changes" was wrong
- moon is the reusable MoonDot component; route vs nav live in App.jsx vs
  AdminLayout.jsx; admin content is 1000px (canvas scales to fit)

Locked decisions: full v1, buttons as a first-class element type, pre-populate
the current hero on first run, native Pointer Events for drag. Phased plan
with per-phase exit checks. No schema change (JSON in settings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 02:08:21 -05:00
2026-06-26 21:51:27 -05:00

UOMysticmoon Website

Public site, wiki, and protected admin panel for the UOMysticmoon private Ultima Online shard — a full-stack app in one repo:

  • Backend — Node.js + Express REST API (layered router → controller → model → db), MariaDB, JWT-in-cookie auth.
  • 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.

The design reference is BACKEND_DESIGN.md (API contract, schema, security).


Contents


Tech stack

Layer Tech
Backend Node.js 20+, Express 4, mariadb driver (parameterized SQL, no ORM)
Auth JWT in an httpOnly cookie, bcrypt password hashing
Database MariaDB 11 (own container)
Frontend React 18, Vite 5, React Router 6
Email Nodemailer (SMTP) with a mailto: fallback
Deploy Docker Compose, Pangolin reverse proxy

Project structure

UOMSITE/
├─ server/                     Express API
│  ├─ src/
│  │  ├─ server.js             bootstrap: ensure schema → seed → listen (0.0.0.0)
│  │  ├─ app.js                middleware + static SPA + routes
│  │  ├─ router/v1/            auth / public / admin route groups
│  │  ├─ model/                users · posts · wiki · settings · activity (.model + .db)
│  │  ├─ middleware/           siteMode · noindex · rateLimit · validate
│  │  └─ utils/                auth (JWT/cookies) · db (pool) · mailer · logger
│  ├─ db/                      schema.sql + seed.js
│  └─ .env.example
├─ client/                     React + Vite SPA
│  ├─ src/
│  │  ├─ routes/public/        Portal, Website, News, Screenshots, FiveOnFriday, Newsletter(+Issue), Status, About, Maintenance
│  │  ├─ routes/wiki/          Wiki landing + WikiArticle
│  │  ├─ routes/admin/         AdminLogin, AdminLayout, views/ (Dashboard, Posts, Wiki, Settings, Activity, Users) + editors
│  │  ├─ components/           SiteHeader, SiteFooter, layout, guards, Modal, …
│  │  ├─ contexts/             AuthContext, SiteContext
│  │  ├─ api/client.js         fetch wrapper (sends cookies)
│  │  └─ styles/theme.css      design tokens
│  └─ public/assets/img/       hero image
├─ Dockerfile                  builds client → serves via Express
├─ docker-compose.yml          app + MariaDB
├─ .env.example                root env (used by Compose)
└─ package.json                workspace scripts

Prerequisites

  • Node.js 20+ and npm (Node 22/24 are fine).
  • Docker Desktop (for MariaDB, and for the full Compose deploy).

Setup & run

Option A — Docker Compose (full stack)

The simplest way to run everything. The image installs server deps, builds the React client, and Express serves it; MariaDB runs in its own container; tables + defaults + the first admin are created automatically on first boot.

cp .env.example .env
# Edit .env and set at least:
#   DB_PASSWORD, DB_ROOT_PASSWORD   (any strong values)
#   JWT_SECRET                      (a long random string)
#   ADMIN_USERNAME, ADMIN_PASSWORD  (your first admin login)

docker compose up -d --build
  • App: http://localhost:3000 (binds 0.0.0.0)
  • Health check: GET http://localhost:3000/api/health{ "status": "ok" }
  • Logs: docker compose logs -f app (and ./logs/app.log on the host)
  • Stop: docker compose down (add -v to also wipe the database + uploads volumes)

Option B — Local development (hot reload)

Run the API and the Vite dev server separately. The Vite server proxies /api and /uploads to the backend, so the SPA stays same-origin (cookies work).

1. Start a MariaDB the backend can reach (published on localhost:3306):

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

2. Configure + start the backend (terminal 1):

cp server/.env.example server/.env
# Set DB_HOST=127.0.0.1, DB_PORT=3306, DB_USER=uomm, DB_PASSWORD=devpass,
#     JWT_SECRET=<anything>, ADMIN_USERNAME=admin, ADMIN_PASSWORD=<your password>
npm run install-server
npm run server          # nodemon → http://localhost:3000

3. Start the frontend (terminal 2):

npm run install-client
npm run client          # Vite → http://localhost:5173

Develop at http://localhost:5173 (hot reload). On Windows, the Vite proxy targets 127.0.0.1:3000 to avoid the IPv6-localhost pitfall.

Tip: npm run install-all installs both server and client deps in one go.

Option C — Production build without Docker

Build the SPA and let Express serve it on a single port (still needs a MariaDB + server/.env):

npm run install-all
npm run build           # → client/dist
npm start               # node server → serves API + SPA at http://localhost:3000

First admin & site mode

  • On first boot, if the users table is empty and ADMIN_USERNAME / ADMIN_PASSWORD are set, the first admin is created automatically. You can also run npm run seed. After it exists you may blank those env vars.
  • The site starts in maintenance mode: public visitors see the polished "coming soon" page; the admin login and panel are always reachable.
  • Sign in at /admin/login, then flip Maintenance → Live from the Dashboard. A logged-in admin can preview the live site even while it's in maintenance.

Pages & routes

Public (gated by site mode):

Route Page
/ Portal landing (hero + destinations)
/site Website index (section cards)
/site/news News feed
/site/screenshots Screenshot gallery
/site/five-on-friday Five on Friday
/site/newsletter · /site/newsletter/:id Newsletter list + issue
/site/about · /site/status About · Shard status
/wiki · /wiki/:slug Wiki landing + article (auto table-of-contents)

Admin (cookie auth, noindex):

Route View
/admin/login Sign in
/admin Dashboard (mode toggle, stats, recent activity)
/admin/posts Posts CRUD + publish + image upload
/admin/wiki Wiki pages CRUD
/admin/settings Site settings
/admin/activity Activity log
/admin/users User management

API endpoints

Group Base Auth
Auth /api/v1/auth (login, logout, me) cookie
Public /api/v1/public (settings, status, posts/:category, posts/:category/:idOrSlug, wiki, wiki/:slug, contact) none
Admin /api/v1/admin (dashboard, site-mode, posts, posts/upload, wiki, settings, activity, users) cookie (admin)

Post categories (URL form): news, five-on-friday, newsletter, screenshots. See BACKEND_DESIGN.md §4 for the full contract.


Environment variables

Copy .env.example (Compose) or server/.env.example (local) and fill in. .env is git-ignored.

Var Default Notes
NODE_ENV production
PORT 3000 server listens on 0.0.0.0:PORT
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_ROOT_PASSWORD MariaDB root (Compose only)
JWT_SECRET required — long random string
JWT_EXPIRES_IN 1d token + cookie lifetime
COOKIE_SECURE auto auto = Secure only over HTTPS (works on LAN HTTP + Pangolin HTTPS)
COOKIE_NAME uomm_token
ADMIN_USERNAME / ADMIN_PASSWORD first-admin bootstrap (first boot only)
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS optional; blank → contact form uses mailto:
CONTACT_TO UOMysticmoon@gmail.com contact recipient
CLIENT_ORIGIN http://localhost:5173 enables CORS in dev only
LOG_LEVEL / FILE_LOG_LEVEL info / debug console / file verbosity
LOG_TO_FILE / LOG_DIR / LOG_FILE true / <server>/logs / app.log log file (bind-mounted to ./logs in Docker)

Security

JWT in an httpOnly, SameSite=Lax cookie (Secure auto-detected) · bcrypt hashing · login & contact rate limiting · express-validator on writes · helmet · admin routes noindex + robots.txt disallow · trust proxy for correct client IPs behind Pangolin · first admin seeded from env (no hardcoded credentials) · .env git-ignored. Passwords and request bodies are never logged. SMTP is optional — the contact form falls back to a mailto: link when unconfigured.


Logging

Every log line goes to both the console and a log file, timestamped and leveled (error / warn / info / debug):

2026-06-26T18:55:01.123Z INFO  [server] listening on http://0.0.0.0:3000 ...
2026-06-26T18:55:09.880Z INFO  [http] 192.168.1.40 admin POST /api/v1/auth/login 200 12 ms - 48 bytes
2026-06-26T18:55:14.402Z WARN  [auth] login failed {"username":"root","ip":"192.168.1.40"}
2026-06-26T18:55:20.110Z ERROR [error] GET /api/v1/public/wiki -> 500 ... {"stack":"..."}

Captured: startup config banner, schema/seed steps, HTTP access logs (real client IP via trust proxy, the authenticated admin, method/URL/status/time/size), login success/failure, rate-limit hits, site-mode changes, all errors with stack traces, and graceful shutdown. Console verbosity is LOG_LEVEL; the file keeps the fuller FILE_LOG_LEVEL record. In Docker the file is bind-mounted to ./logs/app.log and docker compose logs -f app shows the console stream.


Deployment behind Pangolin

docker compose up -d --build exposes the app container on 0.0.0.0:3000 (no 127.0.0.1 binding) so Pangolin can reach it. Point a Pangolin resource at app:3000. Because COOKIE_SECURE defaults to auto, the admin login works both directly via the LAN IP over HTTP and through Pangolin over HTTPS — no config change needed. MariaDB stays on the private Compose network (no published port by default); data persists in the dbdata volume, uploads in uploads.

Description
No description provided
Readme 9.8 MiB
Languages
JavaScript 98.7%
CSS 0.9%
C# 0.3%