feat(api): API keys + Redis rate limiting + usage stats
CI / Python (ingestion) (pull_request) Successful in 12s
CI / Migrations (postgres) (pull_request) Successful in 24s
CI / Go (api) (pull_request) Successful in 53s

Add an optional API-key layer to the public read-only API. Keys grant
higher per-minute rate limits and attribute usage; anonymous callers are
still allowed at a lower IP-based budget.

- migration 0008_api_key: api_key table (sha256 hash only, plaintext shown once)
- apikey pkg: key generation + hashing
- ratelimit pkg: Redis fixed-window limiter + per-key usage counters; fails open
- public API middleware: X-API-Key / Bearer auth, X-RateLimit-* headers, 429+Retry-After
- admin: issue/list/revoke keys + usage view (API + UI tab)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
novaalphastrikeomegaz663
2026-06-20 08:24:07 +00:00
parent 7d7a8f1baf
commit 2820823b36
22 changed files with 1206 additions and 14 deletions
+24
View File
@@ -305,6 +305,30 @@ func (s *Store) ListCategories(ctx context.Context) ([]Category, error) {
return out, rows.Err()
}
// APIKey is the minimal metadata the public API needs to authorize a caller.
type APIKey struct {
ID string
Name string
RateLimitPerMin int
}
// APIKeyByHash returns the active (non-revoked) key matching a SHA-256 hash,
// or ErrNotFound if no such active key exists.
func (s *Store) APIKeyByHash(ctx context.Context, hash string) (*APIKey, error) {
var k APIKey
err := s.pool.QueryRow(ctx,
`SELECT id, name, rate_limit_per_min
FROM api_key WHERE key_hash = $1 AND revoked_at IS NULL`, hash,
).Scan(&k.ID, &k.Name, &k.RateLimitPerMin)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
return &k, nil
}
// Source describes a data source with its license and trust weight.
type Source struct {
ID string `json:"id"`