From 17ae84aadd7213ba9724496d614426b8fdeb2424 Mon Sep 17 00:00:00 2001 From: Graham Barber Date: Tue, 26 May 2026 17:17:08 -0700 Subject: [PATCH] support setting admin DIDs from environment --- README.md | 12 +++++++++++- cmd/web/main.go | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b502bf3..9a613ee 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,21 @@ On first boot the app creates `data/atmoquest.db` (SQLite) and `data/admin_signi | `/admin` | Admin console — requires `users.is_admin = 1` | | `/healthz` | Liveness probe | -To grant yourself admin after signing in once: +To grant yourself admin after signing in once, either: ```bash +# Local dev: edit the SQLite file directly. sqlite3 data/atmoquest.db "UPDATE users SET is_admin=1 WHERE did='did:plc:…';" ``` +Or set `BOOTSTRAP_ADMIN_DIDS` (space-separated) and restart the app. The listed DIDs are promoted to admin on every boot — idempotent and safe to leave set. This is the supported path in deployed environments where `sqlite3` isn't reachable (the prod image is `scratch`-based): + +```bash +BOOTSTRAP_ADMIN_DIDS="did:plc:xxx did:plc:yyy" +``` + +Either way, the DID must have signed in at least once first (so the `users` row exists). + ## Layout ``` @@ -53,6 +62,7 @@ web/resources/ static assets (css, js, fonts, datastar) | `SESSION_SECRET` | dev default | **yes** in prod | | `DATABASE_URL` | `file:data/atmoquest.db?…` | no | | `ADMIN_SIGNING_KEY` | auto-generated in dev | **yes** in prod | +| `BOOTSTRAP_ADMIN_DIDS` | unset | no | | `LOG_LEVEL` | `INFO` | no | `-tags dev` selects the dev config (random session key, auto-generated admin key). `-tags prod` (or no tag, in CI) requires the real secrets above. diff --git a/cmd/web/main.go b/cmd/web/main.go index aaf776c..913240a 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -7,17 +7,21 @@ import ( "atmoquest/internal/oauthclient" "atmoquest/internal/oauthstore" "atmoquest/internal/session" + "atmoquest/internal/users" "atmoquest/nats" "atmoquest/router" "context" + "errors" "fmt" "log/slog" "net" "net/http" "os" "os/signal" + "strings" "time" + "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/httplog/v3" @@ -55,6 +59,22 @@ func run(ctx context.Context) error { } slog.Info("db ready", "dsn", config.Global.DatabaseURL) + for _, s := range strings.Fields(os.Getenv("BOOTSTRAP_ADMIN_DIDS")) { + did, err := syntax.ParseDID(s) + if err != nil { + slog.Warn("bootstrap admin: invalid DID, skipping", "value", s, "error", err) + continue + } + if err := users.SetAdmin(ctx, conn, did, true); err != nil { + if errors.Is(err, users.ErrNotFound) { + slog.Warn("bootstrap admin: DID hasn't signed in yet, skipping", "did", did) + continue + } + return fmt.Errorf("bootstrap admin %s: %w", did, err) + } + slog.Info("bootstrap admin: granted", "did", did) + } + r := chi.NewMux() r.Use( httplog.RequestLogger(logger, nil), -- 2.51.2