770 lines
30 KiB
Python
770 lines
30 KiB
Python
"""
|
||
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': '100/hour', # Для неавторизованных пользователей
|
||
'user': '1000/hour', # Для авторизованных пользователей
|
||
'burst': '60/minute', # Для критичных endpoints (login, register)
|
||
'upload': '20/hour', # Для загрузки файлов
|
||
},
|
||
}
|
||
|
||
# ==============================================
|
||
# JWT
|
||
# ==============================================
|
||
|
||
SIMPLE_JWT = {
|
||
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
|
||
'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
|
||
|