Admin tour
The site-admin surface — what's available, where to find it, and what each thing does. Audience: the first operator (you) signing in to a fresh production instance.
Becoming an admin
The first user is not automatically an admin; the site treats the schema and the human as separate concerns so a self-signup never escalates itself. Promote yourself once via the CLI:
ssh root@shithub.sh
sudo -u shithub /usr/local/bin/shithubd admin bootstrap-admin <username>
bootstrap-admin records an audit row with actor_id = 0 so the
"who promoted whom" history is never blank. After that, additional
admins are toggled from the UI — see /admin/users/<id>.
To stop being an admin, the same UI toggle works (or a peer admin can revoke you). There is intentionally no CLI demote: revocation should leave an audit row by an authenticated actor.
Web surface
All admin routes live under /admin/* and are gated by
RequireSiteAdmin. Non-admins get a 404, not a 403 — existence is
not leaked.
Dashboard — /admin
Counts: users, repos, orgs, jobs, admins. First glance to confirm the instance is alive and to spot anomalies (e.g., 10× job count overnight).
Users — /admin/users and /admin/users/{id}
Lists users with suspension state and admin flag. The detail page has buttons for:
- Suspend / unsuspend (writes audit, sends no email — quiet sanction).
- Toggle site-admin (writes audit, irrevocable from CLI).
- Force a password-reset link (1-hour token, mailed to primary email;
also surfaces in the journal if email backend is
stdout).
Repos — /admin/repos and /admin/repos/{id}
Filterable: deleted, archived. The detail page can force-archive or force-delete a repo. Hard-delete is two-step: first soft-delete (lands in the restore queue with a TTL), then purge after the TTL or via the restore-list page.
Jobs — /admin/jobs and /admin/jobs/{id}
The worker queue. Filter by kind (email_send, webhook_deliver,
repo_archive_purge, …) and status (queued, running, failed,
succeeded). Detail page shows the JSON payload and the last error,
with buttons to Retry (re-enqueue) and Discard (mark dead).
First place to look when "the email never arrived" or "the webhook
didn't fire."
Audit — /admin/audit
Filterable history of security-relevant events: admin actions, login
successes/failures, 2FA changes, key revocations, impersonation. All
the filter dimensions live in the query string (actor, action,
target_type, target_id, since, until). Bookmarkable.
System — /admin/system
Runtime introspection: shithubd version + commit, Go runtime, DB
connection-pool stats, repo-disk-usage rollup. Useful for "is this
the version I just deployed?" — compare commit hash against
git log -1 origin/trunk.
Email — /admin/email
Outbound transactional email queue and last-N delivery results. If a
verification or reset mail is missing, this is the second place to
check (after /admin/jobs filtered to email_send).
Impersonation — /admin/impersonate/{id}
Open a session as another user in read-only mode. All write
endpoints reject with 403 from an impersonated session unless you
explicitly switch to /admin/impersonate/write-mode. Both the start
and the writable-promotion are audited.
End impersonation with /admin/impersonate/stop (or via the banner
that appears at the top of every page during an impersonation).
CLI subcommands
Run as the system user that owns the binary's environment file. The
canonical invocation is sudo -u shithub /usr/local/bin/shithubd admin <subcommand>; the binary reads /etc/shithub/web.env only
when the systemd unit launches it, so for ad-hoc admin commands you
either source that file first or rely on shithubd's own env-only
config (it'll print which knob is missing if so).
| subcommand | purpose |
|---|---|
bootstrap-admin <username> |
Promote first user (chicken-and-egg). |
reset-password <username> |
Issue a 1-hour password-reset link. |
clear-2fa <username> |
Wipe TOTP enrollment (support escape hatch). User is emailed. |
run-job <kind> [json-payload] |
Enqueue an arbitrary worker job. |
recompute <metric> |
Recompute denormalized counters (star_count, fork_count). |
reset-password and clear-2fa both leave audit rows attributed to
actor_id = 0 (system) so a recovered account always has an honest
trail.
When to look where
| Symptom | First stop |
|---|---|
| User says "didn't get my email" | /admin/jobs?kind=email_send&status=failed then /admin/email |
| User locked out of 2FA | CLI clear-2fa <user> (then verify audit row) |
| Suspicious activity from one IP | /admin/audit?actor=<id> + fail2ban-client status shithubd-auth on the droplet |
| "Did this PR ship?" | /admin/system for the running commit; compare to git rev-parse origin/trunk |
| Worker queue piling up | /admin/jobs?status=queued — sort by age, retry or discard the oldest |
| Repo went missing | /admin/repos?deleted=1 — soft-deleted lives in the restore queue |
| Need to peek at a user's view | /admin/impersonate/<id> (read-only by default) |
What the admin surface deliberately does NOT have
- Bulk user actions. No "suspend N users matching a filter." Anything destructive at scale should be a thought-out script, not one click.
- API endpoints. Admin is HTML-only. Token-based admin actions would let a leaked session token compromise the whole instance; cookie sessions plus the per-action audit row keep the blast radius bounded.
- Direct DB editing. If you find yourself wanting it, the CLI
doesn't have it for a reason — write a migration or a one-off
job kind, then enqueue with
admin run-job.
View source
| 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`. |