- 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.
97 lines
2.9 KiB
Python
97 lines
2.9 KiB
Python
import secrets
|
|
from typing import Optional
|
|
|
|
from pydantic import Field
|
|
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),
|
|
validation_alias="JWT_SECRET_KEY",
|
|
)
|
|
password_salt: str = Field(
|
|
default="default-salt",
|
|
validation_alias="PASSWORD_SALT"
|
|
)
|
|
master_password_hash: Optional[str] = Field(
|
|
default=None,
|
|
validation_alias="MASTER_PASSWORD_HASH"
|
|
)
|
|
# ⚠️ WARNING: DEVELOPMENT ONLY - NEVER USE IN PRODUCTION ⚠️
|
|
# This field allows setting a plaintext master password via environment
|
|
# variable for development/testing purposes only. In production
|
|
# deployments, use MASTER_PASSWORD_HASH instead and NEVER set this field.
|
|
master_password: Optional[str] = Field(
|
|
default=None,
|
|
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,
|
|
validation_alias="SESSION_TIMEOUT_HOURS"
|
|
)
|
|
anime_directory: str = Field(
|
|
default="",
|
|
validation_alias="ANIME_DIRECTORY"
|
|
)
|
|
log_level: str = Field(
|
|
default="INFO",
|
|
validation_alias="LOG_LEVEL"
|
|
)
|
|
|
|
# Additional settings from .env
|
|
database_url: str = Field(
|
|
default="sqlite:///./data/aniworld.db",
|
|
validation_alias="DATABASE_URL"
|
|
)
|
|
cors_origins: str = Field(
|
|
default="http://localhost:3000",
|
|
validation_alias="CORS_ORIGINS",
|
|
)
|
|
api_rate_limit: int = Field(
|
|
default=100,
|
|
validation_alias="API_RATE_LIMIT"
|
|
)
|
|
default_provider: str = Field(
|
|
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"
|
|
)
|
|
|
|
@property
|
|
def allowed_origins(self) -> list[str]:
|
|
"""Return the list of allowed CORS origins.
|
|
|
|
The environment variable should contain a comma-separated list.
|
|
When ``*`` is provided we fall back to a safe local development
|
|
default instead of allowing every origin in production.
|
|
"""
|
|
|
|
raw = (self.cors_origins or "").strip()
|
|
if not raw:
|
|
return []
|
|
if raw == "*":
|
|
return [
|
|
"http://localhost:3000",
|
|
"http://localhost:8000",
|
|
]
|
|
return [origin.strip() for origin in raw.split(",") if origin.strip()]
|
|
|
|
|
|
settings = Settings()
|