accounts: niveau interne « unlimited » (invitation seulement, hors /v1/limits), commande keys rotate, options admin/dashboard
4 changed files +25 −6
modified
hfmarketdata/api/accounts/cli.py
+18 −2
@@ -1,6 +1,6 @@ | ||
| 1 | 1 | """`hfmd` — accounts administration from the shell (wrapper: scripts/hfmd). |
| 2 | 2 | |
| 3 | − hfmd users add "Name" email [--tier free|high_usage] [--admin] [--show-key] [--no-mail] | |
| 3 | + hfmd users add "Name" email [--tier free|high_usage|unlimited] [--admin] [--show-key] [--no-mail] | |
| 4 | 4 | hfmd users list |
| 5 | 5 | hfmd users set-tier <email> <free|high_usage> |
| 6 | 6 | hfmd users disable <email> | enable <email> |
@@ -137,6 +137,21 @@ def cmd_users_invite_resend(a) -> int: | ||
| 137 | 137 | return 0 |
| 138 | 138 | |
| 139 | 139 | |
| 140 | +def cmd_keys_rotate(a) -> int: | |
| 141 | + """Revoke the active key(s) and create a fresh one; prints the new key ONLY with --show-key.""" | |
| 142 | + with session() as s: | |
| 143 | + u = _user_or_die(s, a.email) | |
| 144 | + keys = service.active_keys(s, u) | |
| 145 | + if keys: | |
| 146 | + raw, k = service.rotate_key(s, u, keys[0], actor=ACTOR) | |
| 147 | + for extra in keys[1:]: | |
| 148 | + service.revoke_key(s, extra, actor=ACTOR) | |
| 149 | + else: | |
| 150 | + raw, k = service.create_key(s, u, "default", actor=ACTOR, notify=False) | |
| 151 | + print(f"{u.email}: new key {k.prefix}… " + (raw if a.show_key else "(hidden — use --show-key)")) | |
| 152 | + return 0 | |
| 153 | + | |
| 154 | + | |
| 140 | 155 | def cmd_keys_list(a) -> int: |
| 141 | 156 | with session() as s: |
| 142 | 157 | u = _user_or_die(s, a.email) |
@@ -174,7 +189,7 @@ def build_parser() -> argparse.ArgumentParser: | ||
| 174 | 189 | add.add_argument("--no-mail", action="store_true", help="never send e-mail, print the link") |
| 175 | 190 | add.set_defaults(fn=cmd_users_add) |
| 176 | 191 | users.add_parser("list").set_defaults(fn=cmd_users_list) |
| 177 | − st = users.add_parser("set-tier"); st.add_argument("email"); st.add_argument("tier", choices=("free", "high_usage")) | |
| 192 | + st = users.add_parser("set-tier"); st.add_argument("email"); st.add_argument("tier", choices=("free", "high_usage", "unlimited")) | |
| 178 | 193 | st.set_defaults(fn=cmd_users_set_tier) |
| 179 | 194 | d = users.add_parser("disable"); d.add_argument("email"); d.set_defaults(fn=lambda a: cmd_users_status(a, "disabled")) |
| 180 | 195 | e = users.add_parser("enable"); e.add_argument("email"); e.set_defaults(fn=lambda a: cmd_users_status(a, "active")) |
@@ -183,6 +198,7 @@ def build_parser() -> argparse.ArgumentParser: | ||
| 183 | 198 | |
| 184 | 199 | keys = sub.add_parser("keys", help="inspect API keys").add_subparsers(dest="sub", required=True) |
| 185 | 200 | kl = keys.add_parser("list"); kl.add_argument("email"); kl.set_defaults(fn=cmd_keys_list) |
| 201 | + kr = keys.add_parser("rotate", help="revoke + recreate the user's key"); kr.add_argument("email"); kr.add_argument("--show-key", action="store_true"); kr.set_defaults(fn=cmd_keys_rotate) | |
| 186 | 202 | |
| 187 | 203 | seed = sub.add_parser("seed", help="create the initial users (idempotent)") |
| 188 | 204 | seed.add_argument("--show-key", action="store_true") |
modified
hfmarketdata/api/ratelimit/tiers.py
+5 −2
@@ -60,8 +60,11 @@ TIERS: dict[str, Tier] = { | ||
| 60 | 60 | "keyless": Tier("keyless", "ip", 3600, 30, 100_000, 5_000), |
| 61 | 61 | "free": Tier("free", "key", 60, 120, 1_000_000, 50_000), |
| 62 | 62 | "high_usage": Tier("high_usage", "key", 60, 600, 10_000_000, 200_000), |
| 63 | + # internal, invitation-only: no practical limit (kept finite so the Lua counters stay integers) | |
| 64 | + "unlimited": Tier("unlimited", "key", 60, 1_000_000_000, 1_000_000_000_000, 5_000_000), | |
| 63 | 65 | } |
| 64 | −ACCOUNT_TIERS = ("free", "high_usage") | |
| 66 | +ACCOUNT_TIERS = ("free", "high_usage", "unlimited") | |
| 67 | +INTERNAL_TIERS = ("unlimited",) | |
| 65 | 68 | DEFAULT_ACCOUNT_TIER = "free" |
| 66 | 69 | |
| 67 | 70 | |
@@ -90,7 +93,7 @@ def upgrade_hint(tier: Tier) -> str: | ||
| 90 | 93 | |
| 91 | 94 | |
| 92 | 95 | def tiers_public() -> dict[str, dict]: |
| 93 | − out = {name: t.public() for name, t in TIERS.items()} | |
| 96 | + out = {name: t.public() for name, t in TIERS.items() if name not in INTERNAL_TIERS} | |
| 94 | 97 | out["high_usage"]["how_to_get"] = f"e-mail {settings.contact_email}" |
| 95 | 98 | out["free"]["how_to_get"] = f"{settings.public_url}/signup" |
| 96 | 99 | return out |
modified
hfmarketdata/web/src/pages/admin/AdminUsers.jsx
+1 −1
@@ -4,7 +4,7 @@ import { api } from '../../app/api.js' | ||
| 4 | 4 | import PgCallout from '../../components/PgCallout.jsx' |
| 5 | 5 | import PgCopyButton from '../../components/PgCopyButton.jsx' |
| 6 | 6 | |
| 7 | −const TIERS = ['free', 'high_usage'] | |
| 7 | +const TIERS = ['free', 'high_usage', 'unlimited'] | |
| 8 | 8 | const ROLES = ['user', 'admin'] |
| 9 | 9 | const STATUSES = ['invited', 'active', 'disabled'] |
| 10 | 10 | const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ |
modified
hfmarketdata/web/src/pages/dashboard/Account.jsx
+1 −1
@@ -42,7 +42,7 @@ export default function Account() { | ||
| 42 | 42 | <li>{tier.rows.toLocaleString()} rows / {tier.window}</li> |
| 43 | 43 | <li>{tier.maxRows.toLocaleString()} rows per request</li> |
| 44 | 44 | </ul> |
| 45 | − {tier.id !== 'high_usage' && ( | |
| 45 | + {tier.id !== 'high_usage' && tier.id !== 'unlimited' && ( | |
| 46 | 46 | <a className="btn btn-primary" href={`mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent('HF Market Data — high usage tier')}&body=${encodeURIComponent(`Hello,\n\nI would like the high-usage tier for ${user.email}.\nUse case: `)}`}>Need more? {CONTACT_EMAIL}</a> |
| 47 | 47 | )} |
| 48 | 48 | </div> |
| 49 | 49 | |