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

@@ -3,12 +3,20 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.dependencies import get_current_user
from app.datamodels.schemas import (
ForgotPasswordRequest,
LoginRequest,
MessageResponse,
RefreshRequest,
RegisterRequest,
ResetPasswordRequest,
TokenResponse,
UserResponse,
)
from app.integrations.sub2api import admin as sub2api_admin
from app.integrations.sub2api.client import Sub2APIError, Sub2APITransportError
from app.models import User
from app.services.auth_service import AuthService
from app.datamodels.schemas import (
RegisterRequest, LoginRequest, TokenResponse, RefreshRequest,
ForgotPasswordRequest, ResetPasswordRequest, UserResponse, MessageResponse,
)
router = APIRouter(prefix="/auth", tags=["auth"])
@@ -16,7 +24,7 @@ router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", response_model=UserResponse)
async def register(body: RegisterRequest, db: AsyncSession = Depends(get_db)):
user = await AuthService.register(db, body.email, body.password)
return user
return _user_to_response(user)
@router.post("/login", response_model=TokenResponse)
@@ -38,7 +46,7 @@ async def logout():
async def forgot_password(body: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
token = await AuthService.forgot_password(db, body.email)
if token:
# MVP: print token to console; production: send email
# MVP: print to console; production should send via email
print(f"[Password Reset] email={body.email} token={token}")
return {"message": "If the email exists, a reset link has been sent"}
@@ -51,4 +59,24 @@ async def reset_password(body: ResetPasswordRequest, db: AsyncSession = Depends(
@router.get("/me", response_model=UserResponse)
async def me(user: User = Depends(get_current_user)):
return user
"""Merge the local row with sub2api's live balance when available."""
response = _user_to_response(user)
if user.sub2api_user_id:
try:
remote = await sub2api_admin.get_user(user.sub2api_user_id)
response.balance = float(remote.get("balance") or 0)
except (Sub2APIError, Sub2APITransportError):
# non-fatal; fall back to 0
pass
return response
def _user_to_response(user: User) -> UserResponse:
return UserResponse(
id=user.id,
email=user.email,
status=user.status,
created_at=user.created_at,
sub2api_user_id=user.sub2api_user_id,
balance=0.0,
)