This commit is contained in:
2026-01-02 10:43:20 -06:00
commit 14d9af3036
112 changed files with 14274 additions and 0 deletions

View File

View File

@ -0,0 +1,82 @@
from fastapi import APIRouter, Depends, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from jose import JWTError
from app.database import get_db
from app.schemas.user import UserCreate, UserLogin, TokenResponse, UserResponse
from app.services.auth_service import register_user, login_user
from app.crud.user import get_user_by_id
from app.utils.security import decode_token
from app.utils.exceptions import UnauthorizedError
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db)
):
try:
payload = decode_token(token)
user_id: str = payload.get("sub")
if user_id is None:
raise UnauthorizedError()
except JWTError:
raise UnauthorizedError()
user = await get_user_by_id(db, int(user_id))
if user is None:
raise UnauthorizedError()
return user
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserCreate,
db: AsyncSession = Depends(get_db)
):
return await register_user(db, user_data)
@router.post("/login", response_model=TokenResponse)
async def login(
login_data: UserLogin,
db: AsyncSession = Depends(get_db)
):
return await login_user(db, login_data)
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user = Depends(get_current_user)
):
return current_user
@router.post("/refresh", response_model=TokenResponse)
async def refresh_token(
token: str,
db: AsyncSession = Depends(get_db)
):
try:
payload = decode_token(token)
user_id: str = payload.get("sub")
if user_id is None:
raise UnauthorizedError()
except JWTError:
raise UnauthorizedError()
user = await get_user_by_id(db, int(user_id))
if user is None:
raise UnauthorizedError()
from app.utils.security import create_access_token, create_refresh_token
access_token = create_access_token({"sub": str(user.id)})
new_refresh_token = create_refresh_token({"sub": str(user.id)})
return TokenResponse(
access_token=access_token,
refresh_token=new_refresh_token,
)

173
backend/app/routers/bets.py Normal file
View File

@ -0,0 +1,173 @@
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas.bet import BetCreate, BetUpdate, BetResponse, BetDetailResponse, SettleBetRequest
from app.routers.auth import get_current_user
from app.crud.bet import get_bet_by_id, get_open_bets, get_user_bets, create_bet
from app.services.bet_service import accept_bet, settle_bet, cancel_bet
from app.models import User, BetCategory, BetStatus
from app.utils.exceptions import BetNotFoundError, NotBetParticipantError
router = APIRouter(prefix="/api/v1/bets", tags=["bets"])
@router.get("", response_model=list[BetResponse])
async def list_bets(
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
category: BetCategory | None = None,
db: AsyncSession = Depends(get_db)
):
bets = await get_open_bets(db, skip=skip, limit=limit, category=category)
return bets
@router.post("", response_model=BetResponse)
async def create_new_bet(
bet_data: BetCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
bet = await create_bet(db, bet_data, current_user.id)
await db.commit()
bet = await get_bet_by_id(db, bet.id)
return bet
@router.get("/{bet_id}", response_model=BetDetailResponse)
async def get_bet(
bet_id: int,
db: AsyncSession = Depends(get_db)
):
bet = await get_bet_by_id(db, bet_id)
if not bet:
raise BetNotFoundError()
return bet
@router.put("/{bet_id}", response_model=BetResponse)
async def update_bet(
bet_id: int,
bet_data: BetUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
bet = await get_bet_by_id(db, bet_id)
if not bet:
raise BetNotFoundError()
if bet.creator_id != current_user.id:
raise NotBetParticipantError()
if bet.status != BetStatus.OPEN:
raise ValueError("Cannot update non-open bet")
# Update fields
if bet_data.title is not None:
bet.title = bet_data.title
if bet_data.description is not None:
bet.description = bet_data.description
if bet_data.event_date is not None:
bet.event_date = bet_data.event_date
if bet_data.creator_position is not None:
bet.creator_position = bet_data.creator_position
if bet_data.opponent_position is not None:
bet.opponent_position = bet_data.opponent_position
if bet_data.stake_amount is not None:
bet.stake_amount = bet_data.stake_amount
if bet_data.creator_odds is not None:
bet.creator_odds = bet_data.creator_odds
if bet_data.opponent_odds is not None:
bet.opponent_odds = bet_data.opponent_odds
if bet_data.expires_at is not None:
bet.expires_at = bet_data.expires_at
await db.commit()
await db.refresh(bet)
return bet
@router.delete("/{bet_id}")
async def delete_bet(
bet_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
await cancel_bet(db, bet_id, current_user.id)
return {"message": "Bet cancelled successfully"}
@router.post("/{bet_id}/accept", response_model=BetResponse)
async def accept_bet_route(
bet_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
bet = await accept_bet(db, bet_id, current_user.id)
return bet
@router.post("/{bet_id}/settle", response_model=BetDetailResponse)
async def settle_bet_route(
bet_id: int,
settle_data: SettleBetRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
bet = await settle_bet(db, bet_id, settle_data.winner_id, current_user.id)
return bet
@router.get("/my/created", response_model=list[BetResponse])
async def get_my_created_bets(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.models import Bet
result = await db.execute(
select(Bet)
.where(Bet.creator_id == current_user.id)
.options(selectinload(Bet.creator), selectinload(Bet.opponent))
.order_by(Bet.created_at.desc())
)
return list(result.scalars().all())
@router.get("/my/accepted", response_model=list[BetResponse])
async def get_my_accepted_bets(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.models import Bet
result = await db.execute(
select(Bet)
.where(Bet.opponent_id == current_user.id)
.options(selectinload(Bet.creator), selectinload(Bet.opponent))
.order_by(Bet.created_at.desc())
)
return list(result.scalars().all())
@router.get("/my/active", response_model=list[BetResponse])
async def get_my_active_bets(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
bets = await get_user_bets(db, current_user.id)
active_bets = [bet for bet in bets if bet.status in [BetStatus.MATCHED, BetStatus.IN_PROGRESS]]
return active_bets
@router.get("/my/history", response_model=list[BetDetailResponse])
async def get_my_bet_history(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
bets = await get_user_bets(db, current_user.id, status=BetStatus.COMPLETED)
return bets

View File

@ -0,0 +1,62 @@
from fastapi import APIRouter, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas.user import UserResponse, UserUpdate, UserStats
from app.routers.auth import get_current_user
from app.crud.user import get_user_by_id
from app.crud.bet import get_user_bets
from app.models import User, BetStatus
from app.utils.exceptions import UserNotFoundError
router = APIRouter(prefix="/api/v1/users", tags=["users"])
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
db: AsyncSession = Depends(get_db)
):
user = await get_user_by_id(db, user_id)
if not user:
raise UserNotFoundError()
return user
@router.put("/me", response_model=UserResponse)
async def update_current_user(
user_data: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
if user_data.display_name is not None:
current_user.display_name = user_data.display_name
if user_data.avatar_url is not None:
current_user.avatar_url = user_data.avatar_url
if user_data.bio is not None:
current_user.bio = user_data.bio
await db.commit()
await db.refresh(current_user)
return current_user
@router.get("/{user_id}/stats", response_model=UserStats)
async def get_user_stats(
user_id: int,
db: AsyncSession = Depends(get_db)
):
user = await get_user_by_id(db, user_id)
if not user:
raise UserNotFoundError()
# Get active bets count
user_bets = await get_user_bets(db, user_id)
active_bets = sum(1 for bet in user_bets if bet.status in [BetStatus.MATCHED, BetStatus.IN_PROGRESS])
return UserStats(
total_bets=user.total_bets,
wins=user.wins,
losses=user.losses,
win_rate=user.win_rate,
active_bets=active_bets,
)

View File

@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas.wallet import WalletResponse, DepositRequest, WithdrawalRequest, TransactionResponse
from app.routers.auth import get_current_user
from app.crud.wallet import get_user_wallet, get_wallet_transactions
from app.services.wallet_service import deposit_funds, withdraw_funds
from app.models import User
router = APIRouter(prefix="/api/v1/wallet", tags=["wallet"])
@router.get("", response_model=WalletResponse)
async def get_wallet(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
wallet = await get_user_wallet(db, current_user.id)
return wallet
@router.post("/deposit", response_model=WalletResponse)
async def deposit(
deposit_data: DepositRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
wallet = await deposit_funds(db, current_user.id, deposit_data.amount)
return wallet
@router.post("/withdraw", response_model=WalletResponse)
async def withdraw(
withdrawal_data: WithdrawalRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
wallet = await withdraw_funds(db, current_user.id, withdrawal_data.amount)
return wallet
@router.get("/transactions", response_model=list[TransactionResponse])
async def get_transactions(
limit: int = 50,
offset: int = 0,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
transactions = await get_wallet_transactions(db, current_user.id, limit, offset)
return transactions

View File

@ -0,0 +1,43 @@
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
from typing import Dict
import json
router = APIRouter(tags=["websocket"])
# Store active connections
active_connections: Dict[int, WebSocket] = {}
@router.websocket("/api/v1/ws")
async def websocket_endpoint(websocket: WebSocket, token: str = Query(...)):
await websocket.accept()
# In a real implementation, you would validate the token here
# For MVP, we'll accept all connections
user_id = 1 # Placeholder
active_connections[user_id] = websocket
try:
while True:
data = await websocket.receive_text()
# Handle incoming messages if needed
except WebSocketDisconnect:
if user_id in active_connections:
del active_connections[user_id]
async def broadcast_event(event_type: str, data: dict, user_ids: list[int] = None):
"""Broadcast an event to specific users or all connected users"""
message = json.dumps({
"type": event_type,
"data": data
})
if user_ids:
for user_id in user_ids:
if user_id in active_connections:
await active_connections[user_id].send_text(message)
else:
for connection in active_connections.values():
await connection.send_text(message)