mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-03-25 01:33:20 -04:00
* Enhance money parsing and normalization in BackupViewSet * Refactor money parsing in BackupViewSet for schema safety and enhance profile statistics display with new metrics * Improve throttling handling in auth hooks to enhance user experience during high-load scenarios * fix(deps): update countries-states-cities-database v3.1 (#1047) update countries-states-cities-database to fixed some cities error * fix: update appVersion to v0.12.0-main-031526 * feat: enhance CategoryFilterDropdown with event dispatching and URL synchronization. Fixes [BUG] Category Filter not working in v0.12.0 Fixes #990 * feat(profile): add record holders for activities and display details in profile page * feat: restructure issue templates and enhance contribution guidelines * Potential fix for code scanning alert no. 50: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for code scanning alert no. 51: Workflow does not contain permissions Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: 橙 <chengjunchao@hotmail.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
398 lines
15 KiB
Python
398 lines
15 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 = (
|
|
"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.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': (
|
|
'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', '')
|