""" 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' USE_SITE_BUILDER_REFACTOR = os.getenv('USE_SITE_BUILDER_REFACTOR', '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 = [ # 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.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', ] # 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 # 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 - allows external redirects SESSION_COOKIE_AGE = 1209600 # 14 days (2 weeks) SESSION_SAVE_EVERY_REQUEST = True # Enable sliding window - extends session on activity SESSION_COOKIE_PATH = '/' # Explicit path # Don't set SESSION_COOKIE_DOMAIN - let it default to current domain for strict isolation # CRITICAL: Use Redis for session storage (not database) # Provides better performance and automatic expiry SESSION_ENGINE = 'django.contrib.sessions.backends.cache' SESSION_CACHE_ALIAS = 'default' # Configure Redis cache for sessions CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': f"redis://{os.getenv('REDIS_HOST', 'redis')}:{os.getenv('REDIS_PORT', '6379')}/1", 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 'SOCKET_CONNECT_TIMEOUT': 5, 'SOCKET_TIMEOUT': 5, 'CONNECTION_POOL_KWARGS': { 'max_connections': 50, 'retry_on_timeout': True } } } } # 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' JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1) # Increased from 15min to 1 hour 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') os.makedirs(PUBLISH_SYNC_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', }, }, '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', }, }, '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, }, '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' # 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": "IGNY8 Admin", "SITE_URL": "/", "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", }, }, "EXTENSIONS": { "modeltranslation": { "flags": { "en": "🇬🇧", "fr": "🇫🇷", }, }, }, "SIDEBAR": { "show_search": True, "show_all_applications": False, # MUST be False - we provide custom sidebar_navigation }, } # 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')