From 396ed3d029b47d61b582bfd6baf9c04f7f02aa13 Mon Sep 17 00:00:00 2001 From: agatha Date: Sat, 14 Mar 2026 16:34:14 -0400 Subject: [PATCH] feat: add auth and account routes --- src/proxy_pool/accounts/router.py | 165 ++++++++++++++++++++++++++++++ src/proxy_pool/app.py | 3 + 2 files changed, 168 insertions(+) create mode 100644 src/proxy_pool/accounts/router.py diff --git a/src/proxy_pool/accounts/router.py b/src/proxy_pool/accounts/router.py new file mode 100644 index 0000000..d91e3b1 --- /dev/null +++ b/src/proxy_pool/accounts/router.py @@ -0,0 +1,165 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from proxy_pool.accounts.auth import get_current_user +from proxy_pool.accounts.models import ApiKey, CreditLedger, User +from proxy_pool.accounts.schemas import ( + ApiKeyCreate, + ApiKeyCreated, + ApiKeyResponse, + CreditBalanceResponse, + CreditTransactionResponse, + RegisterResponse, + UserCreate, + UserResponse, +) +from proxy_pool.accounts.service import ( + create_user, + generate_api_key, + get_credit_balance, +) +from proxy_pool.common.dependencies import get_db +from proxy_pool.config import get_settings + +auth_router = APIRouter(prefix="/auth", tags=["auth"]) +account_router = APIRouter(prefix="/account", tags=["account"]) + + +@auth_router.post( + "/register", + response_model=RegisterResponse, + status_code=status.HTTP_201_CREATED, +) +async def register( + body: UserCreate, + db: AsyncSession = Depends(get_db), +) -> RegisterResponse: + settings = get_settings() + + # Check for duplicate email + existing = await db.execute(select(User).where(User.email == body.email)) + if existing.scalar_one_or_none() is not None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="A user with this email already exists", + ) + + user, raw_key = await create_user( + db, + email=body.email, + display_name=body.display_name, + settings=settings.account, + ) + + # Find the key we just created to build the response + key_result = await db.execute(select(ApiKey).where(ApiKey.user_id == user.id)) + api_key = key_result.scalar_one() + + return RegisterResponse( + user=UserResponse.model_validate(user), + api_key=ApiKeyCreated( + id=api_key.id, + key=raw_key, + prefix=api_key.prefix, + label=api_key.label, + ), + ) + + +@auth_router.post( + "/keys", + response_model=ApiKeyCreated, + status_code=status.HTTP_201_CREATED, +) +async def create_api_key( + body: ApiKeyCreate, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> ApiKeyCreated: + settings = get_settings() + + raw_key, key_hash, prefix = generate_api_key(settings.account.api_key_prefix) + api_key = ApiKey( + user_id=user.id, + key_hash=key_hash, + prefix=prefix, + label=body.label, + ) + db.add(api_key) + await db.commit() + await db.refresh(api_key) + + return ApiKeyCreated( + id=api_key.id, + key=raw_key, + prefix=api_key.prefix, + label=api_key.label, + ) + + +@auth_router.get("/keys", response_model=list[ApiKeyResponse]) +async def list_api_keys( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> list[ApiKeyResponse]: + result = await db.execute( + select(ApiKey) + .where(ApiKey.user_id == user.id) + .order_by(ApiKey.created_at.desc()) + ) + keys = result.scalars().all() + return [ApiKeyResponse.model_validate(k) for k in keys] + + +@auth_router.delete("/keys/{key_id}", status_code=status.HTTP_204_NO_CONTENT) +async def revoke_api_key( + key_id: str, + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> None: + result = await db.execute( + select(ApiKey).where( + ApiKey.id == key_id, + ApiKey.user_id == user.id, + ) + ) + api_key = result.scalar_one_or_none() + if api_key is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="API key not found", + ) + + api_key.is_active = False + await db.commit() + + +@account_router.get("", response_model=UserResponse) +async def get_account( + user: User = Depends(get_current_user), +) -> UserResponse: + return UserResponse.model_validate(user) + + +@account_router.get("/credits", response_model=CreditBalanceResponse) +async def get_credits( + user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +) -> CreditBalanceResponse: + balance = await get_credit_balance(db, user.id) + + result = await db.execute( + select(CreditLedger) + .where(CreditLedger.user_id == user.id) + .order_by(CreditLedger.created_at.desc()) + .limit(20) + ) + transactions = result.scalars().all() + + return CreditBalanceResponse( + balance=balance, + recent_transactions=[ + CreditTransactionResponse.model_validate(t) for t in transactions + ], + ) diff --git a/src/proxy_pool/app.py b/src/proxy_pool/app.py index 42eb622..9801269 100644 --- a/src/proxy_pool/app.py +++ b/src/proxy_pool/app.py @@ -4,6 +4,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from redis.asyncio import from_url as redis_from_url +from proxy_pool.accounts.router import account_router, auth_router from proxy_pool.common.router import router as health_router from proxy_pool.config import get_settings from proxy_pool.db.session import create_session_factory @@ -63,5 +64,7 @@ def create_app() -> FastAPI: app.include_router(health_router) app.include_router(source_router) app.include_router(proxy_router) + app.include_router(auth_router) + app.include_router(account_router) return app