diff --git a/docs/ADMIN.md b/docs/ADMIN.md index 55948ce..6dce4cf 100644 --- a/docs/ADMIN.md +++ b/docs/ADMIN.md @@ -2,9 +2,177 @@ Creator/operator-only reference. End users should read `USER-GUIDE.md` instead. -This doc covers everything the creator does that buyers never see: generating -the signing keypair, minting license blobs, the dev vs. production key story, -and how to recover from key loss. +This doc covers everything the creator does that buyers never see: minting +through the live server, where state lives on the box, how to rotate secrets, +generating the signing keypair, the dev vs. production key story, and how to +recover from key loss. + +--- + +## Live deployment (PR 1) + +The license server is running at: + +| URL | What it serves | +|---|---| +| `https://datatools.unalogix.com/` | Marketing site (placeholder — "DataTools — coming soon") | +| `https://licenses.datatools.unalogix.com/health` | Liveness + DB reachability probe | +| `https://licenses.datatools.unalogix.com/internal/*` | nginx-blocked on the public side — accessible only via SSH tunnel | +| Postgres @ `127.0.0.1:5433` (localhost) | DB containing the authoritative `licenses` table | + +**Host**: `46.225.166.142` (Ubuntu 24.04), nginx 1.24, Postgres 16-alpine + FastAPI in Docker. + +**Cert**: Let's Encrypt, covers both subdomains, expires 2026-08-12, auto-renews via `certbot.timer`. + +### On-box state + +| Path | Contents | +|---|---| +| `/srv/datatools-license/` | Deploy root, mode 750, owned by `datatools-api` | +| `/srv/datatools-license/compose.yml` | Production docker-compose definition | +| `/srv/datatools-license/app/` | Git clone of this repo (re-clone or `git pull` to update) | +| `/srv/datatools-license/secrets/` | Mode 750 dir holding `pg_password`, `admin_token`. Files are mode 400, owned UID 10001 (container app user) | +| `/srv/datatools-license/backups/` | Postgres dumps land here (cron not yet wired — see §"Backups" below) | +| `/etc/nginx/sites-available/unalogix` | nginx config for both subdomains | +| `/etc/letsencrypt/live/datatools.unalogix.com/` | TLS cert + key | + +Container names: `datatools-api`, `datatools-postgres`. Both use +`restart: unless-stopped`. + +### Get the admin token + +```bash +ssh michael@46.225.166.142 'sudo cat /srv/datatools-license/secrets/admin_token' +``` + +The token is **never** in git, in environment-variable dumps, or in +`docker inspect`. It lives on disk under mode 400 / UID 10001 (so only +root and the container app user can read it). + +### Rotate the admin token + +Any time it's been shown somewhere it shouldn't, or as routine hygiene: + +```bash +cd /srv/datatools-license +openssl rand -hex 32 > secrets/admin_token +chown 10001:10001 secrets/admin_token +chmod 400 secrets/admin_token +docker compose restart api # ~3 seconds; old token stops working immediately +``` + +### Mint a license from your laptop + +```bash +# 1. Open the SSH tunnel (leave running in a background terminal) +ssh -L 8090:127.0.0.1:8090 michael@46.225.166.142 -N & + +# 2. Set the auth env +export DATATOOLS_ADMIN_TOKEN="$(ssh michael@46.225.166.142 'sudo cat /srv/datatools-license/secrets/admin_token')" +export DATATOOLS_ADMIN_URL=http://127.0.0.1:8090 + +# 3. Mint +python3 -m src.admin_cli mint \ + --name "Buyer Name" \ + --email buyer@example.com \ + --tier core + +# 4. (optional) List or revoke +python3 -m src.admin_cli list --email buyer@example.com +python3 -m src.admin_cli revoke DT1-CORE-xxxx-yyyy --reason "refund" +``` + +The blob lands in the response (and in the `licenses` table). Deliver it +to the buyer however suits — copy-paste into email, attach as `.dtlic`. + +### Inspect / debug + +```bash +# Container status + recent logs +ssh michael@46.225.166.142 'cd /srv/datatools-license && docker compose ps && docker compose logs api --tail 30' + +# Query the licenses table directly +ssh michael@46.225.166.142 'cd /srv/datatools-license && docker compose exec -T postgres \ + psql -U datatools_api -d datatools_licenses -c "SELECT license_key, email, tier, source, expires_at FROM licenses ORDER BY created_at DESC LIMIT 20;"' + +# Public-side health +curl https://licenses.datatools.unalogix.com/health +``` + +### Bring it down / back up / rebuild + +```bash +cd /srv/datatools-license + +# Restart just the API (e.g. after rotating a secret) +docker compose restart api + +# Restart everything +docker compose restart + +# Bring down (DB volume PRESERVED) +docker compose down + +# Bring up +docker compose up -d + +# Rebuild the image after a git pull +cd app && git pull +cd .. +docker compose build && docker compose up -d +docker compose exec api alembic upgrade head # if new migrations +``` + +### Backups (not yet automated) + +Postgres state is the system of record for the customer list — once PR 2 +auto-mints from Gumroad webhooks, losing the DB would mean losing every +buyer record. Schedule a daily dump: + +```bash +# /etc/cron.daily/datatools-license-backup — see SETUP-LICENSE-SERVER.md §9 +``` + +Until that's in place, dump manually before any risky operation: + +```bash +docker compose exec -T postgres \ + pg_dump -U datatools_api datatools_licenses \ + | gzip > backups/db-$(date -u +%Y%m%dT%H%M%SZ).sql.gz +``` + +### Production signing key (not yet rotated) + +The server currently signs with the in-tree dev keypair (no +`DATATOOLS_LICENSE_PRIVKEY_FILE` configured → falls back to +`src/license/_dev_keypair.py`). That matches what the desktop currently +verifies against, so existing buyers continue to work. + +**Before shipping v1.0 to paying buyers**, rotate to a production keypair: + +1. `python scripts/generate_keypair.py` (on a trusted machine). +2. Save the private hex to `/srv/datatools-license/secrets/license_privkey`, + chmod 400, chown 10001:10001. +3. Bake the public hex into the PyInstaller build's + `DATATOOLS_LICENSE_PUBKEY` env. +4. Wire `DATATOOLS_LICENSE_PRIVKEY_FILE` + `DATATOOLS_LICENSE_PUBKEY` + into compose.yml's `api.environment` and add `license_privkey` to + the secrets block. +5. `docker compose restart api`. + +### What's deployed (PR 1) vs queued (PR 2 / 3) + +| Capability | Status | +|---|---| +| Mint API + Postgres + auth | **Live** | +| `datatools-admin` CLI (manual mints) | **Live** | +| `licenses.datatools.unalogix.com/health` public | **Live** | +| Gumroad webhook receiver | **PR 2** | +| Postmark transactional email | **PR 2** | +| Buyer renewal / re-delivery portal | **PR 3** | +| Cloudflare in front (DDoS / WAF) | Deferred (DNS at supercp/cPanel) | +| Production signing keypair | Deferred (still using dev key) | +| Automated DB backups | **Pending** — see §"Backups" | ---