- Layered API (router -> controller -> model -> db), serverlinkr pattern - Public / auth / admin route groups; posts, wiki, settings, users, activity models - JWT httpOnly-cookie auth (Secure auto-detected: LAN HTTP + Pangolin HTTPS) - Site LIVE/MAINTENANCE mode with admin preview bypass - Dual file+console logging (info/warn/error/debug) + HTTP access logs - Docker Compose (app + MariaDB), schema.sql + seed, .env.example - Verified end-to-end against MariaDB (27/27 smoke checks) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
3.6 KiB
Markdown
89 lines
3.6 KiB
Markdown
# 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` | `<server>/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.
|