Add public GET /v1/whitelist/pubkeys for active subscriber pubkeys

Expose JSON array of hex pubkeys backed by ListActivePubkeys query.
Includes OpenAPI documentation and integration tests.

Made-with: Love
This commit is contained in:
2026-04-29 06:45:44 +00:00
parent a01797e9b2
commit 611ef5fc4a
5 changed files with 88 additions and 0 deletions

View File

@@ -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]

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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")

View File

@@ -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