""" Production settings for thrillwiki project. This module extends base.py with production-specific configurations: - Debug mode disabled - Strict security settings (HSTS, secure cookies, SSL redirect) - Redis caching (required in production) - Structured JSON logging - Production-optimized static file serving """ from decouple import config from .base import * # noqa: F401,F403 # ============================================================================= # Production Core Settings # ============================================================================= DEBUG = False # Allowed hosts must be explicitly set in production ALLOWED_HOSTS = config( "ALLOWED_HOSTS", cast=lambda v: [s.strip() for s in v.split(",") if s.strip()] ) # CSRF trusted origins for production CSRF_TRUSTED_ORIGINS = config( "CSRF_TRUSTED_ORIGINS", cast=lambda v: [s.strip() for s in v.split(",") if s.strip()] ) # ============================================================================= # Security Settings for Production # ============================================================================= # SSL/HTTPS enforcement SECURE_SSL_REDIRECT = True # HSTS (HTTP Strict Transport Security) SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True # Session cookie security (stricter than development) SESSION_COOKIE_SECURE = True # Only send over HTTPS SESSION_COOKIE_SAMESITE = "Strict" # Stricter than Lax for production # CSRF cookie security (stricter than development) CSRF_COOKIE_SECURE = True # Only send over HTTPS CSRF_COOKIE_SAMESITE = "Strict" # Stricter than Lax for production # Additional security headers X_FRAME_OPTIONS = "DENY" # Never allow framing SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin" SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin" # Proxy SSL header (for reverse proxies like nginx) SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # ============================================================================= # Production Cache Configuration (Redis Required) # ============================================================================= redis_url = config("REDIS_URL", default=None) if redis_url: CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": redis_url, "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "PARSER_CLASS": "redis.connection.HiredisParser", "CONNECTION_POOL_CLASS": "redis.BlockingConnectionPool", "CONNECTION_POOL_CLASS_KWARGS": { "max_connections": config( "REDIS_MAX_CONNECTIONS", default=100, cast=int ), "timeout": 20, "socket_keepalive": True, "retry_on_timeout": True, }, "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor", "IGNORE_EXCEPTIONS": False, # Fail loudly in production }, "KEY_PREFIX": "thrillwiki", }, "sessions": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": config("REDIS_SESSIONS_URL", default=redis_url), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "PARSER_CLASS": "redis.connection.HiredisParser", }, "KEY_PREFIX": "sessions", }, "api": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": config("REDIS_API_URL", default=redis_url), "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "PARSER_CLASS": "redis.connection.HiredisParser", "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor", }, "KEY_PREFIX": "api", }, } # Use Redis for sessions in production SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "sessions" # ============================================================================= # Production Static Files Configuration # ============================================================================= STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" # Update STORAGES for Django 4.2+ STORAGES["staticfiles"]["BACKEND"] = ( # noqa: F405 "whitenoise.storage.CompressedManifestStaticFilesStorage" ) # ============================================================================= # Production REST Framework Settings # ============================================================================= # Only JSON renderer in production (no browsable API) REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] = [ # noqa: F405 "rest_framework.renderers.JSONRenderer", ] # ============================================================================= # Production Logging Configuration # ============================================================================= # Structured JSON logging for log aggregation services LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": { "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", "style": "{", }, "json": { "()": "pythonjsonlogger.jsonlogger.JsonFormatter", "format": ( "%(levelname)s %(asctime)s %(module)s %(process)d " "%(thread)d %(message)s %(pathname)s %(lineno)d" ), }, "simple": { "format": "{levelname} {message}", "style": "{", }, }, "filters": { "require_debug_false": { "()": "django.utils.log.RequireDebugFalse", }, }, "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "json", # JSON for container environments }, "file": { "level": "INFO", "class": "logging.handlers.RotatingFileHandler", "filename": BASE_DIR / "logs" / "django.log", # noqa: F405 "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 10, "formatter": "json", }, "error_file": { "level": "ERROR", "class": "logging.handlers.RotatingFileHandler", "filename": BASE_DIR / "logs" / "django_error.log", # noqa: F405 "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 10, "formatter": "json", }, "security_file": { "level": "INFO", "class": "logging.handlers.RotatingFileHandler", "filename": BASE_DIR / "logs" / "security.log", # noqa: F405 "maxBytes": 1024 * 1024 * 10, # 10MB "backupCount": 10, "formatter": "json", }, "mail_admins": { "level": "ERROR", "filters": ["require_debug_false"], "class": "django.utils.log.AdminEmailHandler", "include_html": True, }, }, "root": { "handlers": ["console", "file"], "level": "INFO", }, "loggers": { "django": { "handlers": ["console", "file", "error_file"], "level": "INFO", "propagate": False, }, "django.request": { "handlers": ["console", "error_file", "mail_admins"], "level": "ERROR", "propagate": False, }, "django.security": { "handlers": ["console", "security_file"], "level": "WARNING", "propagate": False, }, "thrillwiki": { "handlers": ["console", "file", "error_file"], "level": "INFO", "propagate": False, }, "security": { "handlers": ["console", "security_file"], "level": "INFO", "propagate": False, }, "celery": { "handlers": ["console", "file"], "level": "INFO", "propagate": False, }, }, } # ============================================================================= # Sentry Integration (Optional) # ============================================================================= # Configure Sentry for error tracking in production SENTRY_DSN = config("SENTRY_DSN", default="") if SENTRY_DSN: import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.redis import RedisIntegration sentry_sdk.init( dsn=SENTRY_DSN, integrations=[ DjangoIntegration(), CeleryIntegration(), RedisIntegration(), ], environment=config("SENTRY_ENVIRONMENT", default="production"), traces_sample_rate=config( "SENTRY_TRACES_SAMPLE_RATE", default=0.1, cast=float ), send_default_pii=False, # Don't send PII to Sentry attach_stacktrace=True, )