""" AdventureLog Server settings Reference: - Django settings: https://docs.djangoproject.com/en/stable/ref/settings/ """ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os from dotenv import load_dotenv from os import getenv from pathlib import Path from urllib.parse import urlparse from publicsuffix2 import get_sld # --------------------------------------------------------------------------- # Environment & Paths # --------------------------------------------------------------------------- # Load environment variables from .env file early so getenv works everywhere. load_dotenv() BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production # See Django deployment checklist for production hardening. # --------------------------------------------------------------------------- # Core Security & Debug # --------------------------------------------------------------------------- # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = getenv('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = getenv('DEBUG', 'true').lower() == 'true' # ALLOWED_HOSTS = [ # 'localhost', # '127.0.0.1', # 'server' # ] ALLOWED_HOSTS = ['*'] # In production, restrict to known hosts. # --------------------------------------------------------------------------- # Installed Apps # --------------------------------------------------------------------------- INSTALLED_APPS = ( "allauth_ui", 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', 'rest_framework', 'rest_framework.authtoken', 'allauth', 'allauth.account', 'allauth.mfa', 'allauth.headless', 'allauth.socialaccount', 'allauth.socialaccount.providers.github', 'allauth.socialaccount.providers.openid_connect', 'invitations', 'drf_yasg', 'djmoney', 'corsheaders', 'adventures', 'worldtravel', 'users', 'integrations', 'django.contrib.gis', # 'achievements', # Not done yet, will be added later in a future update 'widget_tweaks', 'slippers', ) # --------------------------------------------------------------------------- # Middleware # --------------------------------------------------------------------------- MIDDLEWARE = ( 'whitenoise.middleware.WhiteNoiseMiddleware', 'adventures.middleware.XSessionTokenMiddleware', 'adventures.middleware.DisableCSRFForSessionTokenMiddleware', 'adventures.middleware.DisableCSRFForAPIKeyMiddleware', 'adventures.middleware.DisableCSRFForMobileLoginSignup', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'adventures.middleware.OverrideHostMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'allauth.account.middleware.AccountMiddleware', ) # --------------------------------------------------------------------------- # Caching # --------------------------------------------------------------------------- CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': '127.0.0.1:11211', 'TIMEOUT': 60 * 60 * 24, # Optional: 1 day cache } } # For backwards compatibility for Django 1.8 MIDDLEWARE_CLASSES = MIDDLEWARE ROOT_URLCONF = 'main.urls' # WSGI_APPLICATION = 'demo.wsgi.application' # --------------------------------------------------------------------------- # Database # --------------------------------------------------------------------------- # Using legacy PG environment variables for compatibility with existing setups def env(*keys, default=None): """Return the first non-empty environment variable from a list of keys.""" for key in keys: value = os.getenv(key) if value: return value return default DATABASES = { 'default': { 'ENGINE': 'django.contrib.gis.db.backends.postgis', 'NAME': env('PGDATABASE', 'POSTGRES_DB'), 'USER': env('PGUSER', 'POSTGRES_USER'), 'PASSWORD': env('PGPASSWORD', 'POSTGRES_PASSWORD'), 'HOST': env('PGHOST', default='localhost'), 'PORT': int(env('PGPORT', default='5432')), 'OPTIONS': { 'sslmode': 'prefer', # Prefer SSL, but allow non-SSL connections }, } } # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ # --------------------------------------------------------------------------- # Internationalization # --------------------------------------------------------------------------- LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # --------------------------------------------------------------------------- # Frontend URL & Cookies # --------------------------------------------------------------------------- # Derive frontend URL from environment and configure cookie behavior. unParsedFrontenedUrl = getenv('FRONTEND_URL', 'http://localhost:3000') FRONTEND_URL = unParsedFrontenedUrl.translate(str.maketrans('', '', '\'"')) SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_NAME = 'sessionid' # Secure cookies if frontend is served over HTTPS SESSION_COOKIE_SECURE = FRONTEND_URL.startswith('https') CSRF_COOKIE_SECURE = FRONTEND_URL.startswith('https') # Dynamically determine cookie domain to support subdomains while avoiding IPs hostname = urlparse(FRONTEND_URL).hostname is_ip_address = hostname.replace('.', '').isdigit() is_single_label = '.' not in hostname # single-label hostnames (e.g., "localhost") if is_ip_address or is_single_label: SESSION_COOKIE_DOMAIN = None else: cookie_domain = get_sld(hostname) SESSION_COOKIE_DOMAIN = f".{cookie_domain}" if cookie_domain else hostname # --------------------------------------------------------------------------- # Static & Media Files # --------------------------------------------------------------------------- SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') BASE_DIR = Path(__file__).resolve().parent.parent STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_URL = '/static/' MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' # Must match NGINX root for media serving STATICFILES_DIRS = [BASE_DIR / 'static'] STORAGES = { "staticfiles": { "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", }, "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", } } SILENCED_SYSTEM_CHECKS = ["slippers.E001"] # --------------------------------------------------------------------------- # Templates # --------------------------------------------------------------------------- TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(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', ], }, }, ] ALLAUTH_UI_THEME = "dim" # --------------------------------------------------------------------------- # Authentication & Accounts # --------------------------------------------------------------------------- DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'false').lower() == 'true' DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.') SOCIALACCOUNT_ALLOW_SIGNUP = getenv('SOCIALACCOUNT_ALLOW_SIGNUP', 'false').lower() == 'true' AUTH_USER_MODEL = 'users.CustomUser' ACCOUNT_ADAPTER = 'users.adapters.CustomAccountAdapter' INVITATIONS_ADAPTER = ACCOUNT_ADAPTER INVITATIONS_ACCEPT_INVITE_AFTER_SIGNUP = True INVITATIONS_EMAIL_SUBJECT_PREFIX = 'AdventureLog: ' SOCIALACCOUNT_ADAPTER = 'users.adapters.CustomSocialAccountAdapter' ACCOUNT_SIGNUP_FORM_CLASS = 'users.form_overrides.CustomSignupForm' SESSION_SAVE_EVERY_REQUEST = True LOGIN_REDIRECT_URL = FRONTEND_URL # Redirect to frontend after login SOCIALACCOUNT_LOGIN_ON_GET = True INVITATIONS_INVITE_FORM = 'users.form_overrides.UseAdminInviteForm' INVITATIONS_SIGNUP_REDIRECT_URL = f"{FRONTEND_URL}/signup" HEADLESS_FRONTEND_URLS = { "account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}", "account_reset_password": f"{FRONTEND_URL}/user/reset-password", "account_reset_password_from_key": f"{FRONTEND_URL}/user/reset-password/{{key}}", "account_signup": f"{FRONTEND_URL}/signup", # Fallback if handshake with provider fails and `next` URL is lost. "socialaccount_login_error": f"{FRONTEND_URL}/account/provider/callback", } AUTHENTICATION_BACKENDS = [ 'users.backends.NoPasswordAuthBackend', # 'allauth.account.auth_backends.AuthenticationBackend', # 'django.contrib.auth.backends.ModelBackend', ] EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' SITE_ID = 1 ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_EMAIL_VERIFICATION = getenv('ACCOUNT_EMAIL_VERIFICATION', 'none') # 'none', 'optional', 'mandatory' SOCIALACCOUNT_EMAIL_AUTHENTICATION = True SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True # Auto-link by email SOCIALACCOUNT_AUTO_SIGNUP = True # Allow auto-signup post adapter checks FORCE_SOCIALACCOUNT_LOGIN = getenv('FORCE_SOCIALACCOUNT_LOGIN', 'false').lower() == 'true' # When true, only social login is allowed (no password login) and the login page will show only social providers or redirect directly to the first provider if only one is configured. if getenv('EMAIL_BACKEND', 'console') == 'console': EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' else: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = getenv('EMAIL_HOST') EMAIL_USE_TLS = getenv('EMAIL_USE_TLS', 'true').lower() == 'true' EMAIL_PORT = getenv('EMAIL_PORT', 587) EMAIL_USE_SSL = getenv('EMAIL_USE_SSL', 'false').lower() == 'true' EMAIL_HOST_USER = getenv('EMAIL_HOST_USER') EMAIL_HOST_PASSWORD = getenv('EMAIL_HOST_PASSWORD') DEFAULT_FROM_EMAIL = getenv('DEFAULT_FROM_EMAIL') # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # EMAIL_HOST = 'smtp.resend.com' # EMAIL_USE_TLS = False # EMAIL_PORT = 2465 # EMAIL_USE_SSL = True # EMAIL_HOST_USER = 'resend' # EMAIL_HOST_PASSWORD = '' # DEFAULT_FROM_EMAIL = 'mail@mail.user.com' # --------------------------------------------------------------------------- # Account Rate Limits # --------------------------------------------------------------------------- # Configure rate limits for allauth authentication actions to prevent abuse # Format: "action": "count/period/scope" # Examples: "5/m/user" = 5 per minute per user, "20/m/ip" = 20 per minute per IP ACCOUNT_RATE_LIMITS = { "change_password": "5/m/user", # 5 password changes per minute per user "change_phone": "1/m/user", # 1 phone change per minute per user "manage_email": "10/m/user", # 10 email management actions per minute per user "reset_password": "20/m/ip,5/m/key", # 20 per minute per IP, 5 per minute per email "reauthenticate": "10/m/user", # 10 reauthentication attempts per minute per user "reset_password_from_key": "20/m/ip", # 20 password resets per minute per IP "signup": "20/m/ip", # 20 signups per minute per IP (prevents mass registration) "login": "30/m/ip", # 30 login attempts per minute per IP "login_failed": "10/m/ip,5/5m/key", # 10 failed logins per minute per IP, 5 per 5 min per user "confirm_email": "1/3m/key", # 1 email confirmation per 3 minutes per email } # --------------------------------------------------------------------------- # Django REST Framework # --------------------------------------------------------------------------- REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'users.authentication.APIKeyAuthentication', 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.throttling.UserRateThrottle', ], 'DEFAULT_THROTTLE_RATES': { 'user': '1000/day', 'image_proxy': '60/minute', }, } if DEBUG: REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = ( 'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.BrowsableAPIRenderer', ) else: REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = ( 'rest_framework.renderers.JSONRenderer', ) # --------------------------------------------------------------------------- # CORS & CSRF # --------------------------------------------------------------------------- CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()] CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()] CORS_ALLOW_CREDENTIALS = True DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, 'file': { 'class': 'logging.FileHandler', 'filename': 'scheduler.log', }, }, 'root': { 'handlers': ['console', 'file'], 'level': 'INFO', }, 'loggers': { 'django': { 'handlers': ['console', 'file'], 'level': 'INFO', 'propagate': False, }, }, } # --------------------------------------------------------------------------- # Public URLs & Third-Party Integrations # --------------------------------------------------------------------------- PUBLIC_URL = getenv('PUBLIC_URL', 'http://localhost:8000') # ADVENTURELOG_CDN_URL = getenv('ADVENTURELOG_CDN_URL', 'https://cdn.adventurelog.app') # Major release version of AdventureLog, not including the patch version date. ADVENTURELOG_RELEASE_VERSION = 'v0.12.0' # https://github.com/dr5hn/countries-states-cities-database/tags COUNTRY_REGION_JSON_VERSION = 'v3.1' # External service keys (do not hardcode secrets) GOOGLE_MAPS_API_KEY = getenv('GOOGLE_MAPS_API_KEY', '') STRAVA_CLIENT_ID = getenv('STRAVA_CLIENT_ID', '') STRAVA_CLIENT_SECRET = getenv('STRAVA_CLIENT_SECRET', '')