""" Third-party application configuration for thrillwiki project. This module configures third-party Django applications including: - django-allauth (authentication) - Celery (task queue) - Health checks - Tailwind CSS - Cloudflare Images - Road Trip service Why python-decouple? - Already used in base.py for consistency - Simpler API than django-environ - Sufficient for our configuration needs - Better separation of config from code """ from decouple import config # ============================================================================= # Django Allauth Configuration # ============================================================================= # https://django-allauth.readthedocs.io/ SITE_ID = 1 # Signup fields configuration # The asterisks indicate required fields ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"] # Login methods - allow both email and username ACCOUNT_LOGIN_METHODS = {"email", "username"} # Email verification settings ACCOUNT_EMAIL_VERIFICATION = config("ACCOUNT_EMAIL_VERIFICATION", default="mandatory") # Note: ACCOUNT_EMAIL_REQUIRED is handled by ACCOUNT_SIGNUP_FIELDS above (email* = required) ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_CHANGE = True ACCOUNT_EMAIL_VERIFICATION_SUPPORTS_RESEND = True # Security settings ACCOUNT_REAUTHENTICATION_REQUIRED = True ACCOUNT_EMAIL_NOTIFICATIONS = True ACCOUNT_EMAIL_UNKNOWN_ACCOUNTS = False # Redirect URLs LOGIN_REDIRECT_URL = config("LOGIN_REDIRECT_URL", default="/") ACCOUNT_LOGOUT_REDIRECT_URL = config("ACCOUNT_LOGOUT_REDIRECT_URL", default="/") # Custom adapters for extending allauth behavior ACCOUNT_ADAPTER = "apps.accounts.adapters.CustomAccountAdapter" SOCIALACCOUNT_ADAPTER = "apps.accounts.adapters.CustomSocialAccountAdapter" # Social account provider settings SOCIALACCOUNT_PROVIDERS = { "google": { "SCOPE": [ "profile", "email", ], "AUTH_PARAMS": {"access_type": "online"}, }, "discord": { "SCOPE": ["identify", "email"], "OAUTH_PKCE_ENABLED": True, }, } # Additional social account settings SOCIALACCOUNT_LOGIN_ON_GET = True SOCIALACCOUNT_AUTO_SIGNUP = False SOCIALACCOUNT_STORE_TOKENS = True # ============================================================================= # MFA (Multi-Factor Authentication) Configuration # ============================================================================= # https://docs.allauth.org/en/latest/mfa/index.html # Supported authenticator types - TOTP and WebAuthn (Passkeys) MFA_SUPPORTED_TYPES = ["totp", "webauthn"] # TOTP settings MFA_TOTP_ISSUER = config("MFA_TOTP_ISSUER", default="ThrillWiki") # Number of digits for TOTP codes (default is 6) MFA_TOTP_DIGITS = 6 # Interval in seconds for TOTP code generation (default 30) MFA_TOTP_PERIOD = 30 # WebAuthn/Passkey settings MFA_PASSKEY_LOGIN_ENABLED = config("MFA_PASSKEY_LOGIN_ENABLED", default=True, cast=bool) # Read DEBUG directly (same source as base.py) to avoid circular import _DEBUG_MFA = config("DEBUG", default=True, cast=bool) # Allow insecure origin (http://localhost) for WebAuthn in development MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = config( "MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN", default=_DEBUG_MFA, cast=bool ) # ============================================================================= # Login By Code (Magic Link) Configuration # ============================================================================= # https://docs.allauth.org/en/latest/account/configuration.html#login-by-code # Enable magic link / login by code feature ACCOUNT_LOGIN_BY_CODE_ENABLED = config("ACCOUNT_LOGIN_BY_CODE_ENABLED", default=True, cast=bool) # Maximum attempts to enter the code ACCOUNT_LOGIN_BY_CODE_MAX_ATTEMPTS = config("ACCOUNT_LOGIN_BY_CODE_MAX_ATTEMPTS", default=3, cast=int) # Code expiration timeout in seconds (5 minutes default) ACCOUNT_LOGIN_BY_CODE_TIMEOUT = config("ACCOUNT_LOGIN_BY_CODE_TIMEOUT", default=300, cast=int) # ============================================================================= # Headless API Configuration # ============================================================================= # https://docs.allauth.org/en/latest/headless/configuration.html # Frontend URL for email links (password reset, email verification, etc.) HEADLESS_FRONTEND_URLS = { "account_confirm_email": config("FRONTEND_URL", default="http://localhost:5173") + "/auth/callback?key={key}", "account_reset_password": config("FRONTEND_URL", default="http://localhost:5173") + "/auth/reset-password?key={key}", "account_signup": config("FRONTEND_URL", default="http://localhost:5173") + "/auth?tab=signup", "socialaccount_login_error": config("FRONTEND_URL", default="http://localhost:5173") + "/auth?error=social", } # Set to True since our frontend is a separate SPA HEADLESS_ONLY = config("HEADLESS_ONLY", default=False, cast=bool) # Allow both "app" and "browser" clients for flexibility # "browser" uses cookies, "app" uses Authorization header HEADLESS_CLIENTS = ("app", "browser") # ============================================================================= # Celery Configuration # ============================================================================= # Celery task queue settings (actual Celery config is in config/celery.py) CELERY_BROKER_URL = config("REDIS_URL", default="redis://localhost:6379/1") CELERY_RESULT_BACKEND = config("REDIS_URL", default="redis://localhost:6379/1") # Task settings for test environments CELERY_TASK_ALWAYS_EAGER = config("CELERY_TASK_ALWAYS_EAGER", default=False, cast=bool) CELERY_TASK_EAGER_PROPAGATES = config("CELERY_TASK_EAGER_PROPAGATES", default=False, cast=bool) # ============================================================================= # Health Check Configuration # ============================================================================= # https://django-health-check.readthedocs.io/ HEALTH_CHECK = { "DISK_USAGE_MAX": config("HEALTH_CHECK_DISK_USAGE_MAX", default=90, cast=int), "MEMORY_MIN": config("HEALTH_CHECK_MEMORY_MIN", default=100, cast=int), } # Custom health check backends HEALTH_CHECK_BACKENDS = [ "health_check.db", "health_check.cache", "health_check.storage", "core.health_checks.custom_checks.CacheHealthCheck", "core.health_checks.custom_checks.DatabasePerformanceCheck", "core.health_checks.custom_checks.ApplicationHealthCheck", "core.health_checks.custom_checks.ExternalServiceHealthCheck", "core.health_checks.custom_checks.DiskSpaceHealthCheck", ] # ============================================================================= # Tailwind CSS Configuration # ============================================================================= # https://django-tailwind.readthedocs.io/ TAILWIND_CLI_CONFIG_FILE = "tailwind.config.js" TAILWIND_CLI_SRC_CSS = "static/css/src/input.css" TAILWIND_CLI_DIST_CSS = "css/tailwind.css" # ============================================================================= # Cloudflare Images Configuration # ============================================================================= # https://developers.cloudflare.com/images/ CLOUDFLARE_IMAGES = { "ACCOUNT_ID": config("CLOUDFLARE_IMAGES_ACCOUNT_ID", default=""), "API_TOKEN": config("CLOUDFLARE_IMAGES_API_TOKEN", default=""), "ACCOUNT_HASH": config("CLOUDFLARE_IMAGES_ACCOUNT_HASH", default=""), # Optional settings "DEFAULT_VARIANT": config("CLOUDFLARE_IMAGES_DEFAULT_VARIANT", default="public"), "UPLOAD_TIMEOUT": config("CLOUDFLARE_IMAGES_UPLOAD_TIMEOUT", default=300, cast=int), "WEBHOOK_SECRET": config("CLOUDFLARE_IMAGES_WEBHOOK_SECRET", default=""), "CLEANUP_EXPIRED_HOURS": config("CLOUDFLARE_IMAGES_CLEANUP_HOURS", default=24, cast=int), "MAX_FILE_SIZE": config("CLOUDFLARE_IMAGES_MAX_FILE_SIZE", default=10 * 1024 * 1024, cast=int), "ALLOWED_FORMATS": ["jpeg", "png", "gif", "webp"], "REQUIRE_SIGNED_URLS": config("CLOUDFLARE_IMAGES_REQUIRE_SIGNED_URLS", default=False, cast=bool), "DEFAULT_METADATA": {}, } # ============================================================================= # Road Trip Service Configuration # ============================================================================= # Settings for the road trip planning service using OpenStreetMap ROADTRIP_CACHE_TIMEOUT = config("ROADTRIP_CACHE_TIMEOUT", default=3600 * 24, cast=int) # 24 hours for geocoding ROADTRIP_ROUTE_CACHE_TIMEOUT = config("ROADTRIP_ROUTE_CACHE_TIMEOUT", default=3600 * 6, cast=int) # 6 hours for routes ROADTRIP_MAX_REQUESTS_PER_SECOND = config( "ROADTRIP_MAX_REQUESTS_PER_SECOND", default=1, cast=int ) # Respect OSM rate limits ROADTRIP_USER_AGENT = config("ROADTRIP_USER_AGENT", default="ThrillWiki/1.0 (https://thrillwiki.com)") ROADTRIP_REQUEST_TIMEOUT = config("ROADTRIP_REQUEST_TIMEOUT", default=10, cast=int) # seconds ROADTRIP_MAX_RETRIES = config("ROADTRIP_MAX_RETRIES", default=3, cast=int) ROADTRIP_BACKOFF_FACTOR = config("ROADTRIP_BACKOFF_FACTOR", default=2, cast=int) # ============================================================================= # Autocomplete Configuration # ============================================================================= # django-autocomplete-light settings AUTOCOMPLETE_BLOCK_UNAUTHENTICATED = config("AUTOCOMPLETE_BLOCK_UNAUTHENTICATED", default=False, cast=bool) # ============================================================================= # Frontend Configuration # ============================================================================= FRONTEND_DOMAIN = config("FRONTEND_DOMAIN", default="https://thrillwiki.com") # ============================================================================= # Cloudflare Turnstile Configuration # ============================================================================= # https://developers.cloudflare.com/turnstile/ TURNSTILE_SITEKEY = config("TURNSTILE_SITEKEY", default="") TURNSTILE_SECRET = config("TURNSTILE_SECRET", default="") # Read DEBUG directly (same source as base.py) to avoid circular import _DEBUG = config("DEBUG", default=True, cast=bool) # Skip Turnstile validation in debug mode or if no secret configured TURNSTILE_SKIP_VALIDATION = config( "TURNSTILE_SKIP_VALIDATION", default=(_DEBUG or not TURNSTILE_SECRET), cast=bool )