Files
core/auth/middleware.py

527 lines
25 KiB
Python
Raw Normal View History

2025-05-16 09:23:48 +03:00
"""
2025-05-30 14:05:50 +03:00
Единый middleware для обработки авторизации в GraphQL запросах
2025-05-16 09:23:48 +03:00
"""
2025-05-29 12:37:39 +03:00
2025-07-31 18:55:59 +03:00
import json
2025-05-30 14:05:50 +03:00
import time
from collections.abc import Awaitable, MutableMapping
from typing import Any, Callable, Optional
2025-05-29 12:37:39 +03:00
from graphql import GraphQLResolveInfo
2025-05-30 14:08:29 +03:00
from sqlalchemy.orm import exc
2025-05-30 14:05:50 +03:00
from starlette.authentication import UnauthenticatedUser
2025-05-22 04:34:30 +03:00
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.types import ASGIApp
2025-05-29 12:37:39 +03:00
2025-05-30 14:05:50 +03:00
from auth.credentials import AuthCredentials
from auth.orm import Author
2025-06-02 21:50:58 +03:00
from auth.tokens.storage import TokenStorage as TokenManager
2025-05-30 14:05:50 +03:00
from services.db import local_session
2025-05-29 12:37:39 +03:00
from settings import (
2025-05-30 14:05:50 +03:00
ADMIN_EMAILS as ADMIN_EMAILS_LIST,
2025-05-30 14:08:29 +03:00
)
from settings import (
2025-05-29 12:37:39 +03:00
SESSION_COOKIE_HTTPONLY,
SESSION_COOKIE_MAX_AGE,
SESSION_COOKIE_NAME,
SESSION_COOKIE_SAMESITE,
SESSION_COOKIE_SECURE,
SESSION_TOKEN_HEADER,
)
2025-05-16 09:23:48 +03:00
from utils.logger import root_logger as logger
2025-05-30 14:05:50 +03:00
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
class AuthenticatedUser:
"""Аутентифицированный пользователь"""
2025-05-30 14:08:29 +03:00
def __init__(
self,
user_id: str,
username: str = "",
roles: Optional[list] = None,
permissions: Optional[dict] = None,
token: Optional[str] = None,
) -> None:
2025-05-30 14:05:50 +03:00
self.user_id = user_id
self.username = username
self.roles = roles or []
self.permissions = permissions or {}
self.token = token
@property
def is_authenticated(self) -> bool:
return True
@property
def display_name(self) -> str:
return self.username
@property
def identity(self) -> str:
return self.user_id
2025-05-16 09:23:48 +03:00
2025-05-19 11:25:41 +03:00
class AuthMiddleware:
2025-05-16 09:23:48 +03:00
"""
2025-05-30 14:05:50 +03:00
Единый middleware для обработки авторизации и аутентификации.
2025-05-29 12:37:39 +03:00
2025-05-19 11:25:41 +03:00
Основные функции:
1. Извлечение Bearer токена из заголовка Authorization или cookie
2025-06-02 21:50:58 +03:00
2. Проверка сессии через TokenStorage
2025-05-30 14:05:50 +03:00
3. Создание request.user и request.auth
4. Предоставление методов для установки/удаления cookies
2025-05-16 09:23:48 +03:00
"""
def __init__(self, app: ASGIApp) -> None:
2025-05-16 09:23:48 +03:00
self.app = app
2025-05-19 11:25:41 +03:00
self._context = None
2025-05-29 12:37:39 +03:00
async def authenticate_user(self, token: str) -> tuple[AuthCredentials, AuthenticatedUser | UnauthenticatedUser]:
2025-05-30 14:05:50 +03:00
"""Аутентифицирует пользователя по токену"""
if not token:
2025-06-28 13:56:05 +03:00
logger.debug("[auth.authenticate] Токен отсутствует")
return AuthCredentials(
author_id=None, scopes={}, logged_in=False, error_message="no token", email=None, token=None
), UnauthenticatedUser()
2025-05-30 14:05:50 +03:00
# Проверяем сессию в Redis
2025-06-28 13:56:05 +03:00
try:
payload = await TokenManager.verify_session(token)
if not payload:
logger.debug("[auth.authenticate] Недействительный токен или сессия не найдена")
return AuthCredentials(
author_id=None,
scopes={},
logged_in=False,
error_message="Invalid token or session",
email=None,
token=None,
), UnauthenticatedUser()
2025-05-30 14:05:50 +03:00
2025-06-28 13:56:05 +03:00
with local_session() as session:
try:
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
# payload может быть словарем или объектом, обрабатываем оба случая
user_id = payload.user_id if hasattr(payload, "user_id") else payload.get("user_id")
if not user_id:
logger.debug("[auth.authenticate] user_id не найден в payload")
return AuthCredentials(
author_id=None,
scopes={},
logged_in=False,
error_message="Invalid token payload",
email=None,
token=None,
), UnauthenticatedUser()
author = session.query(Author).where(Author.id == user_id).one()
2025-06-28 13:56:05 +03:00
if author.is_locked():
logger.debug(f"[auth.authenticate] Аккаунт заблокирован: {author.id}")
return AuthCredentials(
author_id=None,
scopes={},
logged_in=False,
error_message="Account is locked",
email=None,
token=None,
), UnauthenticatedUser()
2025-07-02 22:49:20 +03:00
# Создаем пустой словарь разрешений
# Разрешения будут проверяться через RBAC систему по требованию
2025-07-03 00:20:10 +03:00
scopes: dict[str, Any] = {}
2025-06-28 13:56:05 +03:00
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
# Роли пользователя будут определяться в контексте конкретной операции
# через RBAC систему, а не здесь
roles = []
2025-06-28 13:56:05 +03:00
# Обновляем last_seen
author.last_seen = int(time.time())
session.commit()
# Создаем объекты авторизации с сохранением токена
credentials = AuthCredentials(
author_id=author.id,
scopes=scopes,
logged_in=True,
error_message="",
email=author.email,
token=token,
)
user = AuthenticatedUser(
user_id=str(author.id),
username=author.slug or author.email or "",
roles=roles,
permissions=scopes,
token=token,
)
logger.debug(f"[auth.authenticate] Успешная аутентификация: {author.email}")
return credentials, user
except exc.NoResultFound:
logger.debug("[auth.authenticate] Пользователь не найден в базе данных")
return AuthCredentials(
author_id=None,
scopes={},
logged_in=False,
2025-06-28 13:56:05 +03:00
error_message="User not found",
email=None,
token=None,
), UnauthenticatedUser()
2025-06-28 13:56:05 +03:00
except Exception as e:
logger.error(f"[auth.authenticate] Ошибка при работе с базой данных: {e}")
return AuthCredentials(
author_id=None, scopes={}, logged_in=False, error_message=str(e), email=None, token=None
), UnauthenticatedUser()
except Exception as e:
logger.error(f"[auth.authenticate] Ошибка при проверке сессии: {e}")
return AuthCredentials(
author_id=None, scopes={}, logged_in=False, error_message=str(e), email=None, token=None
), UnauthenticatedUser()
async def __call__(
self,
scope: MutableMapping[str, Any],
receive: Callable[[], Awaitable[MutableMapping[str, Any]]],
send: Callable[[MutableMapping[str, Any]], Awaitable[None]],
) -> None:
2025-05-19 11:25:41 +03:00
"""Обработка ASGI запроса"""
2025-05-16 09:23:48 +03:00
if scope["type"] != "http":
await self.app(scope, receive, send)
return
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
# Извлекаем заголовки используя тот же механизм, что и get_safe_headers
headers = {}
# Первый приоритет: scope из ASGI (самый надежный источник)
if "headers" in scope:
scope_headers = scope.get("headers", [])
if scope_headers:
headers.update({k.decode("utf-8").lower(): v.decode("utf-8") for k, v in scope_headers})
logger.debug(f"[middleware] Получены заголовки из scope: {len(headers)}")
# Логируем все заголовки из scope для диагностики
logger.debug(f"[middleware] Заголовки из scope: {list(headers.keys())}")
# Логируем raw заголовки из scope
logger.debug(f"[middleware] Raw scope headers: {scope_headers}")
# Проверяем наличие authorization заголовка
if "authorization" in headers:
logger.debug(f"[middleware] Authorization заголовок найден: {headers['authorization'][:50]}...")
else:
logger.debug("[middleware] Authorization заголовок НЕ найден в scope headers")
else:
logger.debug("[middleware] Заголовки scope отсутствуют")
# Логируем все заголовки для диагностики
logger.debug(f"[middleware] Все заголовки: {list(headers.keys())}")
# Логируем конкретные заголовки для диагностики
auth_header_value = headers.get("authorization", "")
logger.debug(f"[middleware] Authorization header: {auth_header_value[:50]}...")
session_token_value = headers.get(SESSION_TOKEN_HEADER.lower(), "")
logger.debug(f"[middleware] {SESSION_TOKEN_HEADER} header: {session_token_value[:50]}...")
# Используем тот же механизм получения токена, что и в декораторе
2025-05-16 09:23:48 +03:00
token = None
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
# 0. Проверяем сохраненный токен в scope (приоритет)
if "auth_token" in scope:
token = scope["auth_token"]
logger.debug(f"[middleware] Токен получен из scope.auth_token: {len(token)}")
else:
logger.debug("[middleware] scope.auth_token НЕ найден")
# Стандартная система сессий уже обрабатывает кэширование
# Дополнительной проверки Redis кэша не требуется
# Отладка: детальная информация о запросе без Authorization
if not token:
method = scope.get("method", "UNKNOWN")
path = scope.get("path", "UNKNOWN")
logger.warning(f"[middleware] ЗАПРОС БЕЗ AUTHORIZATION: {method} {path}")
logger.warning(f"[middleware] User-Agent: {headers.get('user-agent', 'НЕ НАЙДЕН')}")
logger.warning(f"[middleware] Referer: {headers.get('referer', 'НЕ НАЙДЕН')}")
logger.warning(f"[middleware] Origin: {headers.get('origin', 'НЕ НАЙДЕН')}")
logger.warning(f"[middleware] Content-Type: {headers.get('content-type', 'НЕ НАЙДЕН')}")
logger.warning(f"[middleware] Все заголовки: {list(headers.keys())}")
# Проверяем, есть ли активные сессии в Redis
try:
from services.redis import redis as redis_adapter
# Получаем все активные сессии
session_keys = await redis_adapter.keys("session:*")
logger.debug(f"[middleware] Найдено активных сессий в Redis: {len(session_keys)}")
if session_keys:
# Пытаемся найти токен через активные сессии
for session_key in session_keys[:3]: # Проверяем первые 3 сессии
try:
session_data = await redis_adapter.hgetall(session_key)
if session_data:
logger.debug(f"[middleware] Найдена активная сессия: {session_key}")
# Извлекаем user_id из ключа сессии
user_id = (
session_key.decode("utf-8").split(":")[1]
if isinstance(session_key, bytes)
else session_key.split(":")[1]
)
logger.debug(f"[middleware] User ID из сессии: {user_id}")
break
except Exception as e:
logger.debug(f"[middleware] Ошибка чтения сессии {session_key}: {e}")
else:
logger.debug("[middleware] Активных сессий в Redis не найдено")
except Exception as e:
logger.debug(f"[middleware] Ошибка проверки сессий: {e}")
# 1. Проверяем заголовок Authorization
if not token:
auth_header = headers.get("authorization", "")
if auth_header:
if auth_header.startswith("Bearer "):
token = auth_header[7:].strip()
logger.debug(f"[middleware] Токен получен из заголовка Authorization: {len(token)}")
else:
token = auth_header.strip()
logger.debug(f"[middleware] Прямой токен получен из заголовка Authorization: {len(token)}")
# 2. Проверяем основной заголовок авторизации, если Authorization не найден
if not token:
auth_header = headers.get(SESSION_TOKEN_HEADER.lower(), "")
if auth_header:
if auth_header.startswith("Bearer "):
token = auth_header[7:].strip()
logger.debug(f"[middleware] Токен получен из заголовка {SESSION_TOKEN_HEADER}: {len(token)}")
else:
token = auth_header.strip()
logger.debug(f"[middleware] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}: {len(token)}")
# 3. Проверяем cookie
2025-05-16 09:23:48 +03:00
if not token:
cookies = headers.get("cookie", "")
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
logger.debug(f"[middleware] Проверяем cookies: {cookies[:100]}...")
2025-05-16 09:23:48 +03:00
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()
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
logger.debug(f"[middleware] Токен получен из cookie {SESSION_COOKIE_NAME}: {len(token)}")
2025-05-16 09:23:48 +03:00
break
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
if token:
logger.debug(f"[middleware] Токен найден: {len(token)} символов")
else:
logger.debug("[middleware] Токен не найден")
2025-05-30 14:05:50 +03:00
# Аутентифицируем пользователя
auth, user = await self.authenticate_user(token or "")
2025-05-30 14:08:29 +03:00
2025-05-30 14:05:50 +03:00
# Добавляем в scope данные авторизации и пользователя
scope["auth"] = auth
scope["user"] = user
2025-05-30 14:08:29 +03:00
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
# Сохраняем токен в scope для использования в последующих запросах
2025-05-16 09:23:48 +03:00
if token:
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
scope["auth_token"] = token
logger.debug(f"[middleware] Токен сохранен в scope.auth_token: {len(token)}")
2025-05-30 14:05:50 +03:00
logger.debug(f"[middleware] Пользователь аутентифицирован: {user.is_authenticated}")
e2e-fixing fix: убран health endpoint, E2E тест использует корневой маршрут - Убран health endpoint из main.py (не нужен) - E2E тест теперь проверяет корневой маршрут / вместо /health - Корневой маршрут доступен без логина, что подходит для проверки состояния сервера - E2E тест с браузером работает корректно docs: обновлен отчет о прогрессе E2E теста - Убраны упоминания health endpoint - Указано что используется корневой маршрут для проверки серверов - Обновлен список измененных файлов fix: исправлены GraphQL проблемы и E2E тест с браузером - Добавлено поле success в тип CommonResult для совместимости с фронтендом - Обновлены резолверы community, collection, topic для возврата поля success - Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint - E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице - Все GraphQL проблемы с полем success решены - E2E тест работает правильно с браузером как требовалось fix: исправлен поиск UI элементов в E2E тесте - Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300 - Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×) - Добавлен правильный поиск модального окна с множественными селекторами - Добавлен правильный поиск кнопки подтверждения в модальном окне - E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Обновлен отчет о прогрессе с полными результатами тестирования fix: исправлен импорт require_any_permission в resolvers/collection.py - Заменен импорт require_any_permission с auth.decorators на services.rbac - Бэкенд сервер теперь запускается корректно - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Оба сервера (бэкенд и фронтенд) работают стабильно fix: исправлен порядок импортов в resolvers/collection.py - Перемещен импорт require_any_permission в правильное место - E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения - Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
# Токен уже сохранен в стандартной системе сессий через SessionTokenManager
# Дополнительного кэширования не требуется
logger.debug("[middleware] Токен обработан стандартной системой сессий")
2025-05-20 00:00:24 +03:00
else:
logger.debug("[middleware] Токен не найден, пользователь неаутентифицирован")
2025-05-16 09:23:48 +03:00
await self.app(scope, receive, send)
2025-05-29 12:37:39 +03:00
def set_context(self, context) -> None:
2025-05-19 11:25:41 +03:00
"""Сохраняет ссылку на контекст GraphQL запроса"""
self._context = context
2025-05-20 00:00:24 +03:00
logger.debug(f"[middleware] Установлен контекст GraphQL: {bool(context)}")
2025-05-29 12:37:39 +03:00
2025-06-02 21:50:58 +03:00
def set_cookie(self, key: str, value: str, **options: Any) -> None:
2025-05-20 00:00:24 +03:00
"""
Устанавливает cookie в ответе
2025-05-29 12:37:39 +03:00
2025-05-20 00:00:24 +03:00
Args:
key: Имя cookie
value: Значение cookie
**options: Дополнительные параметры (httponly, secure, max_age, etc.)
"""
success = False
2025-05-29 12:37:39 +03:00
2025-05-20 00:00:24 +03:00
# Способ 1: Через response
2025-05-19 11:25:41 +03:00
if self._context and "response" in self._context and hasattr(self._context["response"], "set_cookie"):
2025-05-20 00:00:24 +03:00
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: {e!s}")
2025-05-29 12:37:39 +03:00
2025-05-20 00:00:24 +03:00
# Способ 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: {e!s}")
2025-05-29 12:37:39 +03:00
2025-05-20 00:00:24 +03:00
if not success:
logger.error(f"[middleware] Не удалось установить cookie {key}: объекты response недоступны")
2025-05-16 09:23:48 +03:00
2025-06-02 21:50:58 +03:00
def delete_cookie(self, key: str, **options: Any) -> None:
2025-05-20 00:00:24 +03:00
"""
Удаляет cookie из ответа
"""
success = False
2025-05-29 12:37:39 +03:00
2025-05-20 00:00:24 +03:00
# Способ 1: Через response
2025-05-19 11:25:41 +03:00
if self._context and "response" in self._context and hasattr(self._context["response"], "delete_cookie"):
2025-05-20 00:00:24 +03:00
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: {e!s}")
2025-05-29 12:37:39 +03:00
2025-05-20 00:00:24 +03:00
# Способ 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: {e!s}")
2025-05-29 12:37:39 +03:00
2025-05-20 00:00:24 +03:00
if not success:
logger.error(f"[middleware] Не удалось удалить cookie {key}: объекты response недоступны")
2025-05-16 09:23:48 +03:00
async def resolve(
2025-06-02 21:50:58 +03:00
self, next_resolver: Callable[..., Any], root: Any, info: GraphQLResolveInfo, *args: Any, **kwargs: Any
) -> Any:
2025-05-16 09:23:48 +03:00
"""
Middleware для обработки запросов GraphQL.
Добавляет методы для установки cookie в контекст.
"""
try:
# Получаем доступ к контексту запроса
context = info.context
2025-05-29 12:37:39 +03:00
2025-05-16 09:23:48 +03:00
# Сохраняем ссылку на контекст
2025-05-19 11:25:41 +03:00
self.set_context(context)
2025-05-29 12:37:39 +03:00
2025-05-16 09:23:48 +03:00
# Добавляем себя как объект, содержащий утилитные методы
context["extensions"] = self
2025-05-29 12:37:39 +03:00
2025-05-20 00:00:24 +03:00
# Проверяем наличие response в контексте
if "response" not in context or not context["response"]:
context["response"] = JSONResponse({})
logger.debug("[middleware] Создан новый response объект в контексте GraphQL")
2025-05-29 12:37:39 +03:00
logger.debug("[middleware] GraphQL resolve: контекст подготовлен, добавлены расширения для работы с cookie")
2025-05-29 12:37:39 +03:00
2025-06-02 21:50:58 +03:00
return await next_resolver(root, info, *args, **kwargs)
2025-05-16 09:23:48 +03:00
except Exception as e:
logger.error(f"[AuthMiddleware] Ошибка в GraphQL resolve: {e!s}")
2025-05-16 09:23:48 +03:00
raise
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
async def process_result(self, request: Request, result: Any) -> Response:
"""
Обрабатывает результат GraphQL запроса, поддерживая установку cookie
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
Args:
request: Starlette Request объект
result: результат GraphQL запроса (dict или Response)
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
Returns:
Response: HTTP-ответ с результатом и cookie (если необходимо)
"""
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
# Проверяем, является ли result уже объектом Response
if isinstance(result, Response):
response = result
# Пытаемся получить данные из response для проверки логина/логаута
result_data = {}
if isinstance(result, JSONResponse):
try:
body_content = result.body
if isinstance(body_content, (bytes, memoryview)):
body_text = bytes(body_content).decode("utf-8")
result_data = json.loads(body_text)
else:
result_data = json.loads(str(body_content))
2025-05-22 04:34:30 +03:00
except Exception as e:
logger.error(f"[process_result] Не удалось извлечь данные из JSONResponse: {e!s}")
2025-05-22 04:34:30 +03:00
else:
response = JSONResponse(result)
result_data = result
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
# Проверяем, был ли токен в запросе или ответе
if request.method == "POST":
try:
data = await request.json()
op_name = data.get("operationName", "").lower()
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
# Если это операция логина или обновления токена, и в ответе есть токен
if op_name in ["login", "refreshtoken"]:
token = None
# Пытаемся извлечь токен из данных ответа
if result_data and isinstance(result_data, dict):
data_obj = result_data.get("data", {})
if isinstance(data_obj, dict) and op_name in data_obj:
op_result = data_obj.get(op_name, {})
if isinstance(op_result, dict) and "token" in op_result:
token = op_result.get("token")
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
if token:
# Устанавливаем cookie с токеном
response.set_cookie(
key=SESSION_COOKIE_NAME,
value=token,
2025-05-29 12:37:39 +03:00
httponly=SESSION_COOKIE_HTTPONLY,
2025-05-22 04:34:30 +03:00
secure=SESSION_COOKIE_SECURE,
samesite=SESSION_COOKIE_SAMESITE,
max_age=SESSION_COOKIE_MAX_AGE,
)
2025-05-29 12:37:39 +03:00
logger.debug(
f"[graphql_handler] Установлена cookie {SESSION_COOKIE_NAME} для операции {op_name}"
)
2025-05-22 04:34:30 +03:00
# Если это операция logout, удаляем cookie
elif op_name == "logout":
response.delete_cookie(
key=SESSION_COOKIE_NAME,
secure=SESSION_COOKIE_SECURE,
httponly=SESSION_COOKIE_HTTPONLY,
2025-05-29 12:37:39 +03:00
samesite=SESSION_COOKIE_SAMESITE,
2025-05-22 04:34:30 +03:00
)
logger.debug(f"[graphql_handler] Удалена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
except Exception as e:
logger.error(f"[process_result] Ошибка при обработке POST запроса: {e!s}")
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
return response
2025-05-29 12:37:39 +03:00
2025-05-22 04:34:30 +03:00
# Создаем единый экземпляр AuthMiddleware для использования с GraphQL
async def _dummy_app(
scope: MutableMapping[str, Any],
receive: Callable[[], Awaitable[MutableMapping[str, Any]]],
send: Callable[[MutableMapping[str, Any]], Awaitable[None]],
) -> None:
"""Dummy ASGI app for middleware initialization"""
auth_middleware = AuthMiddleware(_dummy_app)