### 🔄 Изменения - **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации - **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)` ### 🧪 Тестирование - **Исправление тестов** - адаптация к новой структуре моделей - **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py` - **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев - **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями - **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода ### 🔧 Рефакторинг - **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру - **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль - **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры - **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей - **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки ### 🔧 Авторизация с cookies - **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization - **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно - **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token` - **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession` - **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author` - **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами - **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession` - **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации ### 📝 Документация - **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа - Обновлена документация RBAC - Обновлена документация авторизации с cookies
This commit is contained in:
118
auth/utils.py
118
auth/utils.py
@@ -3,7 +3,7 @@
|
||||
Содержит функции для работы с токенами, заголовками и запросами
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Tuple
|
||||
|
||||
from settings import SESSION_COOKIE_NAME, SESSION_TOKEN_HEADER
|
||||
from utils.logger import root_logger as logger
|
||||
@@ -56,6 +56,122 @@ def get_safe_headers(request: Any) -> dict[str, str]:
|
||||
return headers
|
||||
|
||||
|
||||
async def extract_token_from_request(request) -> str | None:
|
||||
"""
|
||||
DRY функция для извлечения токена из request.
|
||||
Проверяет cookies и заголовок Authorization.
|
||||
|
||||
Args:
|
||||
request: Request объект
|
||||
|
||||
Returns:
|
||||
Optional[str]: Токен или None
|
||||
"""
|
||||
if not request:
|
||||
return None
|
||||
|
||||
# 1. Проверяем cookies
|
||||
if hasattr(request, "cookies") and request.cookies:
|
||||
token = request.cookies.get(SESSION_COOKIE_NAME)
|
||||
if token:
|
||||
logger.debug(f"[utils] Токен получен из cookie {SESSION_COOKIE_NAME}")
|
||||
return token
|
||||
|
||||
# 2. Проверяем заголовок Authorization
|
||||
headers = get_safe_headers(request)
|
||||
auth_header = headers.get("authorization", "")
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:].strip()
|
||||
logger.debug("[utils] Токен получен из заголовка Authorization")
|
||||
return token
|
||||
|
||||
logger.debug("[utils] Токен не найден ни в cookies, ни в заголовке")
|
||||
return None
|
||||
|
||||
|
||||
async def get_user_data_by_token(token: str) -> Tuple[bool, dict | None, str | None]:
|
||||
"""
|
||||
Получает данные пользователя по токену.
|
||||
|
||||
Args:
|
||||
token: Токен авторизации
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[dict], Optional[str]]: (success, user_data, error_message)
|
||||
"""
|
||||
try:
|
||||
from auth.tokens.storage import TokenStorage as TokenManager
|
||||
from orm.author import Author
|
||||
from storage.db import local_session
|
||||
|
||||
# Проверяем сессию через TokenManager
|
||||
payload = await TokenManager.verify_session(token)
|
||||
|
||||
if not payload:
|
||||
return False, None, "Сессия не найдена"
|
||||
|
||||
# Получаем user_id из payload
|
||||
user_id = payload.user_id if hasattr(payload, "user_id") else payload.get("user_id")
|
||||
|
||||
if not user_id:
|
||||
return False, None, "Токен не содержит user_id"
|
||||
|
||||
# Получаем данные пользователя
|
||||
with local_session() as session:
|
||||
author_obj = session.query(Author).where(Author.id == int(user_id)).first()
|
||||
if not author_obj:
|
||||
return False, None, f"Пользователь с ID {user_id} не найден в БД"
|
||||
|
||||
try:
|
||||
user_data = author_obj.dict()
|
||||
except Exception:
|
||||
user_data = {
|
||||
"id": author_obj.id,
|
||||
"email": author_obj.email,
|
||||
"name": getattr(author_obj, "name", ""),
|
||||
"slug": getattr(author_obj, "slug", ""),
|
||||
"username": getattr(author_obj, "username", ""),
|
||||
}
|
||||
|
||||
logger.debug(f"[utils] Данные пользователя получены для ID {user_id}")
|
||||
return True, user_data, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[utils] Ошибка при получении данных пользователя: {e}")
|
||||
return False, None, f"Ошибка получения данных: {e!s}"
|
||||
|
||||
|
||||
async def get_auth_token_from_context(info: Any) -> str | None:
|
||||
"""
|
||||
Извлекает токен авторизации из GraphQL контекста.
|
||||
Порядок проверки:
|
||||
1. Проверяет заголовок Authorization
|
||||
2. Проверяет cookie session_token
|
||||
3. Переиспользует логику get_auth_token для request
|
||||
|
||||
Args:
|
||||
info: GraphQLResolveInfo объект
|
||||
|
||||
Returns:
|
||||
Optional[str]: Токен авторизации или None
|
||||
"""
|
||||
try:
|
||||
context = getattr(info, "context", {})
|
||||
request = context.get("request")
|
||||
|
||||
if request:
|
||||
# Переиспользуем существующую логику для request
|
||||
return await get_auth_token(request)
|
||||
|
||||
# Если request отсутствует, возвращаем None
|
||||
logger.debug("[utils] Request отсутствует в GraphQL контексте")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[utils] Ошибка при извлечении токена из GraphQL контекста: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_auth_token(request: Any) -> str | None:
|
||||
"""
|
||||
Извлекает токен авторизации из запроса.
|
||||
|
||||
Reference in New Issue
Block a user