""" 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' # Unified API Standard v1.0 Feature Flags # Set IGNY8_USE_UNIFIED_EXCEPTION_HANDLER=True to enable unified exception handler # Set IGNY8_DEBUG_THROTTLE=True to bypass rate limiting in development IGNY8_DEBUG_THROTTLE = os.getenv('IGNY8_DEBUG_THROTTLE', str(DEBUG)).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', 'drf_spectacular', # OpenAPI 3.0 schema generation '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', 'igny8_core.modules.automation.apps.AutomationConfig', 'igny8_core.business.site_building.apps.SiteBuildingConfig', 'igny8_core.business.optimization.apps.OptimizationConfig', 'igny8_core.business.publishing.apps.PublishingConfig', 'igny8_core.business.integration.apps.IntegrationConfig', 'igny8_core.modules.site_builder.apps.SiteBuilderConfig', 'igny8_core.modules.linker.apps.LinkerConfig', 'igny8_core.modules.optimizer.apps.OptimizerConfig', 'igny8_core.modules.publisher.apps.PublisherConfig', 'igny8_core.modules.integration.apps.IntegrationConfig', ] # 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.middleware.request_id.RequestIDMiddleware', # Request ID tracking (must be early) '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 ], # Unified API Standard v1.0 Configuration # Exception handler - wraps all errors in unified format # Unified API Standard v1.0: Exception handler enabled by default # Set IGNY8_USE_UNIFIED_EXCEPTION_HANDLER=False to disable 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler' if os.getenv('IGNY8_USE_UNIFIED_EXCEPTION_HANDLER', 'True').lower() == 'false' else 'igny8_core.api.exception_handlers.custom_exception_handler', # Rate limiting - configured but bypassed in DEBUG mode 'DEFAULT_THROTTLE_CLASSES': [ 'igny8_core.api.throttles.DebugScopedRateThrottle', ], 'DEFAULT_THROTTLE_RATES': { # AI Functions - Expensive operations 'ai_function': '10/min', # AI content generation, clustering 'image_gen': '15/min', # Image generation # Content Operations 'content_write': '30/min', # Content creation, updates 'content_read': '100/min', # Content listing, retrieval # Authentication 'auth': '20/min', # Login, register, password reset 'auth_strict': '5/min', # Sensitive auth operations # Planner Operations 'planner': '60/min', # Keyword, cluster, idea operations 'planner_ai': '10/min', # AI-powered planner operations # Writer Operations 'writer': '60/min', # Task, content management 'writer_ai': '10/min', # AI-powered writer operations # System Operations 'system': '100/min', # Settings, prompts, profiles 'system_admin': '30/min', # Admin-only system operations # Billing Operations 'billing': '30/min', # Credit queries, usage logs 'billing_admin': '10/min', # Credit management (admin) 'linker': '30/min', # Content linking operations 'optimizer': '10/min', # AI-powered optimization # Default fallback 'default': '100/min', # Default for endpoints without scope }, # OpenAPI Schema Generation (drf-spectacular) 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } # drf-spectacular Settings for OpenAPI 3.0 Schema Generation SPECTACULAR_SETTINGS = { 'TITLE': 'IGNY8 API v1.0', 'DESCRIPTION': ''' IGNY8 Unified API Standard v1.0 A comprehensive REST API for content planning, creation, and management. ## Features - **Unified Response Format**: All endpoints return consistent JSON structure - **Layered Authorization**: Authentication → Tenant Access → Role → Site/Sector - **Centralized Error Handling**: All errors wrapped in unified format - **Scoped Rate Limiting**: Different limits for different operation types - **Tenant Isolation**: All resources scoped by account/site/sector - **Request Tracking**: Every request has a unique ID for debugging ## Authentication All endpoints require JWT Bearer token authentication except: - `GET /api/v1/system/ping/` - Health check endpoint - `POST /api/v1/auth/login/` - User login - `POST /api/v1/auth/register/` - User registration - `GET /api/v1/auth/plans/` - List subscription plans - `GET /api/v1/auth/industries/` - List industries - `GET /api/v1/system/status/` - System status Include token in Authorization header: ``` Authorization: Bearer ``` ## Response Format All successful responses follow this format: ```json { "success": true, "data": {...}, "message": "Optional success message", "request_id": "uuid" } ``` All error responses follow this format: ```json { "success": false, "error": "Error message", "errors": { "field_name": ["Field-specific errors"] }, "request_id": "uuid" } ``` ## Rate Limiting Rate limits are scoped by operation type. Check response headers: - `X-Throttle-Limit`: Maximum requests allowed - `X-Throttle-Remaining`: Remaining requests in current window - `X-Throttle-Reset`: Time when limit resets (Unix timestamp) ## Pagination List endpoints support pagination with query parameters: - `page`: Page number (default: 1) - `page_size`: Items per page (default: 10, max: 100) Paginated responses include: ```json { "success": true, "count": 100, "next": "http://api.igny8.com/api/v1/endpoint/?page=2", "previous": null, "results": [...] } ``` ''', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, 'SCHEMA_PATH_PREFIX': '/api/v1', 'COMPONENT_SPLIT_REQUEST': True, 'COMPONENT_NO_READ_ONLY_REQUIRED': True, # Custom schema generator to include unified response format 'SCHEMA_GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator', # Include request/response examples 'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], 'SERVE_AUTHENTICATION': None, # Allow unauthenticated access to docs # Tag configuration - prevent auto-generation and use explicit tags 'TAGS': [ {'name': 'Authentication', 'description': 'User authentication and registration'}, {'name': 'Planner', 'description': 'Keywords, clusters, and content ideas'}, {'name': 'Writer', 'description': 'Tasks, content, and images'}, {'name': 'System', 'description': 'Settings, prompts, and integrations'}, {'name': 'Billing', 'description': 'Credits, usage, and transactions'}, ], 'TAGS_ORDER': ['Authentication', 'Planner', 'Writer', 'System', 'Billing'], # Postprocessing hook to filter out auto-generated tags 'POSTPROCESSING_HOOKS': ['igny8_core.api.schema_extensions.postprocess_schema_filter_tags'], # Swagger UI configuration 'SWAGGER_UI_SETTINGS': { 'deepLinking': True, 'displayOperationId': False, 'defaultModelsExpandDepth': 1, # Collapse models by default 'defaultModelExpandDepth': 1, # Collapse model properties by default 'defaultModelRendering': 'model', # Show models in a cleaner format 'displayRequestDuration': True, 'docExpansion': 'none', # Collapse all operations by default 'filter': True, # Enable filter box 'showExtensions': True, 'showCommonExtensions': True, 'tryItOutEnabled': True, # Enable "Try it out" by default }, # ReDoc configuration 'REDOC_UI_SETTINGS': { 'hideDownloadButton': False, 'hideHostname': False, 'hideLoading': False, 'hideSingleRequestSampleTab': False, 'expandResponses': '200,201', # Expand successful responses 'jsonSampleExpandLevel': 2, # Expand JSON samples 2 levels 'hideFab': False, 'theme': { 'colors': { 'primary': { 'main': '#32329f' } } } }, # Schema presentation improvements 'SCHEMA_COERCE_PATH_PK': True, 'SCHEMA_COERCE_METHOD_NAMES': { 'retrieve': 'get', 'list': 'list', 'create': 'post', 'update': 'put', 'partial_update': 'patch', 'destroy': 'delete', }, # Custom response format documentation 'EXTENSIONS_INFO': { 'x-code-samples': [ { 'lang': 'Python', 'source': ''' import requests headers = { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' } response = requests.get('https://api.igny8.com/api/v1/planner/keywords/', headers=headers) data = response.json() if data['success']: keywords = data['results'] # or data['data'] for single objects else: print(f"Error: {data['error']}") ''' }, { 'lang': 'JavaScript', 'source': ''' const response = await fetch('https://api.igny8.com/api/v1/planner/keywords/', { headers: { 'Authorization': 'Bearer ', 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { const keywords = data.results || data.data; } else { console.error('Error:', data.error); } ''' } ] } } # 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