""" Django settings для Uchill - образовательной платформы. """ import os from pathlib import Path from datetime import timedelta import dj_database_url # Build paths inside the project BASE_DIR = Path(__file__).resolve().parent.parent # ============================================== # SENTRY (Мониторинг ошибок) - ОТКЛЮЧЕН # ============================================== # ПРИМЕЧАНИЕ: Sentry отключен для экономии ресурсов сервера # Для включения: установить SENTRY_DSN в .env.production # # Варианты: # 1. Sentry.io (облачный) - бесплатный план, 5,000 событий/месяц # 2. Self-hosted Sentry - требует 4GB RAM (см. SENTRY_SETUP.md) # # Настройка Sentry для production (когда будет готово) if os.getenv('SENTRY_DSN') and os.getenv('DEBUG', 'True') == 'False': try: import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.redis import RedisIntegration sentry_sdk.init( dsn=os.getenv('SENTRY_DSN'), integrations=[ DjangoIntegration(), CeleryIntegration(), RedisIntegration(), ], # Процент транзакций для отслеживания производительности traces_sample_rate=float(os.getenv('SENTRY_TRACES_SAMPLE_RATE', '0.1')), # Отправка личных данных (PII) - только для production send_default_pii=True, # Окружение environment=os.getenv('SENTRY_ENVIRONMENT', 'production'), # Релиз (версия приложения) release=os.getenv('SENTRY_RELEASE', 'uchill@1.0.0'), # Игнорировать определенные ошибки ignore_errors=[ KeyboardInterrupt, 'django.http.response.Http404', ], ) except ImportError: # Sentry SDK не установлен - это нормально, если не используется pass # ============================================== # БЕЗОПАСНОСТЬ # ============================================== # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-CHANGE-THIS-IN-PRODUCTION') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', 'False') == 'True' ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') # Production настройки безопасности if not DEBUG: # HTTPS настройки SECURE_SSL_REDIRECT = os.getenv('SECURE_SSL_REDIRECT', 'False') == 'True' SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = int(os.getenv('SECURE_HSTS_SECONDS', '31536000')) # 1 год SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_BROWSER_XSS_FILTER = True X_FRAME_OPTIONS = 'DENY' # Защита от clickjacking SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin' # Дополнительные настройки безопасности USE_TZ = True SECURE_CROSS_ORIGIN_OPENER_POLICY = 'same-origin' else: # В режиме разработки отключаем строгие настройки SECURE_SSL_REDIRECT = False SESSION_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False # ============================================== # ПРИЛОЖЕНИЯ # ============================================== INSTALLED_APPS = [ # Django по умолчанию 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third-party приложения 'rest_framework', 'rest_framework_simplejwt', 'corsheaders', 'django_filters', 'drf_yasg', # API документация (Swagger/OpenAPI) 'channels', 'celery', 'django_celery_beat', 'django_celery_results', 'rest_framework_simplejwt.token_blacklist', # Инструменты для профилирования (только в DEBUG режиме) # Добавляем только если модули установлены # *(['debug_toolbar'] if DEBUG else []), # Django Silk добавляем условно, только если установлен (проверка ниже) # *(['silk'] if DEBUG else []), # Наши приложения 'apps.core', # Системные операции (бэкапы, очистка) 'apps.users', 'apps.schedule', 'apps.video', 'apps.board', 'apps.homework', 'apps.materials', 'apps.notifications', 'apps.subscriptions', 'apps.analytics', 'apps.referrals', 'apps.chat', # Чат и сообщения ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', # раздача static при прямом обращении к Django (порт 8123) 'corsheaders.middleware.CorsMiddleware', # CORS должен быть перед CommonMiddleware 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', # Должен быть перед другими middleware, которые используют request.user 'apps.users.middleware.activity.UpdateLastActivityMiddleware', # Обновление last_activity (после AuthenticationMiddleware) 'apps.subscriptions.middleware.SubscriptionMiddleware', # Проверка подписки (после AuthenticationMiddleware) 'apps.users.middleware.mentor_student_access.MentorStudentAccessMiddleware', # Доступ ментор—студент только после подтверждения 'apps.users.middleware.email_verification.EmailVerificationMiddleware', # Проверка подтверждения email 'django.contrib.messages.middleware.MessageMiddleware', # Отключаем XFrameOptionsMiddleware для работы Telegram Login Widget # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] # Инструменты профилирования (Silk, Debug Toolbar) отключены — # при включении нужно раскомментировать их в INSTALLED_APPS выше и этот блок # if DEBUG: # try: # import silk # MIDDLEWARE = ['silk.middleware.SilkyMiddleware'] + MIDDLEWARE # except ImportError: # if 'silk' in INSTALLED_APPS: # INSTALLED_APPS.remove('silk') # pass # try: # import debug_toolbar # MIDDLEWARE = MIDDLEWARE + ['debug_toolbar.middleware.DebugToolbarMiddleware'] # except ImportError: # pass ROOT_URLCONF = 'config.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / '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 = 'config.wsgi.application' ASGI_APPLICATION = 'config.asgi.application' # ============================================== # БАЗА ДАННЫХ # ============================================== DATABASES = { 'default': dj_database_url.config( default=f"postgresql://{os.getenv('POSTGRES_USER', 'platform_user')}:" f"{os.getenv('POSTGRES_PASSWORD', 'platform_password')}@" f"{os.getenv('POSTGRES_HOST', 'db')}:" f"{os.getenv('POSTGRES_PORT', '5432')}/" f"{os.getenv('POSTGRES_DB', 'platform_db')}", conn_max_age=600 ) } # ============================================== # КЭШИРОВАНИЕ (REDIS) # ============================================== REDIS_URL = os.getenv('REDIS_URL', 'redis://redis:6379/0') # Парсинг Redis URL для channels_redis def parse_redis_url(url): """Парсит Redis URL в формат для channels_redis.""" from urllib.parse import urlparse parsed = urlparse(url) host = parsed.hostname or 'redis' port = parsed.port or 6379 db = int(parsed.path.lstrip('/')) if parsed.path else 0 # Формируем конфигурацию config = {'db': db} # Если есть пароль в URL (формат: redis://:password@host:port/db) if parsed.password: config['password'] = parsed.password return (host, port, config) # Получаем Redis URL для channels (используем базу 0, как для кеша) REDIS_CHANNELS_URL = os.getenv('REDIS_URL', 'redis://redis:6379/0') CACHES = { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': REDIS_URL, 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 'SOCKET_CONNECT_TIMEOUT': 5, 'SOCKET_TIMEOUT': 5, 'CONNECTION_POOL_KWARGS': {'max_connections': 50} }, 'KEY_PREFIX': 'platform', 'TIMEOUT': 300, } } # ============================================== # CHANNELS (WEBSOCKET) # ============================================== CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [parse_redis_url(REDIS_CHANNELS_URL)], 'capacity': 50000, # Увеличено для больших сообщений (до 50 MB) 'expiry': 180, # Увеличено время жизни сообщений до 3 минут }, # Дополнительные настройки для больших сообщений 'SYMMETRIC_ENCRYPTION_KEYS': [], # Отключаем шифрование для скорости }, } # ============================================== # CELERY # ============================================== CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://redis:6379/1') CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://redis:6379/2') CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = 'UTC' CELERY_TASK_TRACK_STARTED = True CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 минут # ============================================== # ВАЛИДАЦИЯ ПАРОЛЕЙ # ============================================== 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'}, ] # ============================================== # AUTH USER MODEL # ============================================== AUTH_USER_MODEL = 'users.User' # ============================================== # JWT AUTHENTICATION # ============================================== from config.jwt_settings import SIMPLE_JWT # noqa # Email settings (для восстановления пароля и верификации) DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'noreply@platform.com') FRONTEND_URL = os.getenv('FRONTEND_URL', 'http://127.0.0.1:3000') # ЮKassa настройки YOOKASSA_SHOP_ID = os.getenv('YOOKASSA_SHOP_ID', '') YOOKASSA_SECRET_KEY = os.getenv('YOOKASSA_SECRET_KEY', '') # LiveKit настройки (официальный Go-сервер https://github.com/livekit/livekit) # Внутренний URL для бэкенда (Docker: livekit:7880) LIVEKIT_URL = os.getenv('LIVEKIT_URL', 'ws://livekit:7880') # Публичный URL для фронтенда: через наш сервис (nginx proxy) # Prod: wss://yourdomain.com/livekit Dev без nginx: ws://127.0.0.1:7880 LIVEKIT_PUBLIC_URL = os.getenv('LIVEKIT_PUBLIC_URL', '') # API ключ и секрет из livekit-config.yaml # Формат: APIKeyPlatform2024Secret: ThisIsAVerySecureSecretKeyForPlatform2024VideoConf # Используем значения по умолчанию, если переменные окружения не установлены или пустые livekit_api_key = os.getenv('LIVEKIT_API_KEY', '').strip() LIVEKIT_API_KEY = livekit_api_key if livekit_api_key else 'APIKeyPlatform2024Secret' livekit_api_secret = os.getenv('LIVEKIT_API_SECRET', '').strip() LIVEKIT_API_SECRET = livekit_api_secret if livekit_api_secret else 'ThisIsAVerySecureSecretKeyForPlatform2024VideoConf' LIVEKIT_ICE_SERVERS = os.getenv('LIVEKIT_ICE_SERVERS', None) # JSON строка с массивом ICE серверов # ============================================== # ИНТЕРНАЦИОНАЛИЗАЦИЯ # ============================================== LANGUAGE_CODE = 'ru-ru' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True # ============================================== # СТАТИЧЕСКИЕ ФАЙЛЫ # ============================================== # Важно: РАЗДАЁМ ИЗ STATICFILES (не из папки static). # - static/ = исходники (STATICFILES_DIRS), откуда collectstatic собирает # - staticfiles = сюда collectstatic собирает; отсюда Django и nginx отдают по URL /static/ STATIC_URL = (os.getenv('STATIC_URL') or '/static/').rstrip('/') + '/' STATIC_ROOT = BASE_DIR / 'staticfiles' # сюда collectstatic пишет; это отдаём STATICFILES_DIRS = [BASE_DIR / 'static'] if (BASE_DIR / 'static').exists() else [] # откуда собираем # ============================================== # МЕДИА ФАЙЛЫ # ============================================== # MEDIA_ROOT = папка, откуда отдаём загрузки (то же имя в volume и в nginx alias) MEDIA_URL = (os.getenv('MEDIA_URL') or '/media/').rstrip('/') + '/' MEDIA_ROOT = BASE_DIR / 'media' # сюда пишутся загрузки; это отдаём по /media/ # Максимальный размер загрузки (для проверок в приложении) DATA_UPLOAD_MAX_MEMORY_SIZE = int(os.getenv('DATA_UPLOAD_MAX_MEMORY_SIZE', '10485760')) # 10 MB default FILE_UPLOAD_MAX_MEMORY_SIZE = int(os.getenv('FILE_UPLOAD_MAX_MEMORY_SIZE', '10485760')) # 10 MB default # ============================================== # REST FRAMEWORK # ============================================== REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.SearchFilter', 'rest_framework.filters.OrderingFilter', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20, 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', ], 'DEFAULT_PARSER_CLASSES': [ 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.MultiPartParser', 'rest_framework.parsers.FormParser', ], 'EXCEPTION_HANDLER': 'config.exceptions.custom_exception_handler', # Rate Limiting (защита от перегрузки API) 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.throttling.AnonRateThrottle', 'rest_framework.throttling.UserRateThrottle', ], 'DEFAULT_THROTTLE_RATES': { 'anon': '200/hour', # Для неавторизованных пользователей 'user': '5000/hour', # Для авторизованных пользователей 'burst': '60/minute', # Для критичных endpoints (login, register) 'upload': '60/hour', # Для загрузки файлов }, } # ============================================== # JWT # ============================================== SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(hours=3), # 3 часа - достаточно для длинных уроков 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, 'UPDATE_LAST_LOGIN': True, 'ALGORITHM': 'HS256', 'SIGNING_KEY': SECRET_KEY, 'AUTH_HEADER_TYPES': ('Bearer',), 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), } # ============================================== # CORS # ============================================== # CORS настройки cors_origins = os.getenv( 'CORS_ALLOWED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:3001,http://127.0.0.1:3001,http://app.localhost' ) CORS_ALLOWED_ORIGINS = [origin.strip() for origin in cors_origins.split(',') if origin.strip()] # В режиме разработки разрешаем все origins для упрощения отладки if DEBUG: CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True else: CORS_ALLOW_ALL_ORIGINS = False # В production разрешаем только указанные origins CORS_ALLOW_CREDENTIALS = True # Дополнительная защита в production CORS_PREFLIGHT_MAX_AGE = 86400 # 24 часа CORS_ALLOW_HEADERS = [ 'accept', 'accept-encoding', 'authorization', 'content-type', 'dnt', 'origin', 'user-agent', 'x-csrftoken', 'x-requested-with', ] CORS_EXPOSE_HEADERS = ['content-type', 'authorization'] # CORS методы CORS_ALLOW_METHODS = [ 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', ] # ============================================== # CSRF # ============================================== csrf_origins = os.getenv( 'CSRF_TRUSTED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:3001,http://127.0.0.1:3001,http://app.localhost' ) CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in csrf_origins.split(',') if origin.strip()] # Дополнительные настройки CSRF CSRF_COOKIE_HTTPONLY = False # Нужно для JavaScript доступа CSRF_COOKIE_SAMESITE = 'Lax' # Защита от CSRF атак CSRF_USE_SESSIONS = False # Используем cookies CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure' # Кастомный view для ошибок CSRF # ============================================== # EMAIL # ============================================== # В режиме разработки используем консольный backend (письма выводятся в консоль) # Для продакшн установите EMAIL_BACKEND=smtp в .env email_backend = os.getenv('EMAIL_BACKEND', '') if email_backend == 'smtp': EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' elif email_backend == 'console': EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' elif email_backend: # Если указан полный путь к модулю EMAIL_BACKEND = email_backend else: # По умолчанию: консольный в режиме разработки, SMTP в продакшн EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' if DEBUG else 'django.core.mail.backends.smtp.EmailBackend' # SMTP настройки (используются только если EMAIL_BACKEND=smtp) EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.gmail.com') EMAIL_PORT = int(os.getenv('EMAIL_PORT', '2525')) EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True') == 'True' EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL', 'False') == 'True' # Для порта 465 EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') _default_from = os.getenv('DEFAULT_FROM_EMAIL', '').strip() # Для Mail.ru и др.: From должен совпадать с ящиком SMTP, иначе письма не доходят или уходят в спам if EMAIL_HOST_USER and (not _default_from or _default_from == 'noreply@platform.com'): DEFAULT_FROM_EMAIL = EMAIL_HOST_USER else: DEFAULT_FROM_EMAIL = _default_from or 'noreply@platform.com' # Таймауты для отправки email EMAIL_TIMEOUT = int(os.getenv('EMAIL_TIMEOUT', '10')) # ============================================== # SFU SERVER (ion-sfu) # ============================================== SFU_SERVER_URL = os.getenv('SFU_SERVER_URL', 'http://sfu-server:7001') SFU_WS_URL = os.getenv('SFU_WS_URL', 'ws://sfu-server:7001') # WebSocket работает на том же порту что и HTTP # Janus Gateway # Для Django контейнера используем имя контейнера, для клиента - localhost JANUS_HTTP_URL = os.getenv('JANUS_HTTP_URL', 'http://platform-janus:8088/janus') JANUS_WS_URL = os.getenv('JANUS_WS_URL', 'ws://platform-janus:8188') # Для клиентов (браузер) используем localhost JANUS_CLIENT_HTTP_URL = os.getenv('JANUS_CLIENT_HTTP_URL', 'http://localhost:8088/janus') JANUS_CLIENT_WS_URL = os.getenv('JANUS_CLIENT_WS_URL', 'ws://localhost:8188') JANUS_PUBLIC_IP = os.getenv('JANUS_PUBLIC_IP', '127.0.0.1') JANUS_ROOM_SECRET = os.getenv('JANUS_ROOM_SECRET', 'adminpwd') # Секрет для управления комнатами # LiveKit удален - не используется # Выбор SFU по умолчанию (ion-sfu или janus) DEFAULT_SFU_TYPE = os.getenv('DEFAULT_SFU_TYPE', 'ion-sfu') SFU_CLIENT_TIMEOUT = int(os.getenv('SFU_CLIENT_TIMEOUT', '10')) # ============================================== # ЛОГИРОВАНИЕ # ============================================== LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO' if not DEBUG else 'DEBUG') LOG_DIR = BASE_DIR / 'logs' LOG_DIR.mkdir(exist_ok=True) LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', 'style': '{', 'datefmt': '%Y-%m-%d %H:%M:%S', }, 'simple': { 'format': '{levelname} {message}', 'style': '{', }, 'json': { 'format': '{"time": "%(asctime)s", "name": "%(name)s", "level": "%(levelname)s", "message": "%(message)s", "module": "%(module)s", "pathname": "%(pathname)s", "lineno": %(lineno)d}', 'datefmt': '%Y-%m-%d %H:%M:%S', }, }, 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse', }, 'require_debug_true': { '()': 'django.utils.log.RequireDebugTrue', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'simple' if DEBUG else 'verbose', 'level': LOG_LEVEL, }, 'file': { 'class': 'logging.handlers.RotatingFileHandler', 'filename': str(LOG_DIR / 'django.log'), 'maxBytes': 1024 * 1024 * 10, # 10 MB 'backupCount': 10, 'formatter': 'verbose', 'level': LOG_LEVEL, }, 'error_file': { 'class': 'logging.handlers.RotatingFileHandler', 'filename': str(LOG_DIR / 'django_errors.log'), 'maxBytes': 1024 * 1024 * 10, # 10 MB 'backupCount': 10, 'formatter': 'verbose', 'level': 'ERROR', }, 'celery_file': { 'class': 'logging.handlers.RotatingFileHandler', 'filename': str(LOG_DIR / 'celery.log'), 'maxBytes': 1024 * 1024 * 10, # 10 MB 'backupCount': 10, 'formatter': 'verbose', 'level': LOG_LEVEL, }, 'mail_admins': { 'class': 'django.utils.log.AdminEmailHandler', 'level': 'ERROR', 'filters': ['require_debug_false'], 'formatter': 'verbose', }, }, 'root': { 'handlers': ['console', 'file', 'error_file'], 'level': LOG_LEVEL, }, 'loggers': { 'django': { 'handlers': ['console', 'file', 'error_file'], 'level': LOG_LEVEL, 'propagate': False, }, 'django.request': { 'handlers': ['error_file', 'mail_admins'], 'level': 'ERROR', 'propagate': False, }, 'django.server': { 'handlers': ['file', 'error_file'], 'level': LOG_LEVEL, 'propagate': False, }, 'django.db.backends': { 'handlers': ['file'], 'level': 'WARNING', # Логируем только предупреждения и ошибки SQL 'propagate': False, }, 'celery': { 'handlers': ['console', 'celery_file'], 'level': LOG_LEVEL, 'propagate': False, }, 'apps': { 'handlers': ['console', 'file', 'error_file'], 'level': LOG_LEVEL, 'propagate': False, }, 'channels': { 'handlers': ['console', 'file'], 'level': LOG_LEVEL, 'propagate': False, }, }, } # В production добавляем JSON логирование для интеграции с системами мониторинга if not DEBUG and os.getenv('JSON_LOGGING', 'False') == 'True': LOGGING['handlers']['json_file'] = { 'class': 'logging.handlers.RotatingFileHandler', 'filename': str(LOG_DIR / 'django_json.log'), 'maxBytes': 1024 * 1024 * 10, # 10 MB 'backupCount': 10, 'formatter': 'json', 'level': LOG_LEVEL, } LOGGING['root']['handlers'].append('json_file') LOGGING['loggers']['django']['handlers'].append('json_file') # ============================================== # SWAGGER / OPENAPI # ============================================== SWAGGER_SETTINGS = { 'SECURITY_DEFINITIONS': { 'Bearer': { 'type': 'apiKey', 'name': 'Authorization', 'in': 'header' } }, 'USE_SESSION_AUTH': False, 'PERSIST_AUTH': True, } # ============================================== # ПРОЧИЕ НАСТРОЙКИ # ============================================== DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Пользовательская модель (будет добавлена позже) # AUTH_USER_MODEL = 'users.User' # Telegram Bot TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '') TELEGRAM_USE_WEBHOOK = os.getenv('TELEGRAM_USE_WEBHOOK', 'False') == 'True' TELEGRAM_WEBHOOK_URL = os.getenv('TELEGRAM_WEBHOOK_URL', '') TELEGRAM_WEBHOOK_SECRET_TOKEN = os.getenv('TELEGRAM_WEBHOOK_SECRET_TOKEN', '') # Настройки безопасности для Telegram Login Widget # Разрешаем встраивание в iframe (нужно для Telegram Login Widget) X_FRAME_OPTIONS = 'ALLOWALL' # OpenAI / ИИ для проверки ДЗ (агенты из БД: homework.HomeworkAIAgent) OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '') HOMEWORK_AI_API_KEY = os.getenv('HOMEWORK_AI_API_KEY', '') # Sentry SENTRY_DSN = os.getenv('SENTRY_DSN', '') if SENTRY_DSN: import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration sentry_sdk.init( dsn=SENTRY_DSN, integrations=[DjangoIntegration()], traces_sample_rate=0.1, send_default_pii=True, environment='development' if DEBUG else 'production', ) # ============================================== # ИНСТРУМЕНТЫ ПРОФИЛИРОВАНИЯ # ============================================== if DEBUG: # Django Debug Toolbar INTERNAL_IPS = [ '127.0.0.1', 'localhost', ] # Для Docker контейнеров import socket hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) INTERNAL_IPS += [ip[:-1] + '1' for ip in ips] DEBUG_TOOLBAR_CONFIG = { 'SHOW_TOOLBAR_CALLBACK': lambda request: DEBUG, 'SHOW_COLLAPSED': True, } # Django Silk (только если установлен) # Профилирование отключено для экономии места на диске try: import silk # Профилирование отключено - не создаем директорию для профилей # profiles_dir = BASE_DIR / 'profiles' # profiles_dir.mkdir(exist_ok=True) # Отключаем профилирование Python (не создает .prof файлы) SILKY_PYTHON_PROFILER = False SILKY_PYTHON_PROFILER_BINARY = False # SILKY_PYTHON_PROFILER_RESULT_PATH = str(profiles_dir) # Не нужен, т.к. профилирование отключено # Отключаем SILKY_META, так как он требует дополнительных настроек и может вызывать ошибки SILKY_META = False # Отключаем перехват запросов (0% = не перехватывать, не создавать файлы) SILKY_INTERCEPT_PERCENT = 0 # Отключено: не перехватывать запросы для профилирования # Настройки логирования (если понадобится включить обратно) SILKY_MAX_REQUEST_BODY_SIZE = 1024 # Максимальный размер тела запроса для логирования SILKY_MAX_RESPONSE_BODY_SIZE = 1024 # Максимальный размер тела ответа для логирования SILKY_IGNORE_PATHS = [ r'/admin', r'/static', r'/media', r'/__debug__', r'/silk', r'/health', ] except ImportError: # silk не установлен, пропускаем настройки pass