""" Django settings for igny8_core project. """ from pathlib import Path from datetime import timedelta from urllib.parse import urlparse import os BASE_DIR = Path(__file__).resolve().parent.parent # SECURITY: SECRET_KEY must be set via environment variable in production # Generate a new key with: python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-)#i8!6+_&j97eb_4actu86=qtg)p+p#)vr48!ahjs8u=o5#5aw') # SECURITY: DEBUG should be False in production # Set DEBUG=False via environment variable for production deployments DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' ALLOWED_HOSTS = [ '*', # Allow all hosts for flexibility 'api.igny8.com', 'app.igny8.com', 'igny8.com', 'www.igny8.com', 'localhost', '127.0.0.1', # Note: Do NOT add static IP addresses here - they change on container restart # Use container names or domain names instead ] INSTALLED_APPS = [ 'igny8_core.admin.apps.Igny8AdminConfig', # Custom admin config 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'django_filters', 'corsheaders', 'igny8_core.auth.apps.Igny8CoreAuthConfig', # Use app config with custom label 'igny8_core.ai.apps.AIConfig', # AI Framework 'igny8_core.modules.planner.apps.PlannerConfig', 'igny8_core.modules.writer.apps.WriterConfig', 'igny8_core.modules.system.apps.SystemConfig', 'igny8_core.modules.billing.apps.BillingConfig', ] # System module needs explicit registration for admin AUTH_USER_MODEL = 'igny8_core_auth.User' CSRF_TRUSTED_ORIGINS = [ 'https://api.igny8.com', 'https://app.igny8.com', 'http://localhost:8011', 'http://127.0.0.1:8011', ] # Only use secure cookies in production (HTTPS) # Default to False - set USE_SECURE_COOKIES=True in docker-compose for production # This allows local development to work without HTTPS USE_SECURE_COOKIES = os.getenv('USE_SECURE_COOKIES', 'False').lower() == 'true' SESSION_COOKIE_SECURE = USE_SECURE_COOKIES CSRF_COOKIE_SECURE = USE_SECURE_COOKIES MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'igny8_core.auth.middleware.AccountContextMiddleware', # Multi-account support # AccountContextMiddleware sets request.account from JWT 'igny8_core.middleware.resource_tracker.ResourceTrackingMiddleware', # Resource tracking for admin debug 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'igny8_core.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], '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 = 'igny8_core.wsgi.application' DATABASES = {} database_url = os.getenv("DATABASE_URL") db_engine = os.getenv("DB_ENGINE", "").lower() force_postgres = os.getenv("DJANGO_FORCE_POSTGRES", "false").lower() == "true" if database_url: parsed = urlparse(database_url) scheme = (parsed.scheme or "").lower() if scheme in {"sqlite", "sqlite3"}: # Support both absolute and project-relative SQLite paths netloc_path = f"{parsed.netloc}{parsed.path}" if parsed.netloc else parsed.path db_path = netloc_path.lstrip("/") or "db.sqlite3" if os.path.isabs(netloc_path): sqlite_name = netloc_path else: sqlite_name = Path(db_path) if os.path.isabs(db_path) else BASE_DIR / db_path DATABASES["default"] = { "ENGINE": "django.db.backends.sqlite3", "NAME": str(sqlite_name), } else: DATABASES["default"] = { "ENGINE": "django.db.backends.postgresql", "NAME": parsed.path.lstrip("/") or os.getenv("DB_NAME", "igny8_db"), "USER": parsed.username or os.getenv("DB_USER", "igny8"), "PASSWORD": parsed.password or os.getenv("DB_PASSWORD", "igny8pass"), "HOST": parsed.hostname or os.getenv("DB_HOST", "postgres"), "PORT": str(parsed.port or os.getenv("DB_PORT", "5432")), } elif db_engine in {"sqlite", "sqlite3"} or os.getenv("USE_SQLITE", "false").lower() == "true": sqlite_name = os.getenv("SQLITE_NAME") if not sqlite_name: sqlite_name = BASE_DIR / "db.sqlite3" DATABASES["default"] = { "ENGINE": "django.db.backends.sqlite3", "NAME": str(sqlite_name), } elif DEBUG and not force_postgres and not os.getenv("DB_HOST") and not os.getenv("DB_NAME") and not os.getenv("DB_USER"): DATABASES["default"] = { "ENGINE": "django.db.backends.sqlite3", "NAME": str(BASE_DIR / "db.sqlite3"), } else: DATABASES["default"] = { "ENGINE": "django.db.backends.postgresql", "NAME": os.getenv("DB_NAME", "igny8_db"), "USER": os.getenv("DB_USER", "igny8"), "PASSWORD": os.getenv("DB_PASSWORD", "igny8pass"), "HOST": os.getenv("DB_HOST", "postgres"), "PORT": os.getenv("DB_PORT", "5432"), } 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'}, ] LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Only use SECURE_PROXY_SSL_HEADER in production behind reverse proxy # Default to False - set USE_SECURE_PROXY_HEADER=True in docker-compose for production # Caddy sets X-Forwarded-Proto header, so enable this when behind Caddy USE_SECURE_PROXY = os.getenv('USE_SECURE_PROXY_HEADER', 'False').lower() == 'true' if USE_SECURE_PROXY: SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') else: SECURE_PROXY_SSL_HEADER = None # Admin login URL - use relative URL to avoid hardcoded domain LOGIN_URL = '/admin/login/' LOGIN_REDIRECT_URL = '/admin/' # Force Django to use request.get_host() instead of Sites framework # This ensures redirects use the current request's host USE_X_FORWARDED_HOST = False # REST Framework Configuration REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'igny8_core.api.pagination.CustomPageNumberPagination', 'PAGE_SIZE': 10, 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.AllowAny', # Allow unauthenticated access for now ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'igny8_core.api.authentication.JWTAuthentication', # JWT token authentication 'igny8_core.api.authentication.CSRFExemptSessionAuthentication', # Session auth without CSRF for API 'rest_framework.authentication.BasicAuthentication', # Enable basic auth as fallback ], } # CORS Configuration CORS_ALLOWED_ORIGINS = [ "https://app.igny8.com", "https://igny8.com", "https://www.igny8.com", "http://localhost:5173", "http://localhost:5174", "http://localhost:3000", "http://127.0.0.1:5173", "http://127.0.0.1:5174", ] CORS_ALLOW_CREDENTIALS = True # Allow custom headers for resource tracking # Include default headers plus our custom debug header CORS_ALLOW_HEADERS = [ 'accept', 'accept-encoding', 'authorization', 'content-type', 'dnt', 'origin', 'user-agent', 'x-csrftoken', 'x-requested-with', 'x-debug-resource-tracking', # Allow debug tracking header ] # Note: django-cors-headers has default headers that include the above. # If you want to extend defaults, you can import default_headers from corsheaders.defaults # For now, we're explicitly listing all needed headers including our custom one. # Expose custom headers to frontend CORS_EXPOSE_HEADERS = [ 'x-resource-tracking-id', # Expose request tracking ID ] # JWT Configuration JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY) JWT_ALGORITHM = 'HS256' JWT_ACCESS_TOKEN_EXPIRY = timedelta(minutes=15) JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=7) # Celery Configuration CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', f"redis://{os.getenv('REDIS_HOST', 'redis')}:{os.getenv('REDIS_PORT', '6379')}/0") CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', f"redis://{os.getenv('REDIS_HOST', 'redis')}:{os.getenv('REDIS_PORT', '6379')}/0") CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = TIME_ZONE CELERY_ENABLE_UTC = True CELERY_TASK_TRACK_STARTED = True CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 25 minutes CELERY_WORKER_PREFETCH_MULTIPLIER = 1 CELERY_WORKER_MAX_TASKS_PER_CHILD = 1000