Admin dashboard: login heatmap, signup/checkout funnel, custom-key minting #33
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Internal analytics + admin surface for swisscairn operators. Frontend v1 is being drafted in Claude Design; this tracks the backend + wiring. Full rationale in ADR-0002.
Where it lives
Extend
cairn-license-server— it already owns accounts, sees every login, receives the Paddle webhooks, and now has a SQLite store with room for aneventstable. No separate analytics service (single-binary ethos; low volume; aggregation is just SQL now that #28 landed).How it's reached
GET /v1/me→{email, admin: bool}.account.htmlshows an "Admin" nav item only whenadminis true.admin.htmlonwww.swisscairn.ch(same origin asaccount.html, so it shares thesessionStoragebearer token — no second login). Kept a separate file so the admin bundle isn't shipped to customers.admin.htmlcalls/v1/admin/{overview,heatmap,funnel,mint}.Authorization (the crux — server-side, not a frontend check)
Every
/v1/admin/*call is gated on the session account against an explicit allowlist envADMIN_ACCOUNTS=nikola@swisscairn.ch,manuel@swisscairn.ch(ship this). A domain rule (@swisscairn.ch) is acceptable only combined with email verification (#31) or Google-OAuth-proven domain control — otherwise open registration lets anyone claim the domain. The existing staticADMIN_TOKENstays valid for machine/manual use; admin endpoints accept either.Data model
One
eventstable in the SQLite store:id, ts, type, account_email (nullable), country, meta JSON./v1/loginsuccess + OAuth callback (Caddy must forwardX-Forwarded-For; app trusts only Caddy), resolve to a country via an offline GeoIP DB on the VPS (MaxMind GeoLite2-Country or DB-IP lite). Store country only, never the raw IP. Map =SELECT country, count(*) GROUP BY country.account_createdat/v1/register;checkout_startedfrom the Paddletransaction.createdwebhook (Paddle auto-creates a draft txn when a checkout opens — confirmed);purchase_completedfromtransaction.completed(already handled). Derives: created→purchased vs created→never, and checkout_started→not completed (abandoned) in a window. Only server change: subscribe/recordtransaction.created(today it's acknowledge-only).POST /v1/admin/licenses(alreadyissueFor(years)) to take explicit seats + interval/years + optional exact expiry + a label. A 5-year key isyears: 5. These are non-subscription keys (noPaddleSub) → they simply don't auto-renew; the account UI already handles "not subscription-backed."Privacy posture (decided with Nikola, 2026-08-25)
Related hardening
Sequencing
Hold the backend build until the Claude Design v1 lands so endpoints match the UI. Can stub
events,/v1/me, and the admin allowlist now if we want plumbing ready.Auth decision (Nikola, 2026-08-25): explicit allowlist, seeded from env with
no-reply@swisscairn.chfor now (the only swisscairn mailbox that exists today).Refinement: the admin panel itself will include an "admins" section to promote/demote admin users (backed by an
is_adminflag on the account, persisted in SQLite). So the env allowlist is just the bootstrap seed — once you're in, you add other admins through the UI. This means Google OAuth domain-proof is not needed; drop it from scope for now. (Email verification, #31, still lands first and remains the right default for all accounts.)Net auth model:
ADMIN_ACCOUNTSenv = bootstrap seed (currentlyno-reply@swisscairn.ch).accounts.is_admin= source of truth thereafter, managed in the admin panel./v1/admin/*call checks the session account is admin (seed OR flag). StaticADMIN_TOKENstill valid for machine use.Closing — the tracked scope (backend + wiring) is complete and deployed:
GET /v1/me, staff allowlist/gate (ADMIN_ACCOUNTSseed +adminstable), promote/demote, custom-key minting (/v1/admin/{admins,mint,audit}) — PR #35.eventstable, overview/accounts/timeline rollups, Paddle checkout funnel (recordPaddleEvent) — PR #36..mmdbplaced on the VPS + monthly refresh timer (ops commit).Auth landed as the explicit-allowlist model from the 2026-08-25 decision, so it's self-contained (not gated on #22/#31). Frontend
admin.htmlis the separate Claude Design track. Remaining hardening lives in its own issues (#31/#34 email verification, #32 CAPTCHA, #38 onboard first owner).