# UOMysticmoon Website Public site, wiki, and protected admin panel for the UOMysticmoon private Ultima Online shard. Built on the `serverlinkr` layered pattern: **Express + MariaDB + JWT** backend and a **React + Vite** frontend in the same repo, deployed with **Docker Compose** behind a **Pangolin** reverse proxy. > Build order: **(1) backend** (this phase) → (2) frontend design (Claude Design) → > (3) frontend coding. See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) for the full design. ## Layout ``` server/ Express API (router → controller → model → db), MariaDB schema + seed client/ React + Vite SPA (added in the frontend phase) Dockerfile, docker-compose.yml, .env.example ``` ## Quick start (local dev) ```bash # 1. Start a MariaDB (or use your own and set DB_* in server/.env) docker compose up -d db # 2. Configure + install cp server/.env.example server/.env # edit DB_*, JWT_SECRET, ADMIN_USERNAME/PASSWORD npm run install-server # 3. Run the API (creates tables, seeds defaults + first admin on boot) npm run server # http://localhost:3000 (API at /api/v1) ``` `GET /api/health` → `{ "status": "ok" }` confirms it's up. ## Deploy (Docker Compose) ```bash cp .env.example .env # fill in DB creds, JWT_SECRET, admin, SMTP docker compose up -d --build # app on 0.0.0.0:3000, MariaDB on the internal network ``` Point Pangolin at the `app` container on port 3000. The auth cookie auto-detects HTTPS, so the admin panel works both via the LAN IP (HTTP) and through the proxy (HTTPS). The full app image build requires `client/` (frontend phase); until then the server runs API-only. ## Key endpoints | Group | Base | Auth | |---|---|---| | Auth | `/api/v1/auth` (`login`, `logout`, `me`) | cookie | | Public | `/api/v1/public` (`settings`, `status`, `posts/:category`, `wiki`, `contact`) | none | | Admin | `/api/v1/admin` (dashboard, site-mode, posts, wiki, settings, activity, users) | cookie (admin) | See [BACKEND_DESIGN.md](BACKEND_DESIGN.md) §4 for the complete contract. ## Security notes JWT in an httpOnly cookie · bcrypt hashing · login rate limiting · admin routes `noindex` · first admin seeded from env (no hardcoded credentials) · `.env` is git-ignored. SMTP is optional — the contact form falls back to a `mailto:` link when SMTP is not configured. ## 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":"..."} ``` What's 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. Passwords and request bodies are never logged. | Env | Default | Meaning | |---|---|---| | `LOG_LEVEL` | `info` | console verbosity | | `FILE_LOG_LEVEL` | `debug` | file verbosity (keeps a full record) | | `LOG_TO_FILE` | `true` | set `false` for console-only | | `LOG_DIR` | `/logs` (`/app/logs` in Docker) | log directory | | `LOG_FILE` | `app.log` | log file name | In Docker the log file is bind-mounted to `./logs/app.log` on the host; `docker compose logs -f app` also shows the console stream.