Files
goods/migrations/0011_api_quota.up.sql
T
sulaimaannaasif6866 7434e5195e
CI / Go (api) (pull_request) Successful in 15s
CI / Python (ingestion) (pull_request) Successful in 10s
CI / Migrations (postgres) (pull_request) Successful in 16s
feat(api): tiered cumulative quota + self-service registration
Anonymous callers get a free cumulative quota (1000 calls per IP); once
exhausted they get 403 quota_exhausted and must register. Public users can
self-register (email+password) to obtain a higher-quota API key, view usage,
and regenerate the key. Quota counters live in Redis; the public API stays
read-only except for the registration writes.

- migration 0011: app_user table + api_key.quota_total + 'registered' tier
- ratelimit: IncrTotal/TotalUsed/CopyTotal lifetime counters
- middleware: enforce cumulative quota + X-Quota-* headers
- store: RegisterUser/Authenticate/RegenerateKey (bcrypt)
- handlers: POST /api/v1/register, /account, /account/regenerate
- admin: quota_total column + registered tier
- public: 'API 密钥' account page + API docs quota section

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-06-21 07:26:41 +00:00

28 lines
1.2 KiB
SQL

-- Cumulative usage quotas + self-service user registration for the public API.
--
-- The free anonymous tier is capped at a lifetime number of calls (enforced in
-- Redis, keyed by client IP). To keep calling beyond the free quota a caller
-- registers an account and self-issues an API key with a higher quota.
-- quota_total = 0 means unlimited.
--
-- Registration is the one place the public server writes to PostgreSQL (it
-- inserts an app_user and its api_key); every other public route stays
-- read-only. Request counting still lives entirely in Redis.
ALTER TABLE api_key ADD COLUMN IF NOT EXISTS quota_total BIGINT NOT NULL DEFAULT 0;
ALTER TABLE api_key DROP CONSTRAINT IF EXISTS api_key_tier_chk;
ALTER TABLE api_key ADD CONSTRAINT api_key_tier_chk
CHECK (tier IN ('free', 'registered', 'partner', 'internal'));
CREATE TABLE IF NOT EXISTS app_user (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
api_key_id UUID REFERENCES api_key (id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Case-insensitive uniqueness so each email registers at most once.
CREATE UNIQUE INDEX IF NOT EXISTS idx_app_user_email ON app_user (lower(email));