""" Middleware для обработки авторизации в GraphQL запросах """ from starlette.datastructures import Headers from starlette.types import ASGIApp, Scope, Receive, Send from utils.logger import root_logger as logger from settings import SESSION_TOKEN_HEADER, SESSION_COOKIE_NAME class AuthMiddleware: """ Универсальный middleware для обработки авторизации и управления cookies. Основные функции: 1. Извлечение Bearer токена из заголовка Authorization или cookie 2. Добавление токена в заголовки запроса для обработки AuthenticationMiddleware 3. Предоставление методов для установки/удаления cookies в GraphQL резолверах """ def __init__(self, app: ASGIApp): self.app = app self._context = None async def __call__(self, scope: Scope, receive: Receive, send: Send): """Обработка ASGI запроса""" if scope["type"] != "http": await self.app(scope, receive, send) return # Извлекаем заголовки headers = Headers(scope=scope) token = None token_source = None # Сначала пробуем получить токен из заголовка авторизации auth_header = headers.get(SESSION_TOKEN_HEADER) if auth_header: if auth_header.startswith("Bearer "): token = auth_header.replace("Bearer ", "", 1).strip() token_source = "header" logger.debug( f"[middleware] Извлечен Bearer токен из заголовка {SESSION_TOKEN_HEADER}, длина: {len(token) if token else 0}" ) else: # Если заголовок не начинается с Bearer, предполагаем, что это чистый токен token = auth_header.strip() token_source = "header" logger.debug( f"[middleware] Извлечен прямой токен из заголовка {SESSION_TOKEN_HEADER}, длина: {len(token) if token else 0}" ) # Если токен не получен из основного заголовка и это не Authorization, проверяем заголовок Authorization if not token and SESSION_TOKEN_HEADER.lower() != "authorization": auth_header = headers.get("Authorization") if auth_header and auth_header.startswith("Bearer "): token = auth_header.replace("Bearer ", "", 1).strip() token_source = "auth_header" logger.debug( f"[middleware] Извлечен Bearer токен из заголовка Authorization, длина: {len(token) if token else 0}" ) # Если токен не получен из заголовка, пробуем взять из cookie if not token: cookies = headers.get("cookie", "") cookie_items = cookies.split(";") for item in cookie_items: if "=" in item: name, value = item.split("=", 1) if name.strip() == SESSION_COOKIE_NAME: token = value.strip() token_source = "cookie" logger.debug( f"[middleware] Извлечен токен из cookie {SESSION_COOKIE_NAME}, длина: {len(token) if token else 0}" ) break # Если токен получен, обновляем заголовки в scope if token: # Создаем новый список заголовков new_headers = [] for name, value in scope["headers"]: # Пропускаем оригинальный заголовок авторизации if name.decode("latin1").lower() != SESSION_TOKEN_HEADER.lower(): new_headers.append((name, value)) # Добавляем заголовок с чистым токеном new_headers.append((SESSION_TOKEN_HEADER.encode("latin1"), token.encode("latin1"))) # Обновляем заголовки в scope scope["headers"] = new_headers # Также добавляем информацию о типе аутентификации для дальнейшего использования scope["auth"] = { "type": "bearer", "token": token, "source": token_source } logger.debug(f"[middleware] Токен добавлен в scope для аутентификации из источника: {token_source}") else: logger.debug(f"[middleware] Токен не найден ни в заголовке, ни в cookie") await self.app(scope, receive, send) def set_context(self, context): """Сохраняет ссылку на контекст GraphQL запроса""" self._context = context logger.debug(f"[middleware] Установлен контекст GraphQL: {bool(context)}") def set_cookie(self, key, value, **options): """ Устанавливает cookie в ответе Args: key: Имя cookie value: Значение cookie **options: Дополнительные параметры (httponly, secure, max_age, etc.) """ success = False # Способ 1: Через response if self._context and "response" in self._context and hasattr(self._context["response"], "set_cookie"): try: self._context["response"].set_cookie(key, value, **options) logger.debug(f"[middleware] Установлена cookie {key} через response") success = True except Exception as e: logger.error(f"[middleware] Ошибка при установке cookie {key} через response: {str(e)}") # Способ 2: Через собственный response в контексте if not success and hasattr(self, "_response") and self._response and hasattr(self._response, "set_cookie"): try: self._response.set_cookie(key, value, **options) logger.debug(f"[middleware] Установлена cookie {key} через _response") success = True except Exception as e: logger.error(f"[middleware] Ошибка при установке cookie {key} через _response: {str(e)}") if not success: logger.error(f"[middleware] Не удалось установить cookie {key}: объекты response недоступны") def delete_cookie(self, key, **options): """ Удаляет cookie из ответа Args: key: Имя cookie для удаления **options: Дополнительные параметры """ success = False # Способ 1: Через response if self._context and "response" in self._context and hasattr(self._context["response"], "delete_cookie"): try: self._context["response"].delete_cookie(key, **options) logger.debug(f"[middleware] Удалена cookie {key} через response") success = True except Exception as e: logger.error(f"[middleware] Ошибка при удалении cookie {key} через response: {str(e)}") # Способ 2: Через собственный response в контексте if not success and hasattr(self, "_response") and self._response and hasattr(self._response, "delete_cookie"): try: self._response.delete_cookie(key, **options) logger.debug(f"[middleware] Удалена cookie {key} через _response") success = True except Exception as e: logger.error(f"[middleware] Ошибка при удалении cookie {key} через _response: {str(e)}") if not success: logger.error(f"[middleware] Не удалось удалить cookie {key}: объекты response недоступны") async def resolve(self, next, root, info, *args, **kwargs): """ Middleware для обработки запросов GraphQL. Добавляет методы для установки cookie в контекст. """ try: # Получаем доступ к контексту запроса context = info.context # Сохраняем ссылку на контекст self.set_context(context) # Добавляем себя как объект, содержащий утилитные методы context["extensions"] = self # Проверяем наличие response в контексте if "response" not in context or not context["response"]: from starlette.responses import JSONResponse context["response"] = JSONResponse({}) logger.debug("[middleware] Создан новый response объект в контексте GraphQL") logger.debug(f"[middleware] GraphQL resolve: контекст подготовлен, добавлены расширения для работы с cookie") return await next(root, info, *args, **kwargs) except Exception as e: logger.error(f"[AuthMiddleware] Ошибка в GraphQL resolve: {str(e)}") raise