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