markdown · 5793 bytes Raw Blame History

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`.