feat: migrate to Pydantic V2 and implement rate limiting middleware
- Migrate settings.py to Pydantic V2 (SettingsConfigDict, validation_alias) - Update config models to use @field_validator with @classmethod - Replace deprecated datetime.utcnow() with datetime.now(timezone.utc) - Migrate FastAPI app from @app.on_event to lifespan context manager - Implement comprehensive rate limiting middleware with: * Endpoint-specific rate limits (login: 5/min, register: 3/min) * IP-based and user-based tracking * Authenticated user multiplier (2x limits) * Bypass paths for health, docs, static, websocket endpoints * Rate limit headers in responses - Add 13 comprehensive tests for rate limiting (all passing) - Update instructions.md to mark completed tasks - Fix asyncio.create_task usage in anime_service.py All 714 tests passing. No deprecation warnings.
This commit is contained in:
@@ -2,18 +2,25 @@ import secrets
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings from environment variables."""
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
|
||||
jwt_secret_key: str = Field(
|
||||
default_factory=lambda: secrets.token_urlsafe(32),
|
||||
env="JWT_SECRET_KEY",
|
||||
validation_alias="JWT_SECRET_KEY",
|
||||
)
|
||||
password_salt: str = Field(
|
||||
default="default-salt",
|
||||
validation_alias="PASSWORD_SALT"
|
||||
)
|
||||
password_salt: str = Field(default="default-salt", env="PASSWORD_SALT")
|
||||
master_password_hash: Optional[str] = Field(
|
||||
default=None, env="MASTER_PASSWORD_HASH"
|
||||
default=None,
|
||||
validation_alias="MASTER_PASSWORD_HASH"
|
||||
)
|
||||
# ⚠️ WARNING: DEVELOPMENT ONLY - NEVER USE IN PRODUCTION ⚠️
|
||||
# This field allows setting a plaintext master password via environment
|
||||
@@ -21,32 +28,50 @@ class Settings(BaseSettings):
|
||||
# deployments, use MASTER_PASSWORD_HASH instead and NEVER set this field.
|
||||
master_password: Optional[str] = Field(
|
||||
default=None,
|
||||
env="MASTER_PASSWORD",
|
||||
validation_alias="MASTER_PASSWORD",
|
||||
description=(
|
||||
"**DEVELOPMENT ONLY** - Plaintext master password. "
|
||||
"NEVER enable in production. Use MASTER_PASSWORD_HASH instead."
|
||||
),
|
||||
)
|
||||
token_expiry_hours: int = Field(
|
||||
default=24, env="SESSION_TIMEOUT_HOURS"
|
||||
default=24,
|
||||
validation_alias="SESSION_TIMEOUT_HOURS"
|
||||
)
|
||||
anime_directory: str = Field(
|
||||
default="",
|
||||
validation_alias="ANIME_DIRECTORY"
|
||||
)
|
||||
log_level: str = Field(
|
||||
default="INFO",
|
||||
validation_alias="LOG_LEVEL"
|
||||
)
|
||||
anime_directory: str = Field(default="", env="ANIME_DIRECTORY")
|
||||
log_level: str = Field(default="INFO", env="LOG_LEVEL")
|
||||
|
||||
# Additional settings from .env
|
||||
database_url: str = Field(
|
||||
default="sqlite:///./data/aniworld.db", env="DATABASE_URL"
|
||||
default="sqlite:///./data/aniworld.db",
|
||||
validation_alias="DATABASE_URL"
|
||||
)
|
||||
cors_origins: str = Field(
|
||||
default="http://localhost:3000",
|
||||
env="CORS_ORIGINS",
|
||||
validation_alias="CORS_ORIGINS",
|
||||
)
|
||||
api_rate_limit: int = Field(
|
||||
default=100,
|
||||
validation_alias="API_RATE_LIMIT"
|
||||
)
|
||||
api_rate_limit: int = Field(default=100, env="API_RATE_LIMIT")
|
||||
default_provider: str = Field(
|
||||
default="aniworld.to", env="DEFAULT_PROVIDER"
|
||||
default="aniworld.to",
|
||||
validation_alias="DEFAULT_PROVIDER"
|
||||
)
|
||||
provider_timeout: int = Field(
|
||||
default=30,
|
||||
validation_alias="PROVIDER_TIMEOUT"
|
||||
)
|
||||
retry_attempts: int = Field(
|
||||
default=3,
|
||||
validation_alias="RETRY_ATTEMPTS"
|
||||
)
|
||||
provider_timeout: int = Field(default=30, env="PROVIDER_TIMEOUT")
|
||||
retry_attempts: int = Field(default=3, env="RETRY_ATTEMPTS")
|
||||
|
||||
@property
|
||||
def allowed_origins(self) -> list[str]:
|
||||
@@ -67,9 +92,5 @@ class Settings(BaseSettings):
|
||||
]
|
||||
return [origin.strip() for origin in raw.split(",") if origin.strip()]
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
extra = "ignore"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
Reference in New Issue
Block a user