Initial commit: UOMysticmoon backend (Express + MariaDB + JWT)

- 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>
This commit is contained in:
2026-06-26 20:58:32 -05:00
commit eef79e2403
41 changed files with 4195 additions and 0 deletions

88
README.md Normal file
View File

@@ -0,0 +1,88 @@
# 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.