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:
@@ -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,
|
||||
)
|
||||
@@ -1,39 +1,74 @@
|
||||
from typing import List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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 CreateKeyRequest, MessageResponse, UpdateKeyRequest
|
||||
from app.models import User
|
||||
from app.services.key_service import KeyService
|
||||
from app.datamodels.schemas import CreateKeyRequest, ApiKeyResponse, ApiKeyCreatedResponse, MessageResponse
|
||||
|
||||
router = APIRouter(prefix="/keys", tags=["keys"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[ApiKeyResponse])
|
||||
@router.get("")
|
||||
async def list_keys(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
sort_by: str = Query("created_at"),
|
||||
sort_order: str = Query("desc"),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await KeyService.list_keys(db, user.id)
|
||||
) -> Dict[str, Any]:
|
||||
return await KeyService.list_keys(
|
||||
db, user, page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=ApiKeyCreatedResponse)
|
||||
@router.get("/meta/available-groups")
|
||||
async def available_groups(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> List[Dict[str, Any]]:
|
||||
return await KeyService.available_groups(db, user)
|
||||
|
||||
|
||||
@router.get("/{key_id}")
|
||||
async def get_key(
|
||||
key_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
return await KeyService.get_key(db, user, key_id)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_key(
|
||||
body: CreateKeyRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await KeyService.create_key(db, user.id, body.name)
|
||||
) -> Dict[str, Any]:
|
||||
payload = body.model_dump(exclude_none=True)
|
||||
return await KeyService.create_key(db, user, payload)
|
||||
|
||||
|
||||
@router.put("/{key_id}")
|
||||
async def update_key(
|
||||
key_id: int,
|
||||
body: UpdateKeyRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
payload = body.model_dump(exclude_none=True)
|
||||
return await KeyService.update_key(db, user, key_id, payload)
|
||||
|
||||
|
||||
@router.delete("/{key_id}", response_model=MessageResponse)
|
||||
async def delete_key(
|
||||
key_id: str,
|
||||
key_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
await KeyService.delete_key(db, user.id, key_id)
|
||||
return {"message": "Key deleted"}
|
||||
await KeyService.delete_key(db, user, key_id)
|
||||
return {"message": "API key deleted successfully"}
|
||||
@@ -1,64 +1,112 @@
|
||||
from datetime import date
|
||||
from typing import List, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
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 DashboardAPIKeysUsageRequest
|
||||
from app.models import User
|
||||
from app.services.usage_service import UsageService
|
||||
from app.datamodels.schemas import (
|
||||
UsageSummaryResponse, DailyUsageResponse,
|
||||
ModelUsageResponse, KeyUsageResponse, UsageLogResponse,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/usage", tags=["usage"])
|
||||
|
||||
|
||||
@router.get("/summary", response_model=UsageSummaryResponse)
|
||||
async def usage_summary(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await UsageService.summary(db, user.id)
|
||||
|
||||
|
||||
@router.get("/daily", response_model=List[DailyUsageResponse])
|
||||
async def usage_daily(
|
||||
start: Optional[date] = Query(None),
|
||||
end: Optional[date] = Query(None),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await UsageService.daily(db, user.id, start, end)
|
||||
|
||||
|
||||
@router.get("/by-model", response_model=List[ModelUsageResponse])
|
||||
async def usage_by_model(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await UsageService.by_model(db, user.id)
|
||||
|
||||
|
||||
@router.get("/by-key", response_model=List[KeyUsageResponse])
|
||||
async def usage_by_key(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await UsageService.by_key(db, user.id)
|
||||
|
||||
|
||||
@router.get("/logs", response_model=List[UsageLogResponse])
|
||||
async def usage_logs(
|
||||
@router.get("")
|
||||
async def list_logs(
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
sort_by: str = Query("created_at"),
|
||||
sort_order: str = Query("desc"),
|
||||
api_key_id: Optional[int] = Query(None),
|
||||
model: Optional[str] = Query(None),
|
||||
key_id: Optional[str] = Query(None),
|
||||
start: Optional[date] = Query(None),
|
||||
end: Optional[date] = Query(None),
|
||||
request_type: Optional[str] = Query(None),
|
||||
stream: Optional[bool] = Query(None),
|
||||
billing_type: Optional[int] = Query(None),
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
timezone: Optional[str] = Query(None),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await UsageService.logs(db, user.id, page, size, model, key_id, start, end)
|
||||
) -> Dict[str, Any]:
|
||||
return await UsageService.list_logs(
|
||||
db,
|
||||
user,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
api_key_id=api_key_id,
|
||||
model=model,
|
||||
request_type=request_type,
|
||||
stream=stream,
|
||||
billing_type=billing_type,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def stats(
|
||||
period: Optional[str] = Query(None, regex="^(today|week|month)$"),
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
api_key_id: Optional[int] = Query(None),
|
||||
timezone: Optional[str] = Query(None),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
return await UsageService.stats(
|
||||
db,
|
||||
user,
|
||||
period=period,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
api_key_id=api_key_id,
|
||||
timezone=timezone,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/dashboard/stats")
|
||||
async def dashboard_stats(
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
return await UsageService.dashboard_stats(db, user)
|
||||
|
||||
|
||||
@router.get("/dashboard/trend")
|
||||
async def dashboard_trend(
|
||||
granularity: str = Query("day"),
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
timezone: Optional[str] = Query(None),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
return await UsageService.dashboard_trend(
|
||||
db, user, granularity=granularity, start_date=start_date, end_date=end_date, timezone=timezone
|
||||
)
|
||||
|
||||
|
||||
@router.get("/dashboard/models")
|
||||
async def dashboard_models(
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
timezone: Optional[str] = Query(None),
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
return await UsageService.dashboard_models(
|
||||
db, user, start_date=start_date, end_date=end_date, timezone=timezone
|
||||
)
|
||||
|
||||
|
||||
@router.post("/dashboard/api-keys-usage")
|
||||
async def dashboard_api_keys_usage(
|
||||
body: DashboardAPIKeysUsageRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> Dict[str, Any]:
|
||||
return await UsageService.dashboard_api_keys_usage(db, user, body.api_key_ids)
|
||||
Reference in New Issue
Block a user