Skip to content

feat(admin): see every user's activity and volume in one place - #245

Merged
ralyodio merged 1 commit into
masterfrom
worktree-admin-user-stats
Aug 13, 2026
Merged

feat(admin): see every user's activity and volume in one place#245
ralyodio merged 1 commit into
masterfrom
worktree-admin-user-stats

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Adds /admin/users — a sortable, searchable table of every merchant account with their activity and USD volume, plus platform-wide totals and a CSV export.

What you get

Header tiles — users (with 7d/30d signups), logged-in-in-30d, businesses, crypto volume, Stripe volume, invoiced.

Per-user row — signed up, last active (most recent of login/payment/invoice/escrow/Stripe), businesses, settled/total payments, crypto USD, paid/total invoices, Stripe USD, and a combined total. Click a row to expand plan, auth provider, escrow and invoice-fee detail, and the merchant id.

Sort by any column, search by email or name, page through 50 at a time, export up to 500 matching rows as CSV.

How it's built

Piece Where
Aggregation supabase/migrations/20260813120000_admin_user_stats.sqladmin_user_stats() + admin_platform_stats()
Typed wrapper src/lib/stats/admin-user-stats.ts
API src/app/api/admin/users/route.ts, behind the existing requireAdmin()
UI src/app/admin/users/

Aggregation is in Postgres rather than the route, so a page of stats is one round trip instead of five queries per user. Both functions are granted to service_role only — no anon/authenticated grant, so a browser cannot reach them through PostgREST. Per-merchant CTEs are kept separate on purpose: joining businesses to payments and invoices and escrows in one query fans out the row set and multiplies every count by the cardinality of the other branches.

Two things deliberately not reported

  • Crypto-denominated fees. payments.fee_amount and escrows.fee_amount are in chain units. Summing 0.14 across BTC, ETH, SOL and USDC yields a meaningless number, so only invoice fees (which are USD) appear.
  • Crypto-priced invoices. Four invoices are denominated in ETH/USDC_BASE. They're counted but excluded from the USD column, so the count and total legitimately disagree — the page footnote says so rather than silently adding 0.05 ETH to a dollar figure.

"Settled" matches public_landing_stats(): confirmed or forwarded. Expired payment windows stay in payments_total, which is what makes the settle rate readable.

Migration is already applied

The two functions are live on mlywdeoogwsebabohskk (coinpayportal.com) — applied via the Supabase MCP, since no CI applies migrations for this repo. They're additive and read-only, so this branch is safe to merge whenever. Verified against prod: 285 users, 752 of 2,212 payments settled, $24,163.83 crypto volume, $37,493.28 Stripe.

Checks

  • vitest run — 272 files, 3781 passed, 3 skipped (21 of those are new)
  • tsc --noEmit — clean
  • eslint on touched paths — clean
  • next build --webpack — compiles; /admin/users and /api/admin/users register as dynamic routes
  • Search binding verified against prod: ' or 1=1-- returns 0 rows; unknown sort keys fall back; limit/offset clamp

🤖 Generated with Claude Code

Adds /admin/users: a sortable, searchable table of all 285 merchant
accounts with their counts and USD volume across the four things a
merchant can transact with — crypto payments, invoices, escrows and
Stripe — plus platform-wide totals in the header and a CSV export.

Aggregation lives in two Postgres functions rather than the route, so a
page of stats is one round trip instead of five queries per user. Both
are granted to service_role only and reached through requireAdmin(), the
same guard the existing admin pages use.

Two things deliberately not reported, because reporting them would be
worse than omitting them:

- Platform fees on crypto payments and escrows are held in chain units.
  Summing 0.14 across BTC, ETH, SOL and USDC produces a number with no
  meaning, so only invoice fees (which are USD) are shown.
- Four invoices are priced in ETH/USDC_BASE. They are counted but kept
  out of the USD column, so the count and the total legitimately
  disagree — the page says so rather than quietly adding 0.05 ETH to a
  dollar figure.

The per-merchant CTEs are separate on purpose: joining businesses to
payments and invoices and escrows in one query fans out the row set and
multiplies every count by the cardinality of the other branches.

Sort keys are whitelisted in SQL and again in TypeScript, and the search
term is bound as a parameter — a quote in the search box returns zero
rows rather than reaching the statement text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

325 finding(s)

HIGH/CRITICAL: 35 | MEDIUM: 37 | LOW: 253

Severity Rule Location
HIGH secret-private-key .env.example:236
HIGH secret-generic-api-key docs/API.md:430
HIGH secret-generic-api-key docs/API.md:585
HIGH secret-generic-credential docs/FIX_VERIFY_SIGNATURE.md:156
HIGH secret-generic-credential docs/integration-examples/nodejs-bot.md:225
HIGH secret-generic-api-key docs/sdk/getting-started.md:36
HIGH secret-generic-api-key docs/sdk/getting-started.md:318
HIGH sensitive-file-committed doppler.env:1
HIGH secret-generic-api-key packages/sdk/README.md:99
HIGH secret-generic-credential packages/sdk/README.md:122
HIGH secret-generic-credential packages/sdk/README.md:743
HIGH sh-remote-script-execution public/install.sh:132
HIGH sh-remote-script-execution public/install.sh:276
HIGH sh-remote-script-execution public/install.sh:280
HIGH sh-remote-script-execution public/install.sh:285
HIGH sh-remote-script-execution public/install.sh:289
HIGH sh-remote-script-execution public/install.sh:358
HIGH sh-remote-script-execution public/install.sh:594
HIGH sh-remote-script-execution public/install.sh:595
HIGH sh-remote-script-execution public/install.sh:635
HIGH sh-remote-script-execution public/install.sh:636
HIGH sh-remote-script-execution public/install.sh:637
HIGH secret-generic-credential scripts/setup-droplet.sh:570
HIGH secret-generic-api-key src/app/docs/sdk/page.tsx:135
HIGH secret-generic-api-key src/app/docs/sdk/page.tsx:214
HIGH secret-generic-credential src/app/docs/sdk/page.tsx:1001
HIGH secret-generic-credential src/app/docs/sdk/page.tsx:1022
HIGH secret-generic-api-key src/app/docs/sdk/page.tsx:1400
HIGH secret-generic-credential src/app/docs/sdk/page.tsx:1481
HIGH secret-generic-credential src/app/docs/sdk/page.tsx:1490
HIGH secret-generic-api-key src/app/docs/sdk/page.tsx:1532
HIGH secret-generic-credential src/components/docs/AuthenticationDocs.tsx:37
HIGH secret-generic-credential src/components/docs/OAuthDocs.tsx:262
HIGH secret-generic-credential supabase/config.toml:255
HIGH secret-generic-credential supabase/config.toml:287
MEDIUM manifest-install-lifecycle-script package.json:26
MEDIUM js-shell-exec-interpolation packages/sdk/bin/coinpay.js:40
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-issuer.test.js:23
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-reputation.test.js:23
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:22
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:33
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:48
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:63
MEDIUM js-shell-exec-interpolation packages/sdk/test/cli-subscription.test.js:78
MEDIUM js-shell-exec-interpolation packages/sdk/test/wallet-backup.test.js:82
MEDIUM js-shell-exec-interpolation packages/sdk/test/wallet.test.js:249
MEDIUM js-shell-exec-interpolation packages/sdk/test/wallet.test.js:280
MEDIUM insecure-temp-file public/install.sh:73
MEDIUM sh-unquoted-expansion-destructive public/install.sh:551
MEDIUM js-unescaped-html-sink public/payments.js:93

…and 275 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit d188526 into master Aug 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant