7434e5195e
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>
28 lines
1.2 KiB
SQL
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));
|