Add secret management guide, client-side performance monitoring, and search accessibility enhancements

- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
pacnpal
2025-12-23 16:41:42 -05:00
parent ae31e889d7
commit edcd8f2076
155 changed files with 22046 additions and 4645 deletions

View File

@@ -1,36 +1,25 @@
"""
Base Django settings for thrillwiki project.
Common settings shared across all environments.
This file contains only core Django settings that are common across all
environments. Environment-specific settings are in local.py, production.py,
and test.py. Modular configuration is imported from config/settings/.
Structure:
- Core settings (SECRET_KEY, DEBUG, ALLOWED_HOSTS)
- Application definition (INSTALLED_APPS, MIDDLEWARE)
- URL and template configuration
- Internationalization
- Imports from modular settings (database, cache, security, etc.)
"""
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"
)
# =============================================================================
# Path Configuration
# =============================================================================
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
@@ -40,17 +29,34 @@ apps_dir = BASE_DIR / "apps"
if apps_dir.exists() and str(apps_dir) not in sys.path:
sys.path.insert(0, str(apps_dir))
# =============================================================================
# Core Settings
# =============================================================================
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config("SECRET_KEY")
# Allowed hosts (already configured above)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config("DEBUG", default=True, cast=bool)
# CSRF trusted origins
CSRF_TRUSTED_ORIGINS = config(
"CSRF_TRUSTED_ORIGINS", default="", cast=lambda v: [s.strip() for s in v.split(',') if s.strip()]
# Allowed hosts (comma-separated in .env)
ALLOWED_HOSTS = config(
"ALLOWED_HOSTS",
default="localhost,127.0.0.1",
cast=lambda v: [s.strip() for s in v.split(",") if s.strip()]
)
# Application definition
# CSRF trusted origins (comma-separated in .env)
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",
@@ -111,32 +117,46 @@ LOCAL_APPS = [
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# =============================================================================
# Middleware Configuration
# =============================================================================
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.cache.UpdateCacheMiddleware", # Must be first for cache middleware
"django.middleware.gzip.GZipMiddleware", # Response compression
"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
"apps.core.middleware.security_headers.SecurityHeadersMiddleware", # Custom security headers
"apps.core.middleware.rate_limiting.AuthRateLimitMiddleware", # Rate limiting
"whitenoise.middleware.WhiteNoiseMiddleware",
"apps.core.middleware.performance_middleware.PerformanceMiddleware", # Performance monitoring
"apps.core.middleware.performance_middleware.QueryCountMiddleware", # Database query monitoring
"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
"apps.core.middleware.analytics.PgHistoryContextMiddleware", # History context tracking
"allauth.account.middleware.AccountMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
"django_htmx.middleware.HtmxMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware", # Must be last for cache middleware
]
ROOT_URLCONF = "thrillwiki.urls"
# =============================================================================
# URL Configuration
# =============================================================================
# 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
ROOT_URLCONF = "thrillwiki.urls"
WSGI_APPLICATION = "thrillwiki.wsgi.application"
# =============================================================================
# Template Configuration
# =============================================================================
# Toggle to enable/disable Django template support via env var
TEMPLATES_ENABLED = config("TEMPLATES_ENABLED", default=True, cast=bool)
# Conditional TEMPLATES configuration
if TEMPLATES_ENABLED:
TEMPLATES = [
{
@@ -158,11 +178,11 @@ if TEMPLATES_ENABLED:
}
]
else:
# When templates are disabled, we still need APP_DIRS=True for DRF Spectacular to work
# When templates are disabled, still need APP_DIRS=True for DRF Spectacular
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True, # Changed from False to True to support DRF Spectacular templates
"APP_DIRS": True,
"DIRS": [BASE_DIR / "templates/" / "404"],
"OPTIONS": {
"context_processors": [
@@ -179,462 +199,82 @@ else:
}
]
WSGI_APPLICATION = "thrillwiki.wsgi.application"
# =============================================================================
# Custom User Model
# =============================================================================
# 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",
},
]
AUTH_USER_MODEL = "accounts.User"
# =============================================================================
# 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"
# =============================================================================
# Default Primary Key
# =============================================================================
# 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",
]
# =============================================================================
# Test Runner
# =============================================================================
# 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
# Import Modular 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
# Import settings from modular configuration files.
# These imports add/override settings defined above.
# Cache middleware settings
CACHE_MIDDLEWARE_SECONDS = 300 # 5 minutes
CACHE_MIDDLEWARE_KEY_PREFIX = "thrillwiki"
# Database configuration (DATABASES, GDAL_LIBRARY_PATH, GEOS_LIBRARY_PATH)
from config.settings.database import * # noqa: F401,F403,E402
# Cache configuration (CACHES, SESSION_*, CACHE_MIDDLEWARE_*)
from config.settings.cache import * # noqa: F401,F403,E402
# Security configuration (SECURE_*, CSRF_*, SESSION_COOKIE_*, AUTH_PASSWORD_VALIDATORS)
from config.settings.security import * # noqa: F401,F403,E402
# Email configuration (EMAIL_*, FORWARD_EMAIL_*)
from config.settings.email import * # noqa: F401,F403,E402
# Logging configuration (LOGGING)
from config.settings.logging import * # noqa: F401,F403,E402
# REST Framework configuration (REST_FRAMEWORK, CORS_*, SIMPLE_JWT, REST_AUTH, SPECTACULAR_SETTINGS)
from config.settings.rest_framework import * # noqa: F401,F403,E402
# Third-party configuration (ACCOUNT_*, SOCIALACCOUNT_*, CLOUDFLARE_IMAGES, etc.)
from config.settings.third_party import * # noqa: F401,F403,E402
# Storage configuration (STATIC_*, MEDIA_*, STORAGES, WHITENOISE_*, FILE_UPLOAD_*)
from config.settings.storage import * # noqa: F401,F403,E402
# =============================================================================
# JWT Settings
# Post-Import Overrides
# =============================================================================
# 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
# Settings that need to reference values from imported modules
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),
}
# Update SimpleJWT to use the SECRET_KEY
SIMPLE_JWT["SIGNING_KEY"] = SECRET_KEY # noqa: F405
# =============================================================================
# dj-rest-auth Settings
# Startup Validation
# =============================================================================
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",
}
# Run configuration validation after all settings are loaded.
# These validations catch configuration errors early during Django startup.
from config.settings.secrets import run_startup_validation as validate_secrets # noqa: E402
from config.settings.validation import run_startup_validation as validate_config # noqa: E402
# Run secret validation (fails fast in production, warns in development)
validate_secrets()
# Run configuration validation (fails fast in production, warns in development)
validate_config()