integrate sub2api as upstream for auth/keys/usage via FastAPI BFF

Preserve local user table for superDream-specific features while syncing
user lifecycle, API key CRUD and usage queries through sub2api. Admin token
handles reads and user lifecycle; per-user tokens (Fernet-encrypted in DB)
handle key writes that admin endpoints do not expose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xuyong
2026-04-17 21:23:08 +08:00
parent 20e842a60a
commit 35c0b7de16
30 changed files with 1707 additions and 803 deletions

View File

@@ -0,0 +1,97 @@
"""Admin-token-authenticated calls to sub2api.
Used for:
- User lifecycle sync (create / update / delete / lookup)
- Reading a user's API keys and usage (admin endpoints only support reads)
"""
from __future__ import annotations
from typing import Any
from app.integrations.sub2api.client import admin_request
# ── Users ─────────────────────────────────────────────────────────────
async def create_user(
*,
email: str,
password: str,
username: str = "",
notes: str = "",
balance: float = 0,
concurrency: int = 0,
allowed_groups: list[int] | None = None,
) -> dict[str, Any]:
return await admin_request(
"POST",
"/admin/users",
json={
"email": email,
"password": password,
"username": username,
"notes": notes,
"balance": balance,
"concurrency": concurrency,
"allowed_groups": allowed_groups or [],
},
)
async def update_user(user_id: int, **fields: Any) -> dict[str, Any]:
"""Partial update. Only non-None fields are sent."""
payload = {k: v for k, v in fields.items() if v is not None}
return await admin_request("PUT", f"/admin/users/{user_id}", json=payload)
async def delete_user(user_id: int) -> None:
await admin_request("DELETE", f"/admin/users/{user_id}")
async def get_user(user_id: int) -> dict[str, Any]:
return await admin_request("GET", f"/admin/users/{user_id}")
async def find_user_by_email(email: str) -> dict[str, Any] | None:
data = await admin_request(
"GET",
"/admin/users",
params={"search": email, "page": 1, "page_size": 5},
)
items = (data or {}).get("items") or []
for item in items:
if (item.get("email") or "").lower() == email.lower():
return item
return None
# ── API Keys (read-only from admin side) ──────────────────────────────
async def list_user_api_keys(
user_id: int,
*,
page: int = 1,
page_size: int = 20,
sort_by: str = "created_at",
sort_order: str = "desc",
) -> dict[str, Any]:
return await admin_request(
"GET",
f"/admin/users/{user_id}/api-keys",
params={
"page": page,
"page_size": page_size,
"sort_by": sort_by,
"sort_order": sort_order,
},
)
# ── Usage (admin view per user) ───────────────────────────────────────
async def get_user_usage_stats(user_id: int, period: str = "month") -> dict[str, Any]:
return await admin_request(
"GET",
f"/admin/users/{user_id}/usage",
params={"period": period},
)