Restore source files from Google Drive backup

This commit is contained in:
Aryan Singh committed 2026-10-27 12:34:01 -05:00
1 parent dff2b5a9c4
commit 328525b37f
27 files changed
+975 -106

No files matched your search

+174
View File
@@ -0,0 +1,174 @@
# Public read-only mirror (`PUBLIC_READONLY`)
CraftDeck is an **admin** panel: it mounts the Docker socket and the project
directory, runs console commands, edits files, and manages mods. It must only
ever be reachable on a trusted LAN/VPN (see [networking.md](networking.md)).
`PUBLIC_READONLY=1` turns the **same image** into a locked-down, anonymous,
read-only mirror that is safe to expose to the internet — a live "status page"
for a portfolio. It cannot touch the server: no login, no Docker socket, no
RCON, no project files, no secrets. It serves a pre-exported **feed** and
nothing else.
## How it works
```
LAN box (trusted) Public host (untrusted)
┌───────────────────────────┐ ┌────────────────────────────┐
│ dashboard (admin) │ data/dashboard │ feed-export (sidecar) │
│ Docker socket + RCON │ craftdeck.db │ reads DB read-only │
│ writes craftdeck.db ─────┼──────ro────────▶│ writes feed.db │
└───────────────────────────┘ │ │ data/feed │
│ ▼ │
│ dashboard-public │
│ PUBLIC_READONLY=1 │
│ serves feed.db only │
└────────────────────────────┘
```
Two independent guards keep it safe:
1. **Middleware allowlist** (`dashboard/src/proxy.ts`) — default-deny. When the
flag is set, **every** path returns `404` unless it is on a tiny GET-only
allowlist. Non-GET methods are refused before anything else, so every
`POST/PUT/PATCH/DELETE` route (console, power, files, mods, config, auth…)
is unreachable. `404` (not `401/403`) is used so the public deployment
never even hints an admin app exists.
2. **Data isolation** — in public mode the sampler, log watcher, auth watcher,
scheduler, RCON client and Docker client are **never imported** (they are
loaded via dynamic `import()` only in the admin code paths — see
`dashboard/src/server/boot.ts` and the allowed route handlers). All reads
come from the feed file at `FEED_PATH` via `dashboard/src/server/feed.ts`.
### The allowlist (enforced in `src/proxy.ts`)
| Surface | Path | Source in public mode |
|---|---|---|
| Overview page | `GET /` | feed |
| Players page | `GET /players` | feed |
| Performance page | `GET /performance` | feed |
| Health probe | `GET /api/health` | static |
| Server status | `GET /api/server` | feed status blob |
| Metrics history | `GET /api/metrics` | feed `metrics` table |
| Activity | `GET /api/activity` | feed `activity` (join/leave only) |
| Players | `GET /api/players` | feed status (names + counts only) |
| Lag / tick health | `GET /api/lag` | feed `metrics` (no per-player ping) |
| Live stream (SSE) | `GET /api/stream/events` | feed (metrics + join/leave frames only) |
**Everything else → 404**, explicitly including: `/console`, `/access`,
`/security`, `/mods`, `/backups`, `/files`, `/config`, `/schedules`,
`/settings`, `/login`; all of `/api/auth/*`, `/api/admin`, `/api/approved`,
`/api/notifications`, `/api/console`, `/api/server/power`, `/api/world`,
`/api/mods*`, `/api/files*`, `/api/backups*`, `/api/config*`, `/api/schedules`,
`/api/security`, `/api/settings*`, `/api/lag/healthreport`, and
**`/api/stream/logs`** (the raw console stream). Public mode has **no login** —
it is anonymous read-only, so there is no session to protect.
## The feed
`dashboard/scripts/export-feed.mjs` reads the live panel DB **read-only** and
writes a separate SQLite file containing **only**:
- `metrics` — the sampler time-series (last 7 days)
- `activity` — **join/leave events only**, as `(id, ts, type, player)`. The
free-text `detail` column is dropped, so IP addresses that appear on
`connect`/`blocked` rows can never reach the feed.
- `feed_meta` — one small status JSON (running, online names, counts, version)
It **never** copies `panel_users`, `approved_players`, `settings`,
`notifications`, `schedules`, or `ping_samples`. The file is written to a temp
path and atomically renamed, so readers never see a half-written feed; the
reader hot-reloads when the file's mtime changes. A status blob older than 5
minutes is treated as "server offline" so a dead exporter can't show a stale
"online".
## Running it
### Option A — compose sidecars (on the LAN box)
The `public` profile adds two services (excluded from the normal `up`):
```bash
# normal admin stack (unchanged)
docker compose up -d
# add the public mirror + its feed exporter
docker compose --profile public up -d
# the mirror is now on http://<host>:8090 (override with DASHBOARD_PUBLIC_PORT)
```
- `feed-export` — reads `data/dashboard/craftdeck.db` (ro) → writes
`data/feed/feed.db` every `FEED_INTERVAL_SECONDS` (default 30).
- `dashboard-public` — `PUBLIC_READONLY=1`, mounts **only** `data/feed` (ro).
No `docker.sock`, no `./:/project`, no `RCON_PASSWORD`, no
`PANEL_SESSION_SECRET`.
To actually expose it, run `dashboard-public` on a separate untrusted host and
ship `data/feed/feed.db` to it (rsync/scp/object storage). Put TLS + the
`Secure` cookie concerns aside — there is no cookie — but do front it with a
reverse proxy for HTTPS.
### Option B — cron on the LAN box
```cron
# refresh the public feed every minute
* * * * * cd /path/to/minecraft/dashboard && \
SOURCE_DB=../data/dashboard/craftdeck.db FEED_PATH=../data/feed/feed.db \
MC_VERSION=26.2 SERVER_TYPE=FABRIC node scripts/export-feed.mjs >/dev/null 2>&1
```
Then run the public dashboard container/image anywhere with
`PUBLIC_READONLY=1` and `FEED_PATH` pointing at the copied `feed.db`.
### Environment variables
| Var | Where | Meaning |
|---|---|---|
| `PUBLIC_READONLY` | dashboard-public | `1` enables read-only mirror mode |
| `FEED_PATH` | dashboard-public + exporter | path to `feed.db` |
| `SOURCE_DB` | exporter | path to the live panel DB (read-only) |
| `MC_VERSION`, `SERVER_TYPE` | exporter (optional) | shown on the status blob |
| `FEED_INTERVAL_SECONDS` | feed-export sidecar | export cadence (default 30) |
| `DASHBOARD_PUBLIC_PORT` | compose | host port for the mirror (default 8090) |
## Security checklist (verified)
| # | Check | Result |
|---|---|---|
| 1 | No `POST/PUT/PATCH/DELETE` reaches any handler in public mode | **PASS** — `proxy.ts` refuses every non-GET before routing (`method !== "GET" && !== "HEAD" → 404`) |
| 2 | Admin pages/APIs 404 (console, files, mods, backups, config, schedules, settings, access, security, admin, approved, notifications, world, power) | **PASS** — default-deny allowlist; curl matrix below |
| 3 | Auth routes 404; no login in public mode | **PASS** — `GET /api/auth/me`, `POST /api/auth/login`, `GET /login` all 404 |
| 4 | Raw console SSE (`/api/stream/logs`) unreachable | **PASS** — not on the allowlist → 404 |
| 5 | `/api/stream/events` emits **only** `metrics` + join/leave; never notifications or console lines | **PASS** — notification listener not registered in public mode; `mc-event` filtered to `join`/`leave`; `detail`/IP stripped |
| 6 | RCON / Docker / sampler modules never imported in public mode | **PASS** — grep shows no static `server/{docker,rcon,mc,sampler}` import in `boot.ts`, `api.ts`, or any allowed route; error classes live in `server/errors.ts` |
| 7 | Feed excludes `panel_users`, `approved_players`, `settings`, `notifications`, `schedules`, `ping_samples`, and the activity `detail`/IP column | **PASS** — feed has only `metrics`, `activity(id,ts,type,player)`, `feed_meta`; leak scan for hashes/IPs/notification text found nothing |
| 8 | `dashboard-public` has no docker.sock / project dir / RCON or session secret | **PASS** — `docker compose --profile public config` shows only `data/feed:ro` |
| 9 | Admin mode unchanged when the flag is unset | **PASS** — auth still enforced (`/api/server`→401, `/console`→307→login); full panel services boot |
| 10 | Committed `dev-data` cannot ship in an image | **PASS** — `dashboard/.dockerignore` excludes `dev-data` |
### curl verification (booted with `PUBLIC_READONLY=1` + a generated feed)
```
ALLOWED (200): / /players /performance
/api/health /api/metrics /api/server /api/players /api/activity /api/lag
BLOCKED (404): /console /settings /login
GET /api/auth/me · POST /api/auth/login · GET /api/notifications
GET /api/admin · POST /api/console · POST /api/server/power
GET /api/stream/logs · GET /api/world · POST /api/metrics
```
A leak scan across every allowed API response found no password hashes, IP
addresses, or notification content. With a fresh feed the mirror served live
data (`/api/server` running=true, 2 players; `/api/metrics` 360 buckets;
`/api/lag` verdict `all-good`).
## Note on the committed dev database
`dashboard/dev-data/app/craftdeck.db` is git-ignored (root `.gitignore` →
`dashboard/dev-data/`) and its `panel_users` table **does contain a real
scrypt password hash** for the `admin` user (`must_change=1`). It is a local
dev seed, not a production credential, but it is a hash nonetheless — it must
never end up in a built image. `dashboard/.dockerignore` now excludes
`dev-data`, and the public mirror never mounts it. Do not commit this file.