strat-gameplay-webapp/backend/app/services/sba_api_client.py
Cal Corum 9b30d3dfb2 CLAUDE: Implement Discord OAuth authentication and SBA API integration
## Authentication Implementation

### Backend
- Implemented complete Discord OAuth flow in auth.py:
  * POST /api/auth/discord/callback - Exchange code for tokens
  * POST /api/auth/refresh - Refresh JWT tokens
  * GET /api/auth/me - Get authenticated user info
  * GET /api/auth/verify - Verify auth status
- JWT token creation with 7-day expiration
- Refresh token support for session persistence
- Bearer token authentication for Discord API calls

### Frontend
- Created auth/login.vue - Discord OAuth initiation page
- Created auth/callback.vue - OAuth callback handler with states
- Integrated with existing auth store (already implemented)
- LocalStorage persistence for tokens and user data
- Full error handling and loading states

### Configuration
- Updated backend .env with Discord OAuth credentials
- Updated frontend .env with Discord Client ID
- Fixed redirect URI to port 3001

## SBA API Integration

### Backend
- Extended SbaApiClient with get_teams(season, active_only=True)
- Added bearer token auth support (_get_headers method)
- Created /api/teams route with TeamResponse model
- Registered teams router in main.py
- Filters out IL (Injured List) teams automatically
- Returns team data: id, abbrev, names, color, gmid, division

### Integration
- Connected to production SBA API: https://api.sba.manticorum.com
- Bearer token authentication working
- Successfully fetches ~16 active Season 3 teams

## Documentation
- Created SESSION_NOTES.md - Current session accomplishments
- Created NEXT_SESSION.md - Game creation implementation guide
- Updated implementation/NEXT_SESSION.md

## Testing
-  Discord OAuth flow tested end-to-end
-  User authentication and session persistence verified
-  Teams API returns real data from production
-  All services running and communicating

## What Works Now
- User can sign in with Discord
- Sessions persist across reloads
- Backend fetches real teams from SBA API
- Ready for game creation implementation

## Next Steps
See .claude/NEXT_SESSION.md for detailed game creation implementation plan.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 16:54:27 -06:00

178 lines
5.6 KiB
Python

"""
SBA API client for fetching player and team data.
Integrates with SBA REST API to retrieve player and team information
for use in game lineup display and game creation.
Author: Claude
Date: 2025-01-10
"""
import logging
from typing import Any
import httpx
from app.config import get_settings
from app.models.player_models import SbaPlayer
logger = logging.getLogger(f"{__name__}.SbaApiClient")
settings = get_settings()
class SbaApiClient:
"""Client for SBA API player and team data lookups."""
def __init__(
self,
base_url: str = "https://api.sba.manticorum.com",
api_key: str | None = None,
):
"""
Initialize SBA API client.
Args:
base_url: Base URL for SBA API (default: production)
api_key: Bearer token for API authentication
"""
self.base_url = base_url
self.api_key = api_key or settings.sba_api_key
self.timeout = httpx.Timeout(10.0, connect=5.0)
def _get_headers(self) -> dict[str, str]:
"""Get headers with auth token."""
headers = {}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
return headers
async def get_teams(self, season: int, active_only: bool = True) -> list[dict[str, Any]]:
"""
Fetch teams from SBA API for a specific season.
Args:
season: Season number (e.g., 3 for Season 3)
active_only: If True, filter out IL teams and teams without divisions
Returns:
List of team dictionaries with id, name, abbreviation, etc.
Example:
teams = await client.get_teams(season=3)
for team in teams:
print(f"{team['abbrev']}: {team['lname']}")
"""
url = f"{self.base_url}/teams"
params = {"season": season}
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(
url, headers=self._get_headers(), params=params
)
response.raise_for_status()
data = response.json()
teams = data.get("teams", [])
if active_only:
# Filter out injured list teams (IL suffix) and teams without divisions
teams = [
t for t in teams
if not t["abbrev"].endswith("IL") and t.get("division")
]
logger.info(f"Loaded {len(teams)} teams for season {season}")
return teams
except httpx.HTTPError as e:
logger.error(f"Failed to fetch teams: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error fetching teams: {e}")
raise
async def get_player(self, player_id: int) -> SbaPlayer:
"""
Fetch player data from SBA API.
Args:
player_id: SBA player ID
Returns:
SbaPlayer instance with all player data
Raises:
httpx.HTTPError: If API request fails
Example:
player = await client.get_player(12288)
print(f"Name: {player.name}") # "Ronald Acuna Jr"
print(f"Positions: {player.get_positions()}") # ['RF']
"""
url = f"{self.base_url}/players/{player_id}"
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(url, headers=self._get_headers())
response.raise_for_status()
data = response.json()
player = SbaPlayer.from_api_response(data)
logger.info(f"Loaded player {player_id}: {player.name}")
return player
except httpx.HTTPError as e:
logger.error(f"Failed to fetch player {player_id}: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error fetching player {player_id}: {e}")
raise
async def get_players_batch(self, player_ids: list[int]) -> dict[int, SbaPlayer]:
"""
Fetch multiple players in parallel.
Args:
player_ids: List of SBA player IDs to fetch
Returns:
Dictionary mapping player_id to SbaPlayer instance
Players that fail to load will be omitted from the result
Example:
players = await client.get_players_batch([12288, 12289, 12290])
for player_id, player in players.items():
print(f"{player.name}: {player.get_positions()}")
"""
if not player_ids:
return {}
results: dict[int, SbaPlayer] = {}
async with httpx.AsyncClient(timeout=self.timeout) as client:
for player_id in player_ids:
try:
url = f"{self.base_url}/players/{player_id}"
response = await client.get(url)
response.raise_for_status()
data = response.json()
player = SbaPlayer.from_api_response(data)
results[player_id] = player
except httpx.HTTPError as e:
logger.warning(f"Failed to fetch player {player_id}: {e}")
# Continue with other players
except Exception as e:
logger.warning(f"Unexpected error fetching player {player_id}: {e}")
# Continue with other players
logger.info(f"Loaded {len(results)}/{len(player_ids)} players")
return results
# Singleton instance
sba_api_client = SbaApiClient()