diff --git a/internal/http/docs/openapi.yaml b/internal/http/docs/openapi.yaml index 857519c..527588d 100644 --- a/internal/http/docs/openapi.yaml +++ b/internal/http/docs/openapi.yaml @@ -108,6 +108,19 @@ paths: content: application/json: schema: { $ref: '#/components/schemas/Pricing' } + /v1/whitelist/pubkeys: + get: + tags: [Public] + summary: Active whitelist pubkeys + description: Hex-encoded pubkeys for all active (whitelisted) subscribers. + responses: + '200': + description: Pubkey list + content: + application/json: + schema: + type: array + items: { type: string, description: Hex pubkey } /v1/invoices: post: tags: [User] diff --git a/internal/http/handlers/users.go b/internal/http/handlers/users.go index 1e0b6c4..eda69be 100644 --- a/internal/http/handlers/users.go +++ b/internal/http/handlers/users.go @@ -61,3 +61,12 @@ func (h *Users) Get(w http.ResponseWriter, r *http.Request) { } WriteJSON(w, http.StatusOK, resp) } + +func (h *Users) ListWhitelistedPubkeys(w http.ResponseWriter, r *http.Request) { + pubkeys, err := h.Users.Repo().ListActivePubkeys(r.Context()) + if err != nil { + WriteError(w, http.StatusInternalServerError, "InternalError", err.Error()) + return + } + WriteJSON(w, http.StatusOK, pubkeys) +} diff --git a/internal/http/server.go b/internal/http/server.go index f6c81b0..37b2e69 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -65,6 +65,7 @@ func NewServer(d Deps) *http.Server { r.Route("/v1", func(r chi.Router) { r.Get("/pricing", pricing.Handle) + r.Get("/whitelist/pubkeys", users.ListWhitelistedPubkeys) r.Get("/users/{pubkey}", users.Get) r.Get("/usernames/{name}/available", usernames.Available) if d.Invoices != nil { diff --git a/internal/http/server_test.go b/internal/http/server_test.go index d7c9619..e7714aa 100644 --- a/internal/http/server_test.go +++ b/internal/http/server_test.go @@ -162,6 +162,53 @@ func TestNostrJSON_EmptyAndPopulated(t *testing.T) { } } +func TestWhitelistPubkeys(t *testing.T) { + f := newFixture(t) + + resp, body := f.get(t, "/v1/whitelist/pubkeys") + if resp.StatusCode != 200 { + t.Fatalf("empty list status %d: %s", resp.StatusCode, body) + } + var empty []string + if err := json.Unmarshal(body, &empty); err != nil { + t.Fatal(err) + } + if len(empty) != 0 { + t.Errorf("expected empty array: %s", body) + } + + f.admin(t, "POST", "/v1/admin/users", map[string]any{ + "pubkey": testHex, "username": "alice", + "subscription_type": "yearly", "years": 1, + }) + + inactiveHex := "1f8c41ebcd55a8d8db2e0a8c3a4b9c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1d" + _, err := f.db.ExecContext(context.Background(), + `INSERT INTO users (pubkey, username, subscription_type, expires_at, is_active, manual_username, created_at) + VALUES (?, 'bob', 'yearly', NULL, 0, 1, datetime('now'))`, + inactiveHex) + if err != nil { + t.Fatal(err) + } + + resp, body = f.get(t, "/v1/whitelist/pubkeys") + if resp.StatusCode != 200 { + t.Fatalf("status %d: %s", resp.StatusCode, body) + } + var pubkeys []string + if err := json.Unmarshal(body, &pubkeys); err != nil { + t.Fatal(err) + } + if len(pubkeys) != 1 || pubkeys[0] != testHex { + t.Errorf("want [%s], got %v (body %s)", testHex, pubkeys, body) + } + for _, pk := range pubkeys { + if pk == inactiveHex { + t.Errorf("inactive pubkey should not appear: %v", pubkeys) + } + } +} + func TestUsernameAvailability(t *testing.T) { f := newFixture(t) resp, body := f.get(t, "/v1/usernames/alice/available") diff --git a/internal/user/repo_query.go b/internal/user/repo_query.go index a2e5e39..b322ae5 100644 --- a/internal/user/repo_query.go +++ b/internal/user/repo_query.go @@ -50,6 +50,24 @@ func (r *Repo) ActiveByName(ctx context.Context) (map[string]string, error) { return out, rows.Err() } +func (r *Repo) ListActivePubkeys(ctx context.Context) ([]string, error) { + rows, err := r.db.QueryContext(ctx, + `SELECT pubkey FROM users WHERE is_active = 1 ORDER BY pubkey`) + if err != nil { + return nil, err + } + defer rows.Close() + out := make([]string, 0) + for rows.Next() { + var pk string + if err := rows.Scan(&pk); err != nil { + return nil, err + } + out = append(out, pk) + } + return out, rows.Err() +} + func (r *Repo) collect(rows interface { Next() bool Scan(...any) error