@@ -0,0 +1,128 @@ |
| 1 | +# Admin tour |
| 2 | + |
| 3 | +The site-admin surface — what's available, where to find it, and what |
| 4 | +each thing does. Audience: the first operator (you) signing in to a |
| 5 | +fresh production instance. |
| 6 | + |
| 7 | +## Becoming an admin |
| 8 | + |
| 9 | +The first user is not automatically an admin; the site treats the |
| 10 | +schema and the human as separate concerns so a self-signup never |
| 11 | +escalates itself. Promote yourself once via the CLI: |
| 12 | + |
| 13 | +```sh |
| 14 | +ssh root@shithub.sh |
| 15 | +sudo -u shithub /usr/local/bin/shithubd admin bootstrap-admin <username> |
| 16 | +``` |
| 17 | + |
| 18 | +`bootstrap-admin` records an audit row with `actor_id = 0` so the |
| 19 | +"who promoted whom" history is never blank. After that, additional |
| 20 | +admins are toggled from the UI — see `/admin/users/<id>`. |
| 21 | + |
| 22 | +To stop being an admin, the same UI toggle works (or a peer admin can |
| 23 | +revoke you). There is intentionally no CLI demote: revocation should |
| 24 | +leave an audit row by an authenticated actor. |
| 25 | + |
| 26 | +## Web surface |
| 27 | + |
| 28 | +All admin routes live under `/admin/*` and are gated by |
| 29 | +`RequireSiteAdmin`. Non-admins get a 404, not a 403 — existence is |
| 30 | +not leaked. |
| 31 | + |
| 32 | +### Dashboard — `/admin` |
| 33 | +Counts: users, repos, orgs, jobs, admins. First glance to confirm the |
| 34 | +instance is alive and to spot anomalies (e.g., 10× job count overnight). |
| 35 | + |
| 36 | +### Users — `/admin/users` and `/admin/users/{id}` |
| 37 | +Lists users with suspension state and admin flag. The detail page has |
| 38 | +buttons for: |
| 39 | +- Suspend / unsuspend (writes audit, sends no email — quiet sanction). |
| 40 | +- Toggle site-admin (writes audit, irrevocable from CLI). |
| 41 | +- Force a password-reset link (1-hour token, mailed to primary email; |
| 42 | + also surfaces in the journal if email backend is `stdout`). |
| 43 | + |
| 44 | +### Repos — `/admin/repos` and `/admin/repos/{id}` |
| 45 | +Filterable: deleted, archived. The detail page can force-archive or |
| 46 | +force-delete a repo. Hard-delete is two-step: first soft-delete (lands |
| 47 | +in the restore queue with a TTL), then purge after the TTL or via the |
| 48 | +restore-list page. |
| 49 | + |
| 50 | +### Jobs — `/admin/jobs` and `/admin/jobs/{id}` |
| 51 | +The worker queue. Filter by kind (`email_send`, `webhook_deliver`, |
| 52 | +`repo_archive_purge`, …) and status (`queued`, `running`, `failed`, |
| 53 | +`succeeded`). Detail page shows the JSON payload and the last error, |
| 54 | +with buttons to **Retry** (re-enqueue) and **Discard** (mark dead). |
| 55 | +First place to look when "the email never arrived" or "the webhook |
| 56 | +didn't fire." |
| 57 | + |
| 58 | +### Audit — `/admin/audit` |
| 59 | +Filterable history of security-relevant events: admin actions, login |
| 60 | +successes/failures, 2FA changes, key revocations, impersonation. All |
| 61 | +the filter dimensions live in the query string (`actor`, `action`, |
| 62 | +`target_type`, `target_id`, `since`, `until`). Bookmarkable. |
| 63 | + |
| 64 | +### System — `/admin/system` |
| 65 | +Runtime introspection: shithubd version + commit, Go runtime, DB |
| 66 | +connection-pool stats, repo-disk-usage rollup. Useful for "is this |
| 67 | +the version I just deployed?" — compare commit hash against |
| 68 | +`git log -1 origin/trunk`. |
| 69 | + |
| 70 | +### Email — `/admin/email` |
| 71 | +Outbound transactional email queue and last-N delivery results. If a |
| 72 | +verification or reset mail is missing, this is the second place to |
| 73 | +check (after `/admin/jobs` filtered to `email_send`). |
| 74 | + |
| 75 | +### Impersonation — `/admin/impersonate/{id}` |
| 76 | +Open a session as another user in **read-only** mode. All write |
| 77 | +endpoints reject with 403 from an impersonated session unless you |
| 78 | +explicitly switch to `/admin/impersonate/write-mode`. Both the start |
| 79 | +and the writable-promotion are audited. |
| 80 | + |
| 81 | +End impersonation with `/admin/impersonate/stop` (or via the banner |
| 82 | +that appears at the top of every page during an impersonation). |
| 83 | + |
| 84 | +## CLI subcommands |
| 85 | + |
| 86 | +Run as the system user that owns the binary's environment file. The |
| 87 | +canonical invocation is `sudo -u shithub /usr/local/bin/shithubd |
| 88 | +admin <subcommand>`; the binary reads `/etc/shithub/web.env` only |
| 89 | +when the systemd unit launches it, so for ad-hoc admin commands you |
| 90 | +either source that file first or rely on `shithubd`'s own env-only |
| 91 | +config (it'll print which knob is missing if so). |
| 92 | + |
| 93 | +| subcommand | purpose | |
| 94 | +|---|---| |
| 95 | +| `bootstrap-admin <username>` | Promote first user (chicken-and-egg). | |
| 96 | +| `reset-password <username>` | Issue a 1-hour password-reset link. | |
| 97 | +| `clear-2fa <username>` | Wipe TOTP enrollment (support escape hatch). User is emailed. | |
| 98 | +| `run-job <kind> [json-payload]` | Enqueue an arbitrary worker job. | |
| 99 | +| `recompute <metric>` | Recompute denormalized counters (`star_count`, `fork_count`). | |
| 100 | + |
| 101 | +`reset-password` and `clear-2fa` both leave audit rows attributed to |
| 102 | +`actor_id = 0` (system) so a recovered account always has an honest |
| 103 | +trail. |
| 104 | + |
| 105 | +## When to look where |
| 106 | + |
| 107 | +| Symptom | First stop | |
| 108 | +|---|---| |
| 109 | +| User says "didn't get my email" | `/admin/jobs?kind=email_send&status=failed` then `/admin/email` | |
| 110 | +| User locked out of 2FA | CLI `clear-2fa <user>` (then verify audit row) | |
| 111 | +| Suspicious activity from one IP | `/admin/audit?actor=<id>` + `fail2ban-client status shithubd-auth` on the droplet | |
| 112 | +| "Did this PR ship?" | `/admin/system` for the running commit; compare to `git rev-parse origin/trunk` | |
| 113 | +| Worker queue piling up | `/admin/jobs?status=queued` — sort by age, retry or discard the oldest | |
| 114 | +| Repo went missing | `/admin/repos?deleted=1` — soft-deleted lives in the restore queue | |
| 115 | +| Need to peek at a user's view | `/admin/impersonate/<id>` (read-only by default) | |
| 116 | + |
| 117 | +## What the admin surface deliberately does NOT have |
| 118 | + |
| 119 | +- **Bulk user actions.** No "suspend N users matching a filter." |
| 120 | + Anything destructive at scale should be a thought-out script, not |
| 121 | + one click. |
| 122 | +- **API endpoints.** Admin is HTML-only. Token-based admin actions |
| 123 | + would let a leaked session token compromise the whole instance; |
| 124 | + cookie sessions plus the per-action audit row keep the blast |
| 125 | + radius bounded. |
| 126 | +- **Direct DB editing.** If you find yourself wanting it, the CLI |
| 127 | + doesn't have it for a reason — write a migration or a one-off |
| 128 | + job kind, then enqueue with `admin run-job`. |