""" Django base settings for ThrillWiki project. These settings are common across all environments. """ from pathlib import Path import environ # Build paths BASE_DIR = Path(__file__).resolve().parent.parent.parent # Initialize environment variables env = environ.Env( DEBUG=(bool, False), ALLOWED_HOSTS=(list, []), ) # Read .env file if it exists environ.Env.read_env(BASE_DIR / '.env') # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env('SECRET_KEY', default='django-insecure-change-this-in-production') # Application definition INSTALLED_APPS = [ # Django apps 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third-party apps 'rest_framework', 'rest_framework_simplejwt', 'ninja', 'django_filters', 'corsheaders', 'guardian', 'django_otp', 'django_otp.plugins.otp_totp', 'allauth', 'allauth.account', 'allauth.socialaccount', 'allauth.socialaccount.providers.google', 'allauth.socialaccount.providers.discord', 'django_celery_beat', 'django_celery_results', 'django_extensions', 'channels', 'storages', 'defender', # Local apps 'apps.core', 'apps.users', 'apps.entities', 'apps.moderation', 'apps.versioning', 'apps.media', 'apps.notifications', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django_otp.middleware.OTPMiddleware', 'allauth.account.middleware.AccountMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'defender.middleware.FailedLoginMiddleware', ] ROOT_URLCONF = 'config.urls' 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', ], }, }, ] WSGI_APPLICATION = 'config.wsgi.application' ASGI_APPLICATION = 'config.asgi.application' # Database DATABASES = { 'default': env.db('DATABASE_URL', default='postgresql://localhost/thrillwiki') } # 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 = 'UTC' USE_I18N = True USE_TZ = True # Static files STATIC_URL = 'static/' STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [BASE_DIR / 'static'] # Media files MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'media' # Default primary key field type DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Custom User Model AUTH_USER_MODEL = 'users.User' # Authentication Backends AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', 'allauth.account.auth_backends.AuthenticationBackend', 'guardian.backends.ObjectPermissionBackend', ] # Django REST Framework REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticatedOrReadOnly', ], 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 50, } # JWT Settings from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, 'ALGORITHM': 'HS256', 'SIGNING_KEY': SECRET_KEY, 'AUTH_HEADER_TYPES': ('Bearer',), } # CORS Settings CORS_ALLOWED_ORIGINS = env.list( 'CORS_ALLOWED_ORIGINS', default=['http://localhost:5173', 'http://localhost:3000'] ) CORS_ALLOW_CREDENTIALS = True # Redis Configuration REDIS_URL = env('REDIS_URL', default='redis://localhost:6379/0') # Caching CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': REDIS_URL, 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 'PARSER_CLASS': 'redis.connection.HiredisParser', }, 'KEY_PREFIX': 'thrillwiki', 'TIMEOUT': 300, } } # Session Configuration SESSION_ENGINE = 'django.contrib.sessions.backends.cache' SESSION_CACHE_ALIAS = 'default' SESSION_COOKIE_AGE = 86400 * 30 # 30 days # Celery Configuration CELERY_BROKER_URL = env('CELERY_BROKER_URL', default=REDIS_URL) CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND', default='redis://localhost:6379/1') CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = TIME_ZONE CELERY_TASK_TRACK_STARTED = True CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes # Django Channels CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [REDIS_URL], }, }, } # Django Cacheops CACHEOPS_REDIS = REDIS_URL CACHEOPS_DEFAULTS = { 'timeout': 60*15 # 15 minutes } CACHEOPS = { 'entities.park': {'ops': 'all', 'timeout': 60*15}, 'entities.ride': {'ops': 'all', 'timeout': 60*15}, 'entities.company': {'ops': 'all', 'timeout': 60*15}, 'core.*': {'ops': 'all', 'timeout': 60*60}, # 1 hour for reference data '*.*': {'timeout': 60*60}, } # Django Allauth ACCOUNT_AUTHENTICATION_METHOD = 'email' ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_EMAIL_VERIFICATION = 'optional' SITE_ID = 1 # CloudFlare Images CLOUDFLARE_ACCOUNT_ID = env('CLOUDFLARE_ACCOUNT_ID', default='') CLOUDFLARE_IMAGE_TOKEN = env('CLOUDFLARE_IMAGE_TOKEN', default='') CLOUDFLARE_IMAGE_HASH = env('CLOUDFLARE_IMAGE_HASH', default='') # Novu NOVU_API_KEY = env('NOVU_API_KEY', default='') NOVU_API_URL = env('NOVU_API_URL', default='https://api.novu.co') # Sentry SENTRY_DSN = env('SENTRY_DSN', default='') if SENTRY_DSN: import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.celery import CeleryIntegration sentry_sdk.init( dsn=SENTRY_DSN, integrations=[ DjangoIntegration(), CeleryIntegration(), ], traces_sample_rate=0.1, send_default_pii=False, ) # Logging Configuration LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 'style': '{', }, 'simple': { 'format': '{levelname} {message}', 'style': '{', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'verbose', }, 'file': { 'class': 'logging.FileHandler', 'filename': BASE_DIR / 'logs' / 'django.log', 'formatter': 'verbose', }, }, 'root': { 'handlers': ['console'], 'level': 'INFO', }, 'loggers': { 'django': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False, }, 'apps': { 'handlers': ['console', 'file'], 'level': 'INFO', 'propagate': False, }, }, } # Rate Limiting RATELIMIT_ENABLE = True RATELIMIT_USE_CACHE = 'default' # Django Defender DEFENDER_LOGIN_FAILURE_LIMIT = 5 DEFENDER_COOLOFF_TIME = 300 # 5 minutes DEFENDER_LOCKOUT_TEMPLATE = 'defender/lockout.html'