Initial commit of mtgonline project

This commit is contained in:
2026-07-18 04:57:40 +00:00
commit 86c12376f8
1870 changed files with 547994 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
"""
Database engine and session management.
Provides async SQLAlchemy engine and session factory for dependency injection.
"""
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
from app.core.settings import get_settings
settings = get_settings()
engine = create_async_engine(
settings.DATABASE_URL,
echo=settings.DEBUG,
pool_pre_ping=True,
pool_size=20,
max_overflow=10,
)
async_session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
class Base(DeclarativeBase):
"""Base class for all ORM models."""
pass
async def get_db() -> AsyncSession:
"""FastAPI dependency that provides a database session."""
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
+85
View File
@@ -0,0 +1,85 @@
"""
Security utilities for authentication and password hashing.
Implements JWT token management and bcrypt password hashing with salt.
"""
from datetime import datetime, timedelta, timezone
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.settings import get_settings
settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a plain password against a bcrypt hash."""
return pwd_context.verify(plain_password, hashed_password)
def hash_password(password: str) -> str:
"""Hash a password using bcrypt with configurable rounds."""
return pwd_context.hash(password, rounds=settings.BCRYPT_ROUNDS)
def create_access_token(
subject: str,
expires_delta: Optional[timedelta] = None,
) -> str:
"""Create a JWT access token."""
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
)
payload = {
"sub": subject,
"exp": expire,
"iat": datetime.now(timezone.utc),
"type": "access",
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
def create_refresh_token(subject: str) -> str:
"""Create a JWT refresh token with longer expiry."""
expire = datetime.now(timezone.utc) + timedelta(
days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
)
payload = {
"sub": subject,
"exp": expire,
"iat": datetime.now(timezone.utc),
"type": "refresh",
}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
def decode_token(token: str) -> Optional[dict]:
"""Decode and validate a JWT token."""
try:
payload = jwt.decode(
token,
settings.SECRET_KEY,
algorithms=[settings.JWT_ALGORITHM],
)
return payload
except JWTError:
return None
def get_current_user(token: str) -> Optional[dict]:
"""Extract user info from JWT token."""
payload = decode_token(token)
if not payload:
return None
if payload.get("type") != "access":
return None
return {
"user_id": payload.get("sub"),
"token_type": payload.get("type"),
}
+54
View File
@@ -0,0 +1,54 @@
"""
Application configuration management.
Uses pydantic-settings for typed configuration with environment variable overrides.
"""
from pydantic_settings import BaseSettings
from typing import Optional
from functools import lru_cache
class Settings(BaseSettings):
"""Application settings loaded from environment variables or .env file."""
# Application
APP_NAME: str = "Cockatrice Web"
APP_VERSION: str = "0.1.0"
DEBUG: bool = False
SECRET_KEY: str = "change-me-in-production"
# Database
DATABASE_URL: str = "postgresql+asyncpg://cockatrice:cockatrice@localhost:5432/cockatrice"
# Redis
REDIS_URL: str = "redis://localhost:6379/0"
# JWT Configuration
JWT_ALGORITHM: str = "HS256"
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7
# CORS
CORS_ORIGINS: list[str] = ["http://localhost:3000", "http://localhost:8080"]
# Email (for password reset, account activation)
SMTP_HOST: Optional[str] = None
SMTP_PORT: int = 587
SMTP_USER: Optional[str] = None
SMTP_PASSWORD: Optional[str] = None
EMAIL_FROM: Optional[str] = None
# Security
BCRYPT_ROUNDS: int = 12
MAX_LOGIN_ATTEMPTS: int = 5
LOGIN_BLOCK_MINUTES: int = 15
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
@lru_cache()
def get_settings() -> Settings:
"""Get cached application settings."""
return Settings()