Admin Portal

Summary

The admin portal is an internal operations dashboard for user lookup, support triage, and account management during beta. It is embedded in the existing dashboard SPA at golivebro.com/dashboard#admin (no standalone /admin page). Access is gated by an is_admin column on the users table, checked by Go middleware with a 30-second in-memory cache, with BFF defense-in-depth as a secondary gate. The portal was designed with input from a security audit that produced 18 findings (3 CRIT, 5 HIGH, 6 MED, 5 LOW, 4 INFO), all incorporated into the design.

The backend exposes admin API endpoints under /api/v1/ops/* (using “ops” instead of “admin” to avoid ad blocker interference). Read endpoints provide user search (3-character minimum, parameterized ILIKE, cursor pagination max 25), full user profiles, linked OAuth accounts (tokens redacted), auth sessions, relay sessions, usage data, chat subscriptions, and billing events. Mutating endpoints (override plan, toggle unlimited hours, revoke sessions, force-stop relay, verify email, remove user) all require password re-authentication via bcrypt and write to the admin_audit_log table transactionally.

A card drilldown feature was added on 2026-04-01, making all 11 aggregate StatCards clickable with a detail panel showing user lists and Chart.js v4 charts. A single backend endpoint (GET /ops/stats/drilldown?card=<key>) returns chart data (line, bar, or donut) and/or paginated user lists depending on the card. Charts include MRR trend (30d), user growth (30d), per-day signups (7d), tier breakdowns, and daily stream hours.

Timeline

  • 2026-03-29: Admin portal design approved. Incorporated 18 security findings. Architecture: Preact SPA on Cloudflare Pages, BFF proxy to Go control plane, is_admin column gating.
  • 2026-03-29: Implementation plan created. Pre-existing CRIT-01 fix (JWT sub claim was never set in SignSessionJWT, breaking telemy_uid cookie binding for all users) identified as prerequisite. 10+ tasks covering migration 0027, store methods, middleware, handlers, frontend SPA.
  • 2026-04-01: Card drilldown design approved. 11 StatCards mapped to chart types and user lists. Single new backend endpoint, Chart.js v4 integration, <ChartPanel> Preact component, <DrilldownPanel> below card rows.
  • 2026-04-01: Card drilldown implementation plan created. 6 tasks: Chart.js install, backend drilldown store queries, HTTP handler + route, API function, ChartPanel component, DrilldownPanel + StatCard updates.

Current State

The admin portal is live at golivebro.com/dashboard#admin. It serves a single admin (founder, michael@liveoptech.com), granted via manual SQL: UPDATE users SET is_admin = true WHERE email = 'michael@liveoptech.com'.

Database schema includes migration 0027 (is_admin column on users, admin_audit_log table with indexes on created_at DESC and target_user_id).

Frontend structure under web/admin/:

  • pages/search.jsx — user search with 11 StatCards (30s auto-refresh), DrilldownPanel below card rows
  • pages/user-detail.jsx — full user view with 8 collapsible sections + quick actions
  • components/user-table.jsx, section-card.jsx, action-button.jsx, chart-panel.jsx
  • lib/api.js — fetch wrapper for /api/v1/ops/*

Backend endpoints (Go, all under /api/v1/ops/*, JWT + is_admin middleware):

  • GET /ops/stats — 12 aggregate metrics, 30s auto-refresh
  • GET /ops/stats/drilldown?card=<key> — chart data + paginated user list per stat card
  • GET /ops/users?q=<search> — search by email/name/id or keyword filter (operators, beta, free, standard, deactivated, past_due, canceled)
  • GET /ops/users/:id — full user profile
  • GET /ops/users/:id/{oauth-accounts,auth-sessions,relay-sessions,usage,chat-subs,billing-events} — sub-resources
  • POST /ops/users/:id/{override-plan,toggle-unlimited,revoke-sessions,force-stop-relay,verify-email,remove} — mutations, all require password re-auth

StatCard drilldown supports 11 cards with the following chart mappings:

  • Chart-only (full-width chart): Est. MRR (line 30d), Total Users (line 30d), Stream Hours 30d (line 30d)
  • Table-only (full-width table): Past Due, Active Relays
  • Chart + table: Paid Active (donut tier breakdown), Beta (bar signups), Elevated (bar grants), Free (bar signups), Canceled (bar cancellations), Signups 7d (bar per-day)

Rate limits: 30 req/min general admin endpoints, 10 req/min search endpoint.

Key Decisions

  • 2026-03-29: Fix CRIT-01 (JWT sub claim) as prerequisite before building admin. SignSessionJWT never set RegisteredClaims.Subject, so extractJwtSub in the BFF proxy returned empty string for all users. Fixed by adding Subject: userID in internal/auth/jwt.go.
  • 2026-03-29: Single admin (founder) initially, gated by is_admin column. No admin user management UI. Manual SQL to grant admin. Admin management deferred until user count justifies it.
  • 2026-03-29: Triple-layer auth: Go adminOnly middleware (primary gate, DB check with 30s cache), BFF proxy defense-in-depth (CRIT-02, lightweight API call to verify is_admin before forwarding admin paths), Cloudflare Pages Function auth gate (CRIT-03, calls /api/v1/auth/me to verify is_admin).
  • 2026-03-29: All mutating admin actions require password re-authentication (HIGH-03). Admin submits current password in request body. Backend verifies bcrypt hash before executing. Prevents CSRF and unauthorized use of stolen sessions.
  • 2026-03-29: All mutations logged transactionally to admin_audit_log table (MED-01). Same DB transaction as the action itself. Records admin_user_id, target_user_id, action, details JSON, timestamp.
  • 2026-03-29: API path uses /api/v1/ops/* instead of /admin/* to avoid ad blocker interference. Labels use “Operator” / “Portal” instead of “Admin” in DOM text/IDs.
  • 2026-03-29: Admin cannot revoke their own sessions (MED-03, self-modification guard).
  • 2026-04-01: Single drilldown endpoint rather than per-card endpoints. Card key acts as a switch to the appropriate query. Cursor-based pagination with max 25 results.
  • 2026-04-01: Chart.js v4 with tree-shaking to line/bar/doughnut controllers only. Theme-aware via CSS variables.

Gotchas & Known Issues

  • Ad blockers can interfere with paths and DOM elements containing “admin”. All API paths use /ops/ and DOM elements avoid the word “admin” in IDs, classes, and visible text. Use “operators” and “portal” instead.
  • The itoa helper in store_admin.go only works for parameter numbers 1-9 (single digit). Admin queries should not exceed 9 parameters, but if they do, switch to fmt.Sprintf("$%d", n).
  • Admin cache TTL is 30 seconds. Revoking admin access takes up to 30 seconds to propagate across in-flight requests.
  • CSP headers on admin pages restrict scripts to 'self' only (no inline scripts). Preact inline styles require style-src 'self' 'unsafe-inline'.
  • MRR calculation in drilldown is an approximation: counts paid active users multiplied by 4.99. Does not account for prorated or discounted subscriptions.
  • Billing events reference lemon_event_id (LemonSqueezy). Migration to Stripe is in progress, which will change the billing event schema.
  • Deploy ordering matters: run DB migrations before uploading new API binary when columns are added or renamed. Postgres service name in docker-compose is postgres, not glb-postgres.
  • Cloudflare Pages CDN retains old static assets and cannot truly delete files, only overwrite. Bump cache buster ?v=N in dashboard.html after rebuilding bundles.

Open Questions

  • When will admin user management UI be built (currently manual SQL to grant is_admin)?
  • Will impersonation (login-as-user) be added? Noted as complex and deferred.
  • When will bulk actions and CSV export be needed? Deferred as not required at <100 users.
  • Will an overview dashboard with aggregate metrics be added? Noted as deferred until user count justifies it. The current StatCards with drilldown partially fill this role.
  • How will the billing events schema change when Stripe migration completes (currently references LemonSqueezy fields)?

Sources