Files
thrillwiki_django_no_react/backend/config/django/base.py
pac7 0153af7339 Improve compatibility with user authentication providers
Suppress specific deprecation warnings from dj_rest_auth related to user settings for better compatibility with django-allauth.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 9bc9dd7a-5328-4cb7-91de-b3cb33a0c48c
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
2025-09-22 00:15:15 +00:00

528 lines
18 KiB
Python

"""
Base Django settings for thrillwiki project.
Common settings shared across all environments.
"""
from datetime import timedelta
import sys
import warnings
from pathlib import Path
from decouple import config
# Suppress django-allauth deprecation warnings for dj_rest_auth compatibility
# TODO: Remove this once dj_rest_auth is updated to work with the new ACCOUNT_SIGNUP_FIELDS format
warnings.filterwarnings(
"ignore",
message=r"app_settings\.(USERNAME|EMAIL)_REQUIRED is deprecated",
module="dj_rest_auth.registration.serializers"
)
# Initialize environment variables with better defaults
DEBUG = config("DEBUG", default=True)
SECRET_KEY = config("SECRET_KEY")
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=lambda v: [s.strip() for s in v.split(',') if s.strip()])
DATABASE_URL = config("DATABASE_URL")
CACHE_URL = config("CACHE_URL", default="locmem://")
EMAIL_URL = config("EMAIL_URL", default="console://")
REDIS_URL = config("REDIS_URL", default="redis://127.0.0.1:6379/1")
CORS_ALLOWED_ORIGINS = config("CORS_ALLOWED_ORIGINS", default="", cast=lambda v: [s.strip() for s in v.split(',') if s.strip()])
API_RATE_LIMIT_PER_MINUTE = config("API_RATE_LIMIT_PER_MINUTE", default=60)
API_RATE_LIMIT_PER_HOUR = config("API_RATE_LIMIT_PER_HOUR", default=1000)
CACHE_MIDDLEWARE_SECONDS = config("CACHE_MIDDLEWARE_SECONDS", default=300)
CACHE_MIDDLEWARE_KEY_PREFIX = config(
"CACHE_MIDDLEWARE_KEY_PREFIX", default="thrillwiki"
)
GDAL_LIBRARY_PATH = config(
"GDAL_LIBRARY_PATH", default="/nix/store/c5y314zvvrbr9lx4wh06ibl1b5c07x92-gdal-3.11.0/lib/libgdal.so"
)
GEOS_LIBRARY_PATH = config(
"GEOS_LIBRARY_PATH", default="/nix/store/r5sgxqxrwfvms97v4v239qbivwsmdfjf-geos-3.13.1/lib/libgeos_c.so"
)
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Add apps directory to sys.path so Django can find the apps
apps_dir = BASE_DIR / "apps"
if apps_dir.exists() and str(apps_dir) not in sys.path:
sys.path.insert(0, str(apps_dir))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config("SECRET_KEY")
# Allowed hosts (already configured above)
# CSRF trusted origins
CSRF_TRUSTED_ORIGINS = config(
"CSRF_TRUSTED_ORIGINS", default="", cast=lambda v: [s.strip() for s in v.split(',') if s.strip()]
)
# Application definition
DJANGO_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites",
# "django.contrib.gis", # GeoDjango - Disabled temporarily for setup
]
THIRD_PARTY_APPS = [
# Django Cloudflare Images Toolkit - moved to top to avoid circular imports
"django_cloudflareimages_toolkit",
"rest_framework", # Django REST Framework
# Token authentication (kept for backward compatibility)
"rest_framework.authtoken",
"rest_framework_simplejwt", # JWT authentication
"rest_framework_simplejwt.token_blacklist", # JWT token blacklist
"dj_rest_auth", # REST authentication with JWT support
"dj_rest_auth.registration", # REST registration support
"drf_spectacular", # OpenAPI 3.0 documentation
"corsheaders", # CORS headers for API
"pghistory", # django-pghistory
"pgtrigger", # Required by django-pghistory
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
"allauth.socialaccount.providers.discord",
"django_cleanup",
"django_filters",
"django_htmx",
"whitenoise",
"django_tailwind_cli",
"autocomplete", # Django HTMX Autocomplete
"django_cotton", # Django Cotton for component templates
"health_check", # Health checks
"health_check.db",
"health_check.cache",
"health_check.storage",
"health_check.contrib.migrations",
"health_check.contrib.redis",
"django_celery_beat", # Celery beat scheduler
"django_celery_results", # Celery result backend
"django_extensions", # Django Extensions for enhanced development tools
]
LOCAL_APPS = [
"apps.core",
"apps.accounts",
"apps.parks",
"apps.rides",
"api", # Centralized API app (located at backend/api/)
"django_forwardemail", # New PyPI package for email service
"apps.moderation",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware",
"corsheaders.middleware.CorsMiddleware", # CORS middleware for API
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"apps.core.middleware.analytics.PgHistoryContextMiddleware", # Add history context tracking
"allauth.account.middleware.AccountMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
"django_htmx.middleware.HtmxMiddleware",
]
ROOT_URLCONF = "thrillwiki.urls"
# Add a toggle to enable/disable Django template support via env var
# Use a distinct environment variable name so it doesn't collide with Django's TEMPLATES setting
TEMPLATES_ENABLED = config("TEMPLATES_ENABLED", default=True, cast=bool)
# Conditional TEMPLATES configuration
if TEMPLATES_ENABLED:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"apps.moderation.context_processors.moderation_access",
]
},
}
]
else:
# When templates are disabled, we still need APP_DIRS=True for DRF Spectacular to work
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True, # Changed from False to True to support DRF Spectacular templates
"DIRS": [BASE_DIR / "templates/" / "404"],
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"apps.moderation.context_processors.moderation_access",
]
},
}
]
WSGI_APPLICATION = "thrillwiki.wsgi.application"
# Cloudflare Images Settings - Updated for django-cloudflareimages-toolkit
CLOUDFLARE_IMAGES = {
'ACCOUNT_ID': config("CLOUDFLARE_IMAGES_ACCOUNT_ID"),
'API_TOKEN': config("CLOUDFLARE_IMAGES_API_TOKEN"),
'ACCOUNT_HASH': config("CLOUDFLARE_IMAGES_ACCOUNT_HASH"),
# Optional settings
'DEFAULT_VARIANT': 'public',
'UPLOAD_TIMEOUT': 300,
'WEBHOOK_SECRET': config("CLOUDFLARE_IMAGES_WEBHOOK_SECRET", default=""),
'CLEANUP_EXPIRED_HOURS': 24,
'MAX_FILE_SIZE': 10 * 1024 * 1024, # 10MB
'ALLOWED_FORMATS': ['jpeg', 'png', 'gif', 'webp'],
'REQUIRE_SIGNED_URLS': False,
'DEFAULT_METADATA': {},
}
# Storage configuration
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
"OPTIONS": {
"location": str(BASE_DIR.parent / "shared" / "media"),
},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
"OPTIONS": {
"location": str(BASE_DIR / "staticfiles"),
},
},
}
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": (
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator"
),
},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
LANGUAGE_CODE = "en-us"
TIME_ZONE = "America/New_York"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
# Media files - point to shared/media directory
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR.parent / "shared" / "media"
# Default primary key field type
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Authentication settings
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
]
# django-allauth settings
SITE_ID = 1
# CORRECTED: Django allauth still expects the old format with asterisks for required fields
# The deprecation warnings are from dj_rest_auth, not our configuration
ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"]
ACCOUNT_LOGIN_METHODS = {"email", "username"}
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_CHANGE = True
ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_RESEND = True
ACCOUNT_REAUTHENTICATION_REQUIRED = True
ACCOUNT_EMAIL_NOTIFICATIONS = True
ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False
LOGIN_REDIRECT_URL = "/"
ACCOUNT_LOGOUT_REDIRECT_URL = "/"
# Custom adapters
ACCOUNT_ADAPTER = "apps.accounts.adapters.CustomAccountAdapter"
SOCIALACCOUNT_ADAPTER = "apps.accounts.adapters.CustomSocialAccountAdapter"
# Social account settings
SOCIALACCOUNT_PROVIDERS = {
"google": {
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {"access_type": "online"},
},
"discord": {
"SCOPE": ["identify", "email"],
"OAUTH_PKCE_ENABLED": True,
},
}
# Additional social account settings
SOCIALACCOUNT_LOGIN_ON_GET = True
SOCIALACCOUNT_AUTO_SIGNUP = False
SOCIALACCOUNT_STORE_TOKENS = True
# Custom User Model
AUTH_USER_MODEL = "accounts.User"
# Autocomplete configuration
AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = False
# Tailwind configuration
TAILWIND_CLI_CONFIG_FILE = "tailwind.config.js"
TAILWIND_CLI_SRC_CSS = "static/css/src/input.css"
TAILWIND_CLI_DIST_CSS = "css/tailwind.css"
# Test runner
TEST_RUNNER = "django.test.runner.DiscoverRunner"
# Road Trip Service Settings
ROADTRIP_CACHE_TIMEOUT = 3600 * 24 # 24 hours for geocoding
ROADTRIP_ROUTE_CACHE_TIMEOUT = 3600 * 6 # 6 hours for routes
ROADTRIP_MAX_REQUESTS_PER_SECOND = 1 # Respect OSM rate limits
ROADTRIP_USER_AGENT = config("ROADTRIP_USER_AGENT")
ROADTRIP_REQUEST_TIMEOUT = 10 # seconds
ROADTRIP_MAX_RETRIES = 3
ROADTRIP_BACKOFF_FACTOR = 2
# Frontend URL Configuration
FRONTEND_DOMAIN = config("FRONTEND_DOMAIN", default="https://thrillwiki.com")
# ForwardEmail Configuration
FORWARD_EMAIL_BASE_URL = config(
"FORWARD_EMAIL_BASE_URL", default="https://api.forwardemail.net")
FORWARD_EMAIL_API_KEY = config("FORWARD_EMAIL_API_KEY", default="")
FORWARD_EMAIL_DOMAIN = config("FORWARD_EMAIL_DOMAIN", default="")
# Django REST Framework Settings
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication", # Kept for backward compatibility
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning",
"DEFAULT_VERSION": "v1",
"ALLOWED_VERSIONS": ["v1"],
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
],
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
"rest_framework.parsers.FormParser",
"rest_framework.parsers.MultiPartParser",
],
"EXCEPTION_HANDLER": "apps.core.api.exceptions.custom_exception_handler",
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.SearchFilter",
"rest_framework.filters.OrderingFilter",
],
"TEST_REQUEST_DEFAULT_FORMAT": "json",
"NON_FIELD_ERRORS_KEY": "non_field_errors",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
# CORS Settings for API
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = config(
"CORS_ALLOW_ALL_ORIGINS", default=False, cast=bool
) # type: ignore[arg-type]
API_RATE_LIMIT_PER_MINUTE = config(
"API_RATE_LIMIT_PER_MINUTE", default=60, cast=int
) # type: ignore[arg-type]
API_RATE_LIMIT_PER_HOUR = config(
"API_RATE_LIMIT_PER_HOUR", default=1000, cast=int
) # type: ignore[arg-type]
SPECTACULAR_SETTINGS = {
"TITLE": "ThrillWiki API",
"DESCRIPTION": "Comprehensive theme park and ride information API",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"COMPONENT_SPLIT_REQUEST": True,
"TAGS": [
{"name": "Parks", "description": "Theme park operations"},
{"name": "Rides", "description": "Ride information and management"},
{
"name": "Statistics",
"description": "Statistical endpoints providing aggregated data and insights",
},
],
"SCHEMA_PATH_PREFIX": "/api/",
"DEFAULT_GENERATOR_CLASS": "drf_spectacular.generators.SchemaGenerator",
"DEFAULT_AUTO_SCHEMA": "drf_spectacular.openapi.AutoSchema",
"PREPROCESSING_HOOKS": [
"api.v1.schema.custom_preprocessing_hook",
],
# "POSTPROCESSING_HOOKS": [
# "api.v1.schema.custom_postprocessing_hook",
# ],
"SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
"SWAGGER_UI_SETTINGS": {
"deepLinking": True,
"persistAuthorization": True,
"displayOperationId": False,
"displayRequestDuration": True,
},
"REDOC_UI_SETTINGS": {
"hideDownloadButton": False,
"hideHostname": False,
"hideLoading": False,
"hideSchemaPattern": True,
"scrollYOffset": 0,
"theme": {"colors": {"primary": {"main": "#1976d2"}}},
},
}
# Health Check Configuration
HEALTH_CHECK = {
"DISK_USAGE_MAX": 90, # Fail if disk usage is over 90%
"MEMORY_MIN": 100, # Fail if less than 100MB available memory
}
# Custom health check backends
HEALTH_CHECK_BACKENDS = [
"health_check.db",
"health_check.cache",
"health_check.storage",
"core.health_checks.custom_checks.CacheHealthCheck",
"core.health_checks.custom_checks.DatabasePerformanceCheck",
"core.health_checks.custom_checks.ApplicationHealthCheck",
"core.health_checks.custom_checks.ExternalServiceHealthCheck",
"core.health_checks.custom_checks.DiskSpaceHealthCheck",
]
# Enhanced Cache Configuration
DJANGO_REDIS_CACHE_BACKEND = "django_redis.cache.RedisCache"
DJANGO_REDIS_CLIENT_CLASS = "django_redis.client.DefaultClient"
CACHES = {
"default": {
"BACKEND": DJANGO_REDIS_CACHE_BACKEND,
# pyright: ignore[reportArgumentType]
# type: ignore
"LOCATION": config("REDIS_URL", default="redis://127.0.0.1:6379/1"),
"OPTIONS": {
"CLIENT_CLASS": DJANGO_REDIS_CLIENT_CLASS,
"PARSER_CLASS": "redis.connection.HiredisParser",
"CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool",
"CONNECTION_POOL_CLASS_KWARGS": {
"max_connections": 50,
"timeout": 20,
},
"COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
"IGNORE_EXCEPTIONS": True,
},
"KEY_PREFIX": "thrillwiki",
"VERSION": 1,
},
"sessions": {
"BACKEND": DJANGO_REDIS_CACHE_BACKEND,
"LOCATION": config("REDIS_URL", default="redis://127.0.0.1:6379/2"),
"OPTIONS": {
"CLIENT_CLASS": DJANGO_REDIS_CLIENT_CLASS,
},
},
"api": {
"BACKEND": DJANGO_REDIS_CACHE_BACKEND,
"LOCATION": config("REDIS_URL", default="redis://127.0.0.1:6379/3"),
"OPTIONS": {
"CLIENT_CLASS": DJANGO_REDIS_CLIENT_CLASS,
},
},
}
# Use Redis for sessions
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "sessions"
SESSION_COOKIE_AGE = 86400 # 24 hours
# Cache middleware settings
CACHE_MIDDLEWARE_SECONDS = 300 # 5 minutes
CACHE_MIDDLEWARE_KEY_PREFIX = "thrillwiki"
# JWT Settings
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), # 1 hour
"REFRESH_TOKEN_LIFETIME": timedelta(days=7), # 7 days
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": True,
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"VERIFYING_KEY": None,
"AUDIENCE": None,
"ISSUER": None,
"JWK_URL": None,
"LEEWAY": 0,
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=60),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
# dj-rest-auth settings
REST_AUTH = {
"USE_JWT": True,
"JWT_AUTH_COOKIE": "thrillwiki-auth",
"JWT_AUTH_REFRESH_COOKIE": "thrillwiki-refresh",
"JWT_AUTH_SECURE": not DEBUG, # Use secure cookies in production
"JWT_AUTH_HTTPONLY": True,
"JWT_AUTH_SAMESITE": "Lax",
"JWT_AUTH_RETURN_EXPIRATION": True,
"JWT_TOKEN_CLAIMS_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
}