mantimon-tcg/backend/app/schemas/user.py
Cal Corum 996c43fbd9 Implement Phase 2: Authentication system
Complete OAuth-based authentication with JWT session management:

Core Services:
- JWT service for access/refresh token creation and verification
- Token store with Redis-backed refresh token revocation
- User service for CRUD operations and OAuth-based creation
- Google and Discord OAuth services with full flow support

API Endpoints:
- GET /api/auth/{google,discord} - Start OAuth flows
- GET /api/auth/{google,discord}/callback - Handle OAuth callbacks
- POST /api/auth/refresh - Exchange refresh token for new access token
- POST /api/auth/logout - Revoke single refresh token
- POST /api/auth/logout-all - Revoke all user sessions
- GET/PATCH /api/users/me - User profile management
- GET /api/users/me/linked-accounts - List OAuth providers
- GET /api/users/me/sessions - Count active sessions

Infrastructure:
- Pydantic schemas for auth/user request/response models
- FastAPI dependencies (get_current_user, get_current_premium_user)
- OAuthLinkedAccount model for multi-provider support
- Alembic migration for oauth_linked_accounts table

Dependencies added: email-validator, fakeredis (dev), respx (dev)

84 new tests, 1058 total passing
2026-01-27 21:49:59 -06:00

115 lines
3.9 KiB
Python

"""User schemas for Mantimon TCG.
This module defines Pydantic models for user-related API requests
and responses.
Example:
user_response = UserResponse(
id="550e8400-e29b-41d4-a716-446655440000",
email="player@example.com",
display_name="Player1",
is_premium=False,
created_at=datetime.now(UTC)
)
"""
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, EmailStr, Field
class UserResponse(BaseModel):
"""Public user information returned by API endpoints.
Attributes:
id: User's unique identifier.
email: User's email address.
display_name: User's public display name.
avatar_url: URL to user's avatar image.
is_premium: Whether user has active premium subscription.
premium_until: When premium subscription expires (if premium).
created_at: When the account was created.
"""
id: UUID = Field(..., description="User ID")
email: EmailStr = Field(..., description="User's email address")
display_name: str = Field(..., description="Public display name")
avatar_url: str | None = Field(default=None, description="Avatar image URL")
is_premium: bool = Field(default=False, description="Premium subscription status")
premium_until: datetime | None = Field(default=None, description="Premium expiration date")
created_at: datetime = Field(..., description="Account creation date")
model_config = {"from_attributes": True}
class UserCreate(BaseModel):
"""Internal schema for creating a user from OAuth data.
Not exposed via API - used internally by auth service.
Attributes:
email: User's email from OAuth provider.
display_name: User's name from OAuth provider.
avatar_url: Avatar URL from OAuth provider.
oauth_provider: OAuth provider name (google, discord).
oauth_id: Unique ID from the OAuth provider.
"""
email: EmailStr = Field(..., description="Email from OAuth provider")
display_name: str = Field(..., max_length=50, description="Display name")
avatar_url: str | None = Field(default=None, description="Avatar URL")
oauth_provider: str = Field(..., description="OAuth provider (google, discord)")
oauth_id: str = Field(..., description="Unique ID from OAuth provider")
class UserUpdate(BaseModel):
"""Schema for updating user profile.
All fields are optional - only provided fields are updated.
Attributes:
display_name: New display name.
avatar_url: New avatar URL.
"""
display_name: str | None = Field(
default=None, min_length=1, max_length=50, description="New display name"
)
avatar_url: str | None = Field(default=None, description="New avatar URL")
class OAuthUserInfo(BaseModel):
"""Normalized user information from OAuth providers.
This provides a consistent structure regardless of whether
the user authenticated via Google or Discord.
Attributes:
provider: OAuth provider name.
oauth_id: Unique ID from the provider.
email: User's email address.
name: User's display name from provider.
avatar_url: Avatar URL from provider.
"""
provider: str = Field(..., description="OAuth provider (google, discord)")
oauth_id: str = Field(..., description="Unique ID from provider")
email: EmailStr = Field(..., description="User's email")
name: str = Field(..., description="User's name from provider")
avatar_url: str | None = Field(default=None, description="Avatar URL from provider")
def to_user_create(self) -> UserCreate:
"""Convert OAuth info to UserCreate schema.
Returns:
UserCreate instance ready for user creation.
"""
return UserCreate(
email=self.email,
display_name=self.name[:50], # Enforce max length
avatar_url=self.avatar_url,
oauth_provider=self.provider,
oauth_id=self.oauth_id,
)