feat: add auth and account routes

This commit is contained in:
agatha 2026-03-14 16:34:14 -04:00
parent f65179bd0b
commit 396ed3d029
2 changed files with 168 additions and 0 deletions

View File

@ -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
],
)

View File

@ -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