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>
137 lines
3.9 KiB
Python
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,
|
|
)
|