mantimon-tcg/backend/app/api/collections.py
Cal Corum 3ec670753b Fix security and validation issues from code review
Critical fixes:
- Add admin API key authentication for admin endpoints
- Add race condition protection via unique partial index for starter decks
- Make starter deck selection atomic with combined method

Moderate fixes:
- Fix DI pattern violation in validate_deck_endpoint
- Add card ID format validation (regex pattern)
- Add card quantity validation (1-99 range)
- Fix exception chaining with from None (B904)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:16:07 -06:00

137 lines
3.9 KiB
Python

"""Collections API router for Mantimon TCG.
This module provides REST endpoints for card collection management.
Users can view their collections and check individual card ownership.
Endpoints:
GET /collections/me - Get user's full collection
GET /collections/me/cards/{card_id} - Get quantity of a specific card
POST /collections/admin/{user_id}/add - Admin: Add cards to user (requires admin key)
"""
from uuid import UUID
from fastapi import APIRouter, HTTPException, status
from app.api.deps import AdminAuth, CollectionServiceDep, CurrentUser
from app.schemas.collection import (
CollectionAddRequest,
CollectionCardResponse,
CollectionEntryResponse,
CollectionResponse,
)
router = APIRouter(prefix="/collections", tags=["collections"])
@router.get("/me", response_model=CollectionResponse)
async def get_my_collection(
current_user: CurrentUser,
collection_service: CollectionServiceDep,
) -> CollectionResponse:
"""Get the authenticated user's card collection.
Returns all cards the user owns with quantities and source information.
Returns:
CollectionResponse with summary stats and all entries.
"""
collection = await collection_service.get_collection(current_user.id)
entries = [
CollectionEntryResponse(
card_definition_id=entry.card_definition_id,
quantity=entry.quantity,
source=entry.source,
obtained_at=entry.obtained_at,
)
for entry in collection
]
total_cards = sum(entry.quantity for entry in collection)
return CollectionResponse(
total_unique_cards=len(collection),
total_card_count=total_cards,
entries=entries,
)
@router.get("/me/cards/{card_id}", response_model=CollectionCardResponse)
async def get_card_quantity(
card_id: str,
current_user: CurrentUser,
collection_service: CollectionServiceDep,
) -> CollectionCardResponse:
"""Get the quantity of a specific card in user's collection.
Args:
card_id: The card definition ID (e.g., "a1-001-bulbasaur").
Returns:
CollectionCardResponse with card_id and quantity.
Raises:
404: If card not in collection (quantity is 0).
"""
quantity = await collection_service.get_card_quantity(current_user.id, card_id)
if quantity == 0:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Card '{card_id}' not in collection",
)
return CollectionCardResponse(
card_definition_id=card_id,
quantity=quantity,
)
@router.post(
"/admin/{user_id}/add",
response_model=CollectionEntryResponse,
status_code=status.HTTP_201_CREATED,
)
async def admin_add_cards(
user_id: UUID,
request: CollectionAddRequest,
_admin: AdminAuth,
collection_service: CollectionServiceDep,
) -> CollectionEntryResponse:
"""Add cards to a user's collection (admin endpoint).
This endpoint is for administrative purposes (testing, rewards, etc.).
Requires a valid admin API key in the X-Admin-API-Key header.
Args:
user_id: The target user's UUID.
request: Card add request with card_id, quantity, and source.
Returns:
The created/updated collection entry.
Raises:
400: If card_definition_id is invalid.
403: If admin API key is missing, invalid, or not configured.
"""
try:
entry = await collection_service.add_cards(
user_id=user_id,
card_definition_id=request.card_definition_id,
quantity=request.quantity,
source=request.source,
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from None
return CollectionEntryResponse(
card_definition_id=entry.card_definition_id,
quantity=entry.quantity,
source=entry.source,
obtained_at=entry.obtained_at,
)