Commit 4bd5dd1
claude
admin: read-only dashboard for Cloud accounts + drill-down
Problem: once real customers exist on Cloud, we'll need visibility
into who's signed up, how much storage they're using, when they
last backed up, and whether their account is in a weird state
(past_due, canceled). Doing that via Railway SQL console every time
someone emails in is painful. Building the dashboard now, while
there are zero customers, means it exists the day it's needed and
lets us sanity-check the Cloud chain with live data during the
first-customer dry run.
Scope (deliberately narrow):
GET /admin/ — table of all accounts, newest first,
with tier, status, site count, backup
count, total storage, last-backup
timestamp, and signup date
GET /admin/account/{id} — drill-down: summary stats, per-site
breakdown (LEFT JOIN so sites with
zero backups still appear), recent
backups (100 cap), recent web sessions
(session tokens shown truncated to 8
chars so screenshotted pages don't
leak them)
GET /admin/json — same shape as index, JSON format, for
scripting or future CLI integration
Auth: HTTP basic auth against STOCKYARD_ADMIN_PASSWORD env var.
Constant-time password compare to avoid timing leaks. When the env
var is empty, every admin endpoint returns 404 rather than 401 — an
attacker probing /admin/ on a server without admin configured gets
no signal the endpoint exists. Fail-closed.
Read-only by design. No mutation endpoints. Granting tiers,
revoking accounts, deleting backups — all still require SSHing into
Railway and writing SQL. That friction is the biggest protection
against a leaked admin cookie, and keeping the admin surface small
means the footprint to audit is small.
Security headers on every admin response:
Cache-Control: no-store, max-age=0
X-Robots-Tag: noindex, nofollow
HTML rendering uses html/template (auto-escaping), so email
addresses, site slugs, and the like can't inject scripts. JSON
endpoint uses json.NewEncoder(w).Encode.
Session token display: only first 8 chars + ellipsis. An admin who
needs the full token can query the DB; shoulder-surfing or casual
screenshots won't leak it.
Tests (admin_test.go, 9 new, all passing):
- DisabledReturns404 (fail-closed on empty env var)
- UnauthorizedReturns401 (correct 401 + WWW-Authenticate header)
- AuthorizedIndexReturnsHTML (seeded accounts render, HTML type)
- EmptyStateRendersCleanly (no accounts still 200s with empty-state)
- JSONEndpoint (parseable JSON with accounts array)
- AccountDrillDown (account data, sites, backups, version all render)
- AccountDrillDown_NotFound (unknown ID returns 404)
- AccountDrillDown_BadID (non-numeric/negative/zero return 400)
- SecurityHeaders (Cache-Control + X-Robots-Tag set correctly)
Deployment:
Set STOCKYARD_ADMIN_PASSWORD on Railway. Dashboard live immediately.
If the env var is unset, the routes 404 and nothing changes for
customers. Rotate by changing the env var — no code push needed.
Flagged for future:
- Add a refund helper (narrow mutation: mark account refunded + revoke)
- Add search/filter on the index page when the list grows past ~100
- Add a 'resend magic link' button for customer support scenarios
- Add storage-over-time chart once there's enough data to plot1 parent 5932f6d commit 4bd5dd1
3 files changed
Lines changed: 955 additions & 0 deletions
0 commit comments