Files
AdventureLog/backend/server/main/settings.py

363 lines
13 KiB
Python

"""
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 = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
# "allauth_ui",
'rest_framework',
'rest_framework.authtoken',
'allauth',
'allauth.account',
'allauth.mfa',
'allauth.headless',
'allauth.socialaccount',
'allauth.socialaccount.providers.github',
'allauth.socialaccount.providers.openid_connect',
'drf_yasg',
'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.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',
],
},
},
]
# ---------------------------------------------------------------------------
# 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'
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
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'
# ---------------------------------------------------------------------------
# Django REST Framework
# ---------------------------------------------------------------------------
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
}
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.11.0'
# https://github.com/dr5hn/countries-states-cities-database/tags
COUNTRY_REGION_JSON_VERSION = 'v3.0'
# 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', '')