uchill/backend/config/settings.py

770 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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