Skip to content

feat: operator-only user export (moshcode export users [--clean]) - #532

Merged
ralyodio merged 1 commit into
mainfrom
feat/export-users
Sep 24, 2026
Merged

ralyodio merged 1 commit into
mainfrom
feat/export-users

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Replaces reading the prod DB by hand to get the signup list.

App (apps/pwa)

  • GET /api/admin/users/export?format=csv|json: email, display_name, created_at (ISO), id, signup_method (password / passkey / coinpay) for every account with an email, oldest first. Accounts without an email are counted (by method), never listed. JSON adds counts; CSV carries them in X-Users-* headers. Cache-Control: no-store.
  • Operator check: the app had no admin concept, so this adds an ADMIN_EMAILS allowlist (comma-separated, case-insensitive), checked against the API key's user (Bearer wins when sent) or the browser session. Unset means nobody: 403 for all. 401 with no/bad key.
  • The query never selects password_hash or coinpay_sub, only IS NOT NULL flags. Display names that look like spreadsheet formulas are prefixed with '.
  • No rate-limit helper exists in the app, so none was added.
  • Each export logs [admin] <operator> exported N users.

CLI

  • moshcode export users [--clean [cleaner flags]] [--format csv|json] [-o file]. CSV to stdout, or to -o with mode 0600 (an existing file is copied to <name>.bak-NNN.<ext> first). Counts and paths go to stderr, so stdout stays clean CSV.
  • /export users in the pit: always writes a file (~/.moshcode/exports/users-<time>.csv unless -o), prints only the path and counts, never addresses.
  • --clean runs email-cleaner - --format json --report [flags] from cli-tools with the CSV on stdin. It passes through --allow-role --allow-disposable --allow-duplicates --allow-unlikely --allow-no-website --no-dns --fix-typos. If email-cleaner is not on PATH it fails with one line saying to install cli-tools, before any request. Kept rows come out in the same columns (a --fix-typos correction replaces the address). With -o, rejected rows go to <name>.rejected.csv with reasons and suggestion columns. Prints total / kept / rejected by reason.

Tests

  • apps/pwa/test/admin-users-export.test.mjs: 401 / 403 (non-operator, unset allowlist, other operator), CSV shape and order, formula neutralising, no secrets in CSV or JSON, JSON counts, bad format 400. Skips cleanly without PWA deps (checked).
  • test/export-users.test.mjs: arg grammar, CSV, missing email-cleaner, --clean plumbing against a stub email-cleaner on PATH (argv + stdin asserted), --fix-typos, pit mode never prints addresses, 0600 + backups, 403 handling, and an end-to-end run of the real binary against a fake app.
  • Full root suite locally: 3320 tests, 3315 pass, 0 fail, 5 skipped.

Deploy note

Set ADMIN_EMAILS on the Railway moshcode service (e.g. anthony@profullstack.com) or every export returns 403. The CLI half needs a release before moshcode export exists for installed users.

🤖 Generated with Claude Code

- app: GET /api/admin/users/export?format=csv|json, gated by a new
  ADMIN_EMAILS allowlist (API key or session). Returns email, display_name,
  created_at (ISO), id and signup_method for accounts with an email, plus
  counts of the rest. password_hash and coinpay_sub are never selected.
- cli: `moshcode export users [--clean] [--format csv|json] [-o file]` and
  `/export users` in the pit. Files are 0600 and backed up before overwrite;
  the pit always writes a file and prints only its path and counts.
- --clean pipes the CSV through cli-tools' email-cleaner (fails if it is not
  on PATH), keeps the valid rows, and writes <name>.rejected.csv with reasons.

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

Copy link
Copy Markdown

ThreatCrush Security Scan

7 finding(s) in the 13 file(s) this pull request changes.

MEDIUM: 7

Severity Rule Location
MEDIUM sql-string-concatenation src/cli-schema.mjs:180
MEDIUM sql-string-concatenation src/cli-schema.mjs:239
MEDIUM sql-string-concatenation src/cli-schema.mjs:643
MEDIUM sql-string-concatenation src/cli-schema.mjs:676
MEDIUM sql-string-concatenation src/cli-schema.mjs:783
MEDIUM sql-string-concatenation src/cli-schema.mjs:1536
MEDIUM sql-string-concatenation src/cli-schema.mjs:1554
94 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 8 | **MEDIUM**: 75 | **LOW**: 11

Not introduced by this pull request. The full set is in the Security tab.

Severity Rule Location
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH tls-verification-disabled apps/pwa/src/lib/moshpit-gateway.mjs:299
HIGH sh-remote-script-execution install.sh:86
HIGH sh-remote-script-execution install.sh:90
HIGH sh-remote-script-execution install.sh:258
HIGH sh-remote-script-execution install.sh:269
HIGH sh-remote-script-execution install.sh:275
HIGH tls-verification-disabled src/dns.mjs:766
MEDIUM sql-template-interpolation apps/pwa/src/lib/moshpit-certs.mjs:44
MEDIUM sql-template-interpolation apps/pwa/src/lib/moshpit-certs.mjs:82
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:139
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:153
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:179
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:373
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:377
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:422
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:671
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:867
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:869
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:928

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio marked this pull request as ready for review September 24, 2026 06:00
@ralyodio
ralyodio merged commit a69465c into main Sep 24, 2026
6 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