diff --git a/backend/api/client.py b/backend/api/client.py index 0df889b9..b937fdd6 100644 --- a/backend/api/client.py +++ b/backend/api/client.py @@ -17,8 +17,21 @@ class ApiConfig: login_url_name: str = "auth_front:login" # куда редиректить, если токены протухли -def load_config() -> ApiConfig: - base = getattr(settings, "API_BASE_URL", "http://localhost:8000/api") +def load_config(request=None) -> ApiConfig: + """ + Загружает конфигурацию API. + Если передан request, base_url формируется динамически на основе домена запроса. + Иначе используется настройка API_BASE_URL из settings. + """ + if request: + # Динамическое формирование base_url на основе текущего домена + scheme = 'https' if request.is_secure() else 'http' + host = request.get_host() + base = f"{scheme}://{host}/api" + else: + # Fallback на статическую настройку (для фоновых задач, команд и т.д.) + base = getattr(settings, "API_BASE_URL", "http://localhost:9000/api") + return ApiConfig( base_url=base.rstrip("/"), token_obtain_path=getattr(settings, "API_TOKEN_OBTAIN_PATH", "/auth/token/"), @@ -57,7 +70,7 @@ class ApiClient: def __init__(self, request, config: Optional[ApiConfig] = None): self.request = request - self.cfg = config or load_config() + self.cfg = config or load_config(request) self.session = requests.Session() self.access = request.session.get(SESSION_KEY_ACCESS) self.refresh = request.session.get(SESSION_KEY_REFRESH) diff --git a/backend/api/v1/employees/serializers.py b/backend/api/v1/employees/serializers.py index 1be5c8c1..781ec4eb 100644 --- a/backend/api/v1/employees/serializers.py +++ b/backend/api/v1/employees/serializers.py @@ -11,7 +11,6 @@ from employees.models import (Department, DepartmentPermission, DepartmentRole, from eusrr_backend.auth_backends import PHONE_FIELD as DETECTED_PHONE_FIELD from rest_framework import serializers -from ..serializers import Base64ImageField from employees.utils import _normalize_phone Employee = get_user_model() @@ -54,7 +53,7 @@ class RegisterSerializer(serializers.Serializer): whatsapp = serializers.CharField(required=False, allow_blank=True, default="") wechat = serializers.CharField(required=False, allow_blank=True, default="") - avatar = Base64ImageField(required=True) + avatar = serializers.ImageField(required=True) patronymic = serializers.CharField(required=False, allow_blank=True, default="") gender = serializers.ChoiceField( @@ -268,7 +267,7 @@ class EmployeeActionSerializer(serializers.ModelSerializer): class EmployeeSerializer(serializers.ModelSerializer): """Полная версия сотрудника для собственных эндпоинтов /employees/.""" - avatar = Base64ImageField(required=False, allow_null=True) + avatar = serializers.ImageField(required=False, allow_null=True) actions = EmployeeActionSerializer(many=True, read_only=True) # навыки: читаем красиво, пишем ids @@ -419,7 +418,7 @@ class EmployeeSerializer(serializers.ModelSerializer): class EmployeeBriefSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField() - avatar = Base64ImageField(read_only=True) + avatar = serializers.ImageField(read_only=True) full_name = serializers.SerializerMethodField() class Meta: @@ -492,7 +491,7 @@ class EmployeeListSerializer(serializers.ModelSerializer): display_name = serializers.SerializerMethodField() email = serializers.EmailField(read_only=True) phone_number = serializers.CharField(read_only=True) - avatar = Base64ImageField(read_only=True) + avatar = serializers.ImageField(read_only=True) position = PositionBriefSerializer(read_only=True) departments = serializers.SerializerMethodField() full_name = serializers.SerializerMethodField() diff --git a/backend/api/v1/feed/serializers.py b/backend/api/v1/feed/serializers.py index 383638d6..983d6f35 100644 --- a/backend/api/v1/feed/serializers.py +++ b/backend/api/v1/feed/serializers.py @@ -8,6 +8,7 @@ from rest_framework import serializers from feed.constants import TYPE_COMPANY, TYPE_DEPARTMENT, TYPE_EMPLOYEE from feed.models import Comment, Post, PostLike +from ..utils import build_media_url Employee = get_user_model() @@ -28,14 +29,8 @@ class AuthorMiniSerializer(serializers.ModelSerializer): def get_avatar(self, obj): """Возвращает полный URL для аватара""" - if not obj.avatar: - return None request = self.context.get('request') - if request and hasattr(obj.avatar, 'url'): - return request.build_absolute_uri(obj.avatar.url) - if hasattr(obj.avatar, 'url'): - return obj.avatar.url - return str(obj.avatar) + return build_media_url(obj.avatar, request) class CommentMiniSerializer(serializers.ModelSerializer): @@ -104,23 +99,13 @@ class PostListSerializer(serializers.ModelSerializer): def get_image(self, obj): """Возвращает полный URL для изображения""" - if not obj.image: - return None request = self.context.get('request') - if request and hasattr(obj.image, 'url'): - return request.build_absolute_uri(obj.image.url) - return obj.image.url if hasattr(obj.image, 'url') else str(obj.image) + return build_media_url(obj.image, request) def get_attachment(self, obj): """Возвращает полный URL для вложения""" - if not obj.attachment: - return None request = self.context.get('request') - if request and hasattr(obj.attachment, 'url'): - return request.build_absolute_uri(obj.attachment.url) - if hasattr(obj.attachment, 'url'): - return obj.attachment.url - return str(obj.attachment) + return build_media_url(obj.attachment, request) def get_last_comment(self, obj): # Берём готовую мапу из контекста, собранную во viewset.list() diff --git a/backend/api/v1/utils.py b/backend/api/v1/utils.py new file mode 100644 index 00000000..7b8a9e7f --- /dev/null +++ b/backend/api/v1/utils.py @@ -0,0 +1,64 @@ +""" +Утилиты для работы с медиа URL +""" +from django.conf import settings + + +def get_media_base_url(request=None): + """ + Возвращает базовый URL для медиа-файлов. + + Приоритет: + 1. Если USE_HTTPS=true и DOMAIN установлен → используем его + 2. Если есть request → используем домен из запроса + 3. Fallback → /media/ + """ + # Если в настройках указан предпочтительный домен + use_https = getattr(settings, 'USE_HTTPS', False) + domain = getattr(settings, 'DOMAIN', None) + + if domain and domain not in ['127.0.0.1:9000', 'localhost:9000']: + # Глобальный домен из настроек + scheme = 'https' if use_https else 'http' + return f"{scheme}://{domain}/media/" + + # Используем домен из запроса (для локальной разработки) + if request: + return request.build_absolute_uri('/media/') + + # Fallback + return '/media/' + + +def build_media_url(file_field, request=None): + """ + Формирует полный URL для медиа-файла. + + Args: + file_field: ImageField/FileField объект + request: HTTP запрос (опционально) + + Returns: + str: Полный URL к файлу + """ + if not file_field: + return None + + # Получаем относительный путь + if hasattr(file_field, 'url'): + relative_url = file_field.url + else: + relative_url = str(file_field) + + # Убираем /media/ если есть + if relative_url.startswith('/media/'): + relative_url = relative_url[7:] + elif relative_url.startswith('/'): + relative_url = relative_url[1:] + + # Формируем полный URL + base_url = get_media_base_url(request) + if not base_url.endswith('/'): + base_url += '/' + + return f"{base_url}{relative_url}" diff --git a/backend/core/context_processors.py b/backend/core/context_processors.py deleted file mode 100644 index a940542d..00000000 --- a/backend/core/context_processors.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Context processors для глобальных переменных в шаблонах -""" - - -def media_domain(request): - """ - Динамически определяет MEDIA_URL на основе текущего домена запроса. - - Это позволяет правильно формировать ссылки на медиа как для локального, - так и для глобального домена без перезапуска сервера. - """ - host = request.get_host() - scheme = request.scheme - - # Формируем базовый URL для текущего домена - base_url = f"{scheme}://{host}" - - # Динамический MEDIA_URL - dynamic_media_url = f"{base_url}/media/" - - return { - 'MEDIA_URL_DYNAMIC': dynamic_media_url, - 'CURRENT_DOMAIN': host, - 'CURRENT_SCHEME': scheme, - } diff --git a/backend/eusrr_backend/settings.py b/backend/eusrr_backend/settings.py index 18559dff..012d909a 100644 --- a/backend/eusrr_backend/settings.py +++ b/backend/eusrr_backend/settings.py @@ -80,7 +80,6 @@ TEMPLATES = [ "django.contrib.messages.context_processors.messages", "communications.context_processors.chat_unread_total", "eusrr_backend.context_processors.branding", - "core.context_processors.media_domain", ], }, }, @@ -157,13 +156,7 @@ PHONENUMBER_DEFAULT_REGION = "RU" # Медиа файлы MEDIA_ROOT = os.path.join(BASE_DIR, "media/") -# Используем полный URL для медиа на продакшене -USE_HTTPS = os.getenv("USE_HTTPS", "False").lower() == "true" -DOMAIN = os.getenv("DOMAIN", "127.0.0.1:9000") -if USE_HTTPS: - MEDIA_URL = f"https://{DOMAIN}/media/" -else: - MEDIA_URL = "/media/" # относительный URL для локальной разработки +MEDIA_URL = "/media/" # ----------------------------------------------------------------------------- # ЛОГИРОВАНИЕ @@ -294,7 +287,6 @@ SIMPLE_JWT = { # Автообновление JWT токенов: за сколько минут до истечения обновлять JWT_REFRESH_THRESHOLD_MINUTES = int(os.getenv("JWT_REFRESH_THRESHOLD_MIN", "5")) -API_BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:9000/api") # ----------------------------------------------------------------------------- # АУТЕНТИФИКАЦИОННЫЕ БЭКЕНДЫ