Add an authenticated admin API to the ingest Worker so the single maintainer
can review the pending queue and pull a report before Friday's publish run.
Endpoints (on the existing handler):
GET /v1/admin/reports list pending report ids
POST /v1/admin/reports/{id}/remove mark a report removed
DELETE /v1/admin/reports/{id} remove alias
Remove calls lifecycle.MarkRemoved (#10), transitioning pending -> removed so
#13's ListPending excludes it from the next publish. Codes: 200 list/remove,
404 unknown id, 401 missing/bad/unset-secret token, 405 wrong method.
Auth: shared-secret Bearer token compared with crypto/subtle.ConstantTimeCompare,
fail-closed when the secret is unset. Injected via handler.New's new AdminBackend
arg: the dev server and tests wire a memory-backed lifecycle.Manager + ADMIN_TOKEN
env; the Worker reads ADMIN_TOKEN from Secrets Store and builds an R2-backed
Manager per request. Choice documented in docs/decisions/admin-auth.md.
Tests: Go httptest unit tests (list, remove+exclusion, 404, 401 incl. fail-closed,
405) and a Bruno api-tests flow (seed, authed list/remove, exclusion, no/bad
token 401). wrangler.jsonc gains only the ADMIN_TOKEN secret binding; worker
triggers untouched (owned by #13).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
2.7 KiB
Go
67 lines
2.7 KiB
Go
// Command devserver runs the core bug-report ingest handler on a plain
|
|
// net/http server for local development.
|
|
//
|
|
// It deliberately does not depend on TinyGo or the Cloudflare Workers runtime,
|
|
// so `go run ./cmd/devserver` works with the standard Go toolchain and exercises
|
|
// the exact same handler that the deployed Wasm Worker serves. It listens on
|
|
// :8787 by default (matching wrangler dev's default port); override with ADDR.
|
|
//
|
|
// The ingest path is wired with the real scrub+encrypt storage Sink (#9) backed
|
|
// by an in-memory object store and a throwaway per-run AES-256 key, so a POST
|
|
// /v1/reports exercises the full pipeline locally. Stored objects live only for
|
|
// the process lifetime.
|
|
//
|
|
// The maintainer admin API (#11) is wired over a lifecycle.Manager sharing that
|
|
// same in-memory store, so reports ingested via POST /v1/reports are immediately
|
|
// listable and removable under /v1/admin/reports. Its shared-secret bearer token
|
|
// comes from the ADMIN_TOKEN env var; if unset, the admin routes fail closed
|
|
// (every request 401s), matching production's fail-closed behaviour. Set
|
|
// ADMIN_TOKEN=... to exercise the admin API (the Bruno api-tests do this).
|
|
package main
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/crypto"
|
|
"github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/handler"
|
|
"github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/lifecycle"
|
|
"github.com/JMR-dev/LibreMail-Bug-Report-Ingest/internal/storage"
|
|
)
|
|
|
|
func main() {
|
|
addr := os.Getenv("ADDR")
|
|
if addr == "" {
|
|
addr = ":8787"
|
|
}
|
|
|
|
// A throwaway keyring: a single random key generated at startup. Reports are
|
|
// scrubbed, encrypted under it, and held in memory; nothing is persisted.
|
|
key, err := crypto.GenerateKey()
|
|
if err != nil {
|
|
log.Fatalf("devserver: generate key: %v", err)
|
|
}
|
|
keyring, err := crypto.NewKeyring(1, map[uint16][]byte{1: key})
|
|
if err != nil {
|
|
log.Fatalf("devserver: build keyring: %v", err)
|
|
}
|
|
|
|
// One shared in-memory store backs both the ingest Sink and the admin
|
|
// lifecycle Manager, so an ingested report is visible to the admin API.
|
|
store := storage.NewMemoryStore()
|
|
sink := storage.NewSink(store, keyring)
|
|
adminToken := os.Getenv("ADMIN_TOKEN")
|
|
admin := handler.NewManagerBackend(lifecycle.New(store), adminToken)
|
|
|
|
if adminToken == "" {
|
|
log.Print("devserver: ADMIN_TOKEN is unset; /v1/admin routes fail closed (401). Set ADMIN_TOKEN to enable them.")
|
|
} else {
|
|
log.Print("devserver: admin API enabled at /v1/admin/reports (bearer token from ADMIN_TOKEN)")
|
|
}
|
|
log.Printf("devserver listening on %s (try GET / and GET /healthz)", addr)
|
|
if err := http.ListenAndServe(addr, handler.New(sink, admin)); err != nil {
|
|
log.Fatalf("devserver: %v", err)
|
|
}
|
|
}
|