- Data fetched from Open5e API: 3,215 items (339 spells, 330 creatures, 24 classes, 2,319 magic items, 203 equipment) - FastAPI app with API key auth (X-API-Key header or ?api_key= param) - Sliding window rate limiting (60 req/min, 10K req/day) - Dice rolling endpoint (e.g., /api/dice/roll?spec=2d20+5) - Full-text search across all resource types - Pagination, filtering (name, level, school, class, etc.) - Admin CLI for API key management - nginx + systemd service ready for deployment
94 lines
No EOL
2.6 KiB
Python
94 lines
No EOL
2.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Admin CLI for managing D&D SRD API keys.
|
|
|
|
Usage:
|
|
python scripts/manage_keys.py list
|
|
python scripts/manage_keys.py create <name> [--rate-limit N]
|
|
python scripts/manage_keys.py revoke <key>
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import secrets
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
API_KEYS_FILE = Path(__file__).resolve().parent.parent / "data" / "api_keys.json"
|
|
|
|
|
|
def load_keys() -> dict:
|
|
if API_KEYS_FILE.exists():
|
|
with open(API_KEYS_FILE) as f:
|
|
return json.load(f)
|
|
return {"keys": {}}
|
|
|
|
|
|
def save_keys(keys: dict):
|
|
API_KEYS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(API_KEYS_FILE, "w") as f:
|
|
json.dump(keys, f, indent=2)
|
|
|
|
|
|
def cmd_list(args):
|
|
keys = load_keys()
|
|
if not keys["keys"]:
|
|
print("No API keys configured.")
|
|
return
|
|
print(f"{'Name':<20} {'Key':<40} {'Created':<20}")
|
|
print("-" * 80)
|
|
for key, info in keys["keys"].items():
|
|
print(f"{info.get('name', 'unknown'):<20} {key:<40} {info.get('created', ''):<20}")
|
|
|
|
|
|
def cmd_create(args):
|
|
keys = load_keys()
|
|
new_key = "dnd_" + secrets.token_hex(16)
|
|
keys["keys"][new_key] = {
|
|
"name": args.name,
|
|
"created": os.popen("date -Iseconds").read().strip(),
|
|
"rate_limit": args.rate_limit or 60,
|
|
}
|
|
save_keys(keys)
|
|
print(f"✅ Created API key for '{args.name}':")
|
|
print(f" Key: {new_key}")
|
|
print(f" Rate limit: {args.rate_limit or 60} req/min")
|
|
|
|
|
|
def cmd_revoke(args):
|
|
keys = load_keys()
|
|
if args.key in keys["keys"]:
|
|
info = keys["keys"].pop(args.key)
|
|
save_keys(keys)
|
|
print(f"✅ Revoked key for '{info['name']}' ({args.key[:20]}...)")
|
|
else:
|
|
print(f"❌ Key not found: {args.key[:20]}...")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="D&D SRD API Key Manager")
|
|
sub = parser.add_subparsers(dest="command")
|
|
|
|
p_list = sub.add_parser("list", help="List all API keys")
|
|
|
|
p_create = sub.add_parser("create", help="Create a new API key")
|
|
p_create.add_argument("name", help="Name for the key (e.g., 'jez-personal')")
|
|
p_create.add_argument("--rate-limit", type=int, default=60, help="Requests per minute limit")
|
|
|
|
p_revoke = sub.add_parser("revoke", help="Revoke an API key")
|
|
p_revoke.add_argument("key", help="Full API key to revoke")
|
|
|
|
args = parser.parse_args()
|
|
if args.command == "list":
|
|
cmd_list(args)
|
|
elif args.command == "create":
|
|
cmd_create(args)
|
|
elif args.command == "revoke":
|
|
cmd_revoke(args)
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |