""" Django settings for igny8_core project. Test comment: webhook restart test """ 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 = [ # Django Unfold admin theme - MUST be before django.contrib.admin 'unfold', 'unfold.contrib.filters', 'unfold.contrib.import_export', 'unfold.contrib.simple_history', # Core Django apps - Custom admin with IGNY8 branding 'igny8_core.admin.apps.Igny8AdminConfig', # Custom admin config 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third-party apps 'rest_framework', 'django_filters', 'corsheaders', 'drf_spectacular', # OpenAPI 3.0 schema generation 'import_export', 'rangefilter', 'django_celery_results', 'simple_history', # IGNY8 apps '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.business.automation', # AI Automation Pipeline 'igny8_core.business.notifications.apps.NotificationsConfig', # User Notifications 'igny8_core.business.optimization.apps.OptimizationConfig', 'igny8_core.business.publishing.apps.PublishingConfig', 'igny8_core.business.integration.apps.IntegrationConfig', 'igny8_core.modules.linker.apps.LinkerConfig', 'igny8_core.modules.optimizer.apps.OptimizerConfig', 'igny8_core.modules.publisher.apps.PublisherConfig', 'igny8_core.modules.integration.apps.IntegrationConfig', 'igny8_core.plugins.apps.PluginsConfig', # Plugin Distribution System ] # 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 CSRF_COOKIE_SAMESITE = 'Lax' # Match session cookie setting CSRF_COOKIE_DOMAIN = '.igny8.com' # Share CSRF cookie across subdomains # CRITICAL: Session isolation to prevent contamination SESSION_COOKIE_NAME = 'igny8_sessionid' # Custom name to avoid conflicts SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access SESSION_COOKIE_SAMESITE = 'Lax' # Changed from Strict to Lax - allows same-site top-level navigation SESSION_COOKIE_AGE = 3600 # 1 hour - extends on every request due to SESSION_SAVE_EVERY_REQUEST SESSION_SAVE_EVERY_REQUEST = True # CRITICAL: Update session on every request to prevent idle timeout SESSION_COOKIE_PATH = '/' # Explicit path SESSION_COOKIE_DOMAIN = '.igny8.com' # CRITICAL: Share cookie across subdomains (app.igny8.com and api.igny8.com) # CRITICAL: Custom authentication backend to disable user caching AUTHENTICATION_BACKENDS = [ 'igny8_core.auth.backends.NoCacheModelBackend', # Custom backend without caching ] 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', 'simple_history.middleware.HistoryRequestMiddleware', # Audit trail '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': [BASE_DIR / 'igny8_core' / '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', ], }, }, ] 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') STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'igny8_core', 'static'), ] 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': [ 'igny8_core.api.permissions.IsAuthenticatedAndActive', 'igny8_core.api.permissions.HasTenantAccess', ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'igny8_core.api.authentication.APIKeyAuthentication', # WordPress API key authentication (check first) '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 - DISABLED 'DEFAULT_THROTTLE_CLASSES': [], 'DEFAULT_THROTTLE_RATES': {}, # 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': 'Account', 'description': 'Account settings, team, and usage analytics'}, {'name': 'Integration', 'description': 'Site integrations and sync'}, {'name': 'System', 'description': 'Settings, prompts, and integrations'}, {'name': 'Admin Billing', 'description': 'Admin-only billing management'}, {'name': 'Billing', 'description': 'Credits, usage, and transactions'}, {'name': 'Planner', 'description': 'Keywords, clusters, and content ideas'}, {'name': 'Writer', 'description': 'Tasks, content, and images'}, {'name': 'Automation', 'description': 'Automation configuration and runs'}, {'name': 'Linker', 'description': 'Internal linking operations'}, {'name': 'Optimizer', 'description': 'Content optimization operations'}, {'name': 'Publisher', 'description': 'Publishing records and deployments'}, ], 'TAGS_ORDER': [ 'Authentication', 'Account', 'Integration', 'System', 'Admin Billing', 'Billing', 'Planner', 'Writer', 'Automation', 'Linker', 'Optimizer', 'Publisher', ], # 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", "https://sites.igny8.com", "http://localhost:5173", "http://localhost:5174", "http://localhost:5176", "http://localhost:8024", "http://localhost:3000", "http://127.0.0.1:5173", "http://127.0.0.1:5174", "http://127.0.0.1:5176", "http://127.0.0.1:8024", "http://31.97.144.105:8024", ] 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' # Default: 1 hour for normal login, 20 days for remember me JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1) # Default: 1 hour JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME = timedelta(days=30) # Remember me: 30 days JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30) # Extended to 30 days for persistent login # Celery Configuration # FIXED: Use redis:// URL with explicit string parameters to avoid Celery backend key serialization issues 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 # FIXED: Add explicit backend options to prevent key serialization issues CELERY_RESULT_BACKEND_TRANSPORT_OPTIONS = { 'master_name': 'mymaster' } if os.getenv('REDIS_SENTINEL_ENABLED', 'false').lower() == 'true' else {} CELERY_REDIS_BACKEND_USE_SSL = os.getenv('REDIS_SSL_ENABLED', 'false').lower() == 'true' # Publish/Sync Logging Configuration PUBLISH_SYNC_LOG_DIR = os.path.join(BASE_DIR, 'logs', 'publish-sync-logs') BILLING_LOG_DIR = os.path.join(BASE_DIR, 'logs', 'billing-logs') os.makedirs(PUBLISH_SYNC_LOG_DIR, exist_ok=True) os.makedirs(BILLING_LOG_DIR, exist_ok=True) LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '[{asctime}] [{levelname}] [{name}] {message}', 'style': '{', 'datefmt': '%Y-%m-%d %H:%M:%S', }, 'publish_sync': { 'format': '[{asctime}] [{levelname}] {message}', 'style': '{', 'datefmt': '%Y-%m-%d %H:%M:%S', }, 'billing': { 'format': '[{asctime}] [{levelname}] [{name}] {message}', 'style': '{', 'datefmt': '%Y-%m-%d %H:%M:%S', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'verbose', }, 'publish_sync_file': { 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(PUBLISH_SYNC_LOG_DIR, 'publish-sync.log'), 'maxBytes': 10 * 1024 * 1024, # 10 MB 'backupCount': 10, 'formatter': 'publish_sync', }, 'wordpress_api_file': { 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(PUBLISH_SYNC_LOG_DIR, 'wordpress-api.log'), 'maxBytes': 10 * 1024 * 1024, # 10 MB 'backupCount': 10, 'formatter': 'publish_sync', }, 'webhook_file': { 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(PUBLISH_SYNC_LOG_DIR, 'webhooks.log'), 'maxBytes': 10 * 1024 * 1024, # 10 MB 'backupCount': 10, 'formatter': 'publish_sync', }, 'billing_file': { 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(BILLING_LOG_DIR, 'billing.log'), 'maxBytes': 10 * 1024 * 1024, # 10 MB 'backupCount': 20, 'formatter': 'billing', }, 'payment_file': { 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(BILLING_LOG_DIR, 'payments.log'), 'maxBytes': 10 * 1024 * 1024, # 10 MB 'backupCount': 20, 'formatter': 'billing', }, }, 'loggers': { 'publish_sync': { 'handlers': ['console', 'publish_sync_file'], 'level': 'INFO', 'propagate': False, }, 'wordpress_api': { 'handlers': ['console', 'wordpress_api_file'], 'level': 'INFO', 'propagate': False, }, 'webhooks': { 'handlers': ['console', 'webhook_file'], 'level': 'INFO', 'propagate': False, }, 'billing': { 'handlers': ['console', 'billing_file'], 'level': 'INFO', 'propagate': False, }, 'payments': { 'handlers': ['console', 'payment_file'], 'level': 'INFO', 'propagate': False, }, 'auth.middleware': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False, }, 'container.lifecycle': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False, }, }, } # Celery Results Backend CELERY_RESULT_BACKEND = 'django-db' CELERY_CACHE_BACKEND = 'django-cache' CELERY_RESULT_EXTENDED = True # Store task name, args, kwargs in results # Import/Export Settings IMPORT_EXPORT_USE_TRANSACTIONS = True # ============================================================================== # UNFOLD ADMIN CONFIGURATION # ============================================================================== # Modern Django admin theme with advanced features # Documentation: https://unfoldadmin.com/ UNFOLD = { "SITE_TITLE": "IGNY8 Administration", "SITE_HEADER": "", # Empty to hide text, logo will be shown instead "SITE_URL": "/", "SITE_LOGO": lambda request: "/static/admin/img/logo.png", "SITE_SYMBOL": "rocket_launch", # Symbol from Material icons "SHOW_HISTORY": True, # Show history for models with simple_history "SHOW_VIEW_ON_SITE": True, # Show "View on site" button "COLORS": { "primary": { "50": "248 250 252", "100": "241 245 249", "200": "226 232 240", "300": "203 213 225", "400": "148 163 184", "500": "100 116 139", "600": "71 85 105", "700": "51 65 85", "800": "30 41 59", "900": "15 23 42", "950": "2 6 23", }, }, "SIDEBAR": { "show_search": True, "show_all_applications": False, "navigation": [ # Dashboard & Reports { "title": "Dashboard & Reports", "icon": "dashboard", "collapsible": True, "items": [ {"title": "Dashboard", "icon": "home", "link": lambda request: "/admin/dashboard/"}, {"title": "Revenue Report", "icon": "attach_money", "link": lambda request: "/admin/reports/revenue/"}, {"title": "Usage Report", "icon": "data_usage", "link": lambda request: "/admin/reports/usage/"}, {"title": "Content Report", "icon": "article", "link": lambda request: "/admin/reports/content/"}, {"title": "Data Quality", "icon": "verified", "link": lambda request: "/admin/reports/data-quality/"}, {"title": "Token Usage", "icon": "token", "link": lambda request: "/admin/reports/token-usage/"}, {"title": "AI Cost Analysis", "icon": "psychology", "link": lambda request: "/admin/reports/ai-cost-analysis/"}, ], }, # Accounts & Users { "title": "Accounts & Users", "icon": "group", "collapsible": True, "items": [ {"title": "Accounts", "icon": "business", "link": lambda request: "/admin/igny8_core_auth/account/"}, {"title": "Users", "icon": "person", "link": lambda request: "/admin/igny8_core_auth/user/"}, {"title": "Sites", "icon": "language", "link": lambda request: "/admin/igny8_core_auth/site/"}, {"title": "Sectors", "icon": "category", "link": lambda request: "/admin/igny8_core_auth/sector/"}, {"title": "Site Access", "icon": "lock", "link": lambda request: "/admin/igny8_core_auth/siteuseraccess/"}, ], }, # Plans & Billing { "title": "Plans & Billing", "icon": "payments", "collapsible": True, "items": [ {"title": "Plans", "icon": "workspace_premium", "link": lambda request: "/admin/igny8_core_auth/plan/"}, {"title": "Subscriptions", "icon": "subscriptions", "link": lambda request: "/admin/igny8_core_auth/subscription/"}, {"title": "Invoices", "icon": "receipt_long", "link": lambda request: "/admin/billing/invoice/"}, {"title": "Payments", "icon": "paid", "link": lambda request: "/admin/billing/payment/"}, {"title": "Credit Packages", "icon": "card_giftcard", "link": lambda request: "/admin/billing/creditpackage/"}, {"title": "Payment Methods (Global)", "icon": "credit_card", "link": lambda request: "/admin/billing/paymentmethodconfig/"}, {"title": "Account Payment Methods", "icon": "account_balance_wallet", "link": lambda request: "/admin/billing/accountpaymentmethod/"}, {"title": "Payment Logs", "icon": "receipt", "link": lambda request: "/admin/billing/webhookevent/"}, ], }, # Credits & AI Usage (CONSOLIDATED) { "title": "Credits & AI Usage", "icon": "toll", "collapsible": True, "items": [ {"title": "Credit Transactions", "icon": "swap_horiz", "link": lambda request: "/admin/billing/credittransaction/"}, {"title": "Credit Usage Log", "icon": "history", "link": lambda request: "/admin/billing/creditusagelog/"}, {"title": "AI Task Logs", "icon": "smart_toy", "link": lambda request: "/admin/ai/aitasklog/"}, {"title": "Plan Limits", "icon": "speed", "link": lambda request: "/admin/billing/planlimitusage/"}, ], }, # Content Pipeline (RENAMED from Planning + Writing) { "title": "Content Pipeline", "icon": "edit_note", "collapsible": True, "items": [ {"title": "Keywords", "icon": "key", "link": lambda request: "/admin/planner/keywords/"}, {"title": "Clusters", "icon": "hub", "link": lambda request: "/admin/planner/clusters/"}, {"title": "Content Ideas", "icon": "lightbulb", "link": lambda request: "/admin/planner/contentideas/"}, {"title": "Tasks", "icon": "task_alt", "link": lambda request: "/admin/writer/tasks/"}, {"title": "Content", "icon": "description", "link": lambda request: "/admin/writer/content/"}, {"title": "Images", "icon": "image", "link": lambda request: "/admin/writer/images/"}, {"title": "Image Prompts", "icon": "auto_awesome", "link": lambda request: "/admin/writer/imageprompts/"}, ], }, # Taxonomy { "title": "Taxonomy", "icon": "label", "collapsible": True, "items": [ {"title": "Taxonomies", "icon": "sell", "link": lambda request: "/admin/writer/contenttaxonomy/"}, {"title": "Relations", "icon": "link", "link": lambda request: "/admin/writer/contenttaxonomyrelation/"}, {"title": "Attributes", "icon": "tune", "link": lambda request: "/admin/writer/contentattribute/"}, {"title": "Cluster Maps", "icon": "account_tree", "link": lambda request: "/admin/writer/contentclustermap/"}, ], }, # Publishing { "title": "Publishing", "icon": "publish", "collapsible": True, "items": [ {"title": "Publishing Records", "icon": "cloud_upload", "link": lambda request: "/admin/publishing/publishingrecord/"}, {"title": "Deployments", "icon": "rocket", "link": lambda request: "/admin/publishing/deploymentrecord/"}, {"title": "Sync Events", "icon": "sync", "link": lambda request: "/admin/integration/syncevent/"}, {"title": "Publishing Settings", "icon": "schedule", "link": lambda request: "/admin/integration/publishingsettings/"}, ], }, # Automation (NEW SECTION) { "title": "Automation", "icon": "settings_suggest", "collapsible": True, "items": [ {"title": "Default Config", "icon": "settings", "link": lambda request: "/admin/automation/defaultautomationconfig/"}, {"title": "Automation Configs", "icon": "tune", "link": lambda request: "/admin/automation/automationconfig/"}, {"title": "Automation Runs", "icon": "play_circle", "link": lambda request: "/admin/automation/automationrun/"}, ], }, # AI Configuration (SIMPLIFIED) { "title": "AI Configuration", "icon": "psychology", "collapsible": True, "items": [ {"title": "AI Models (Testing/Live)", "icon": "model_training", "link": lambda request: "/admin/billing/aimodelconfig/"}, {"title": "System AI Settings", "icon": "tune", "link": lambda request: "/admin/system/systemaisettings/"}, {"title": "Integration Providers", "icon": "key", "link": lambda request: "/admin/system/integrationprovider/"}, ], }, # Plugin Management { "title": "Plugin Management", "icon": "extension", "collapsible": True, "items": [ {"title": "Plugins", "icon": "apps", "link": lambda request: "/admin/plugins/plugin/"}, {"title": "Plugin Versions", "icon": "new_releases", "link": lambda request: "/admin/plugins/pluginversion/"}, {"title": "Installations", "icon": "cloud_download", "link": lambda request: "/admin/plugins/plugininstallation/"}, {"title": "Downloads", "icon": "download", "link": lambda request: "/admin/plugins/plugindownload/"}, ], }, # Email Settings { "title": "Email Settings", "icon": "email", "collapsible": True, "items": [ {"title": "Email Configuration", "icon": "settings", "link": lambda request: "/admin/system/emailsettings/"}, {"title": "Email Templates", "icon": "article", "link": lambda request: "/admin/system/emailtemplate/"}, {"title": "Email Logs", "icon": "history", "link": lambda request: "/admin/system/emaillog/"}, ], }, # Global Settings (SIMPLIFIED) { "title": "Global Settings", "icon": "settings", "collapsible": True, "items": [ {"title": "Global AI Prompts", "icon": "chat", "link": lambda request: "/admin/system/globalaiprompt/"}, {"title": "Module Settings", "icon": "view_module", "link": lambda request: "/admin/system/globalmodulesettings/"}, {"title": "Author Profiles", "icon": "person_outline", "link": lambda request: "/admin/system/globalauthorprofile/"}, {"title": "Strategies", "icon": "strategy", "link": lambda request: "/admin/system/globalstrategy/"}, ], }, # Account & User Settings (CONSOLIDATED) { "title": "Account & User Settings", "icon": "tune", "collapsible": True, "items": [ {"title": "Account Settings", "icon": "account_circle", "link": lambda request: "/admin/system/accountsettings/"}, {"title": "User Settings", "icon": "person_search", "link": lambda request: "/admin/system/usersettings/"}, {"title": "Module Enable Settings", "icon": "view_module", "link": lambda request: "/admin/system/modulesettings/"}, ], }, # Resources { "title": "Resources", "icon": "inventory_2", "collapsible": True, "items": [ {"title": "Industries", "icon": "factory", "link": lambda request: "/admin/igny8_core_auth/industry/"}, {"title": "Industry Sectors", "icon": "domain", "link": lambda request: "/admin/igny8_core_auth/industrysector/"}, {"title": "Keywords Library", "icon": "eco", "link": lambda request: "/admin/igny8_core_auth/seedkeyword/"}, ], }, # Trash (Soft-Deleted Records) { "title": "Trash", "icon": "delete", "collapsible": True, "items": [ {"title": "Accounts (Trash)", "icon": "business", "link": lambda request: "/admin/igny8_core_auth/accounttrash/"}, {"title": "Sites (Trash)", "icon": "language", "link": lambda request: "/admin/igny8_core_auth/sitetrash/"}, {"title": "Sectors (Trash)", "icon": "category", "link": lambda request: "/admin/igny8_core_auth/sectortrash/"}, {"title": "Content (Trash)", "icon": "description", "link": lambda request: "/admin/writer/contenttrash/"}, {"title": "Tasks (Trash)", "icon": "task_alt", "link": lambda request: "/admin/writer/taskstrash/"}, {"title": "Keywords (Trash)", "icon": "key", "link": lambda request: "/admin/planner/keywordstrash/"}, {"title": "Clusters (Trash)", "icon": "hub", "link": lambda request: "/admin/planner/clusterstrash/"}, {"title": "Images (Trash)", "icon": "image", "link": lambda request: "/admin/writer/imagestrash/"}, {"title": "Content Ideas (Trash)", "icon": "lightbulb", "link": lambda request: "/admin/planner/contentideastrash/"}, ], }, # Logs & Monitoring { "title": "Logs & Monitoring", "icon": "monitor_heart", "collapsible": True, "items": [ {"title": "System Health", "icon": "health_and_safety", "link": lambda request: "/admin/monitoring/system-health/"}, {"title": "API Monitor", "icon": "api", "link": lambda request: "/admin/monitoring/api-monitor/"}, {"title": "Debug Console", "icon": "terminal", "link": lambda request: "/admin/monitoring/debug-console/"}, {"title": "Celery Tasks", "icon": "schedule", "link": lambda request: "/admin/django_celery_results/taskresult/"}, {"title": "Admin Log", "icon": "history", "link": lambda request: "/admin/admin/logentry/"}, ], }, # Django Admin { "title": "Django Admin", "icon": "admin_panel_settings", "collapsible": True, "items": [ {"title": "Groups", "icon": "groups", "link": lambda request: "/admin/auth/group/"}, {"title": "Permissions", "icon": "security", "link": lambda request: "/admin/auth/permission/"}, {"title": "Content Types", "icon": "dns", "link": lambda request: "/admin/contenttypes/contenttype/"}, {"title": "Sessions", "icon": "badge", "link": lambda request: "/admin/sessions/session/"}, ], }, ], }, } # Billing / Payments configuration STRIPE_PUBLIC_KEY = os.getenv('STRIPE_PUBLIC_KEY', '') STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '') STRIPE_WEBHOOK_SECRET = os.getenv('STRIPE_WEBHOOK_SECRET', '') PAYPAL_CLIENT_ID = os.getenv('PAYPAL_CLIENT_ID', '') PAYPAL_CLIENT_SECRET = os.getenv('PAYPAL_CLIENT_SECRET', '') PAYPAL_API_BASE = os.getenv('PAYPAL_API_BASE', 'https://api-m.sandbox.paypal.com') # Frontend URL for redirects (Stripe/PayPal success/cancel URLs) FRONTEND_URL = os.getenv('FRONTEND_URL', 'https://app.igny8.com')