Add webhook endpoints to the org API

Go · Go · intermediate · modification

First half of outgoing webhooks: admins register an endpoint and get its signing secret once, and anyone in the org can see the configured endpoints in settings. `Store` keeps the queries in one place: `ListForOrg` will be shared with the delivery worker, which needs the secret to sign payloads, so there is a single source of truth for an org's endpoints. URLs are validated (https only, no credentials), the request body is capped at 4 KiB, and secrets come from `crypto/rand`. Tested with curl: created two endpoints as an admin, got 403 as a viewer, listed them as a viewer.

Go 1.22 service on the standard library (`net/http` method-pattern `ServeMux`, `log/slog`, `database/sql` with the pgx stdlib driver, PostgreSQL 16). `main` wraps the mux in the auth middleware, so every handler runs for a signed-in org member; `auth.FromContext(ctx)` returns an `auth.Member` with `OrgID` and `Role` (`RoleAdmin`, `RoleMember` or `RoleViewer`). The `webhook_endpoints(id bigserial primary key, org_id bigint not null, url text not null, signing_secret text not null, created_at timestamptz not null default now())` table shipped in last week's migration; the database is encrypted at rest, and app-level encryption of secrets is a separate project. Package `webhooks` already has `writeJSON(w http.ResponseWriter, status int, v any)` in `respond.go`, which sets the JSON content type, writes the status and encodes `v`. The delivery worker lands in the next PR, behind a disabled flag; it will call `Store.ListForOrg` for each event, and it resolves each URL and refuses private, loopback and link-local addresses at send time. Customers verify the `X-Signature` HMAC to trust that an event came from us; orgs routinely add contractors and auditors as viewers.

Requirements

Files touched

--- cmd/api/routes.go
 	"example.com/hooks/internal/billing"
 	"example.com/hooks/internal/projects"
+	"example.com/hooks/internal/webhooks"
 )
 
 	billing.Register(mux, db)
 	projects.Register(mux, db)
+	webhooks.Register(mux, db)
 }
 
--- internal/webhooks/store.go
+package webhooks
+
+import (
+	"context"
+	"database/sql"
+)
+
+type Store struct {
+	DB *sql.DB
+}
+
+func (s *Store) Create(ctx context.Context, orgID int64, url, secret string) (Endpoint, error) {
+	ep := Endpoint{URL: url, SigningSecret: secret}
+	err := s.DB.QueryRowContext(ctx,
+		`INSERT INTO webhook_endpoints (org_id, url, signing_secret)
+		 VALUES ($1, $2, $3) RETURNING id, created_at`,
+		orgID, url, secret,
+	).Scan(&ep.ID, &ep.CreatedAt)

Review this PR

Go practice