Files
thrillwiki_django_no_react/backend/config/django/base.py

641 lines
23 KiB
Python

"""
Base Django settings for thrillwiki project.
Common settings shared across all environments.
"""
from datetime import timedelta
import sys
from pathlib import Path
from decouple import config
# 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="/opt/homebrew/lib/libgdal.dylib"
)
GEOS_LIBRARY_PATH = config(
"GEOS_LIBRARY_PATH", default="/opt/homebrew/lib/libgeos_c.dylib"
)
# 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
]
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
"django_fsm_log", # FSM transition logging
"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
"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",
"apps.core.middleware.security_headers.SecurityHeadersMiddleware", # Custom security headers (CSP, Permissions-Policy)
"apps.core.middleware.rate_limiting.AuthRateLimitMiddleware", # Rate limiting for auth endpoints
"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",
"apps.core.context_processors.fsm_context",
"apps.core.context_processors.breadcrumbs",
"apps.core.context_processors.page_meta",
]
},
}
]
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",
"apps.core.context_processors.fsm_context",
"apps.core.context_processors.breadcrumbs",
"apps.core.context_processors.page_meta",
]
},
}
]
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,
"MAX_PAGE_SIZE": 100,
"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",
],
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "60/minute",
"user": "1000/hour",
},
"TEST_REQUEST_DEFAULT_FORMAT": "json",
"NON_FIELD_ERRORS_KEY": "non_field_errors",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
# CORS Settings for API
# https://github.com/adamchainz/django-cors-headers
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = config(
"CORS_ALLOW_ALL_ORIGINS", default=False, cast=bool
) # type: ignore[arg-type]
# Allowed HTTP headers for CORS requests
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
"x-api-version",
]
# HTTP methods allowed for CORS requests
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
# Expose rate limit headers to browsers
CORS_EXPOSE_HEADERS = [
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-RateLimit-Reset",
"X-API-Version",
]
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.
## API Conventions
### Response Format
All successful responses include a `success: true` field with data nested under `data`.
All error responses include an `error` object with `code` and `message` fields.
### Pagination
List endpoints support pagination with `page` and `page_size` parameters.
Default page size is 20, maximum is 100.
### Filtering
Range filters use `{field}_min` and `{field}_max` naming convention.
Search uses the `search` parameter.
Ordering uses the `ordering` parameter (prefix with `-` for descending).
### Field Naming
All field names use snake_case convention (e.g., `image_url`, `created_at`).
""",
"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": "Park Media", "description": "Park photos and media management"},
{"name": "Ride Media", "description": "Ride photos and media management"},
{"name": "Authentication", "description": "User authentication and session management"},
{"name": "Social Authentication", "description": "Social provider login and account linking"},
{"name": "User Profile", "description": "User profile management"},
{"name": "User Settings", "description": "User preferences and settings"},
{"name": "User Notifications", "description": "User notification management"},
{"name": "User Content", "description": "User-generated content (top lists, reviews)"},
{"name": "User Management", "description": "Admin user management operations"},
{"name": "Self-Service Account Management", "description": "User account deletion and management"},
{"name": "Core", "description": "Core utility endpoints (search, suggestions)"},
{
"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 Security Settings
# =============================================================================
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "sessions"
SESSION_COOKIE_AGE = 3600 # 1 hour (reduced from 24 hours for security)
SESSION_SAVE_EVERY_REQUEST = True # Update session on each request (sliding expiry)
SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access to session cookie
SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Session persists until cookie expires
# Cache middleware settings
CACHE_MIDDLEWARE_SECONDS = 300 # 5 minutes
CACHE_MIDDLEWARE_KEY_PREFIX = "thrillwiki"
# =============================================================================
# JWT Settings
# =============================================================================
# Security considerations:
# - Short access token lifetime reduces window of vulnerability
# - Refresh token rotation prevents token reuse after refresh
# - Token blacklisting allows revocation of compromised tokens
# - JTI claim enables unique token identification for logging
SIMPLE_JWT = {
# Token lifetimes
# Security: Shorter access tokens (15 min) provide better security
# but may require more frequent refreshes
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15), # 15 minutes (reduced from 60)
"REFRESH_TOKEN_LIFETIME": timedelta(days=7), # 7 days
# Token rotation and blacklisting
# Security: Rotate refresh tokens on each use and blacklist old ones
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
# Update last login on token refresh
"UPDATE_LAST_LOGIN": True,
# Cryptographic settings
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"VERIFYING_KEY": None,
# Token validation
"AUDIENCE": None,
"ISSUER": "thrillwiki", # Added issuer for token validation
"JWK_URL": None,
"LEEWAY": 0, # No leeway for token expiration
# Authentication header
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
# User identification
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
# Token classes
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
# JTI claim for unique token identification
# Security: Enables token tracking and revocation
"JTI_CLAIM": "jti",
# Sliding token settings (if using sliding tokens)
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=15),
"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",
# Security: Only send cookies over HTTPS in production
"JWT_AUTH_SECURE": not DEBUG,
# Security: Prevent JavaScript access to cookies
"JWT_AUTH_HTTPONLY": True,
# Security: SameSite cookie attribute (Lax is compatible with OAuth flows)
"JWT_AUTH_SAMESITE": "Lax",
"JWT_AUTH_RETURN_EXPIRATION": True,
"JWT_TOKEN_CLAIMS_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
}