[0.9.7] - 2025-08-18
Some checks failed
Deploy on push / deploy (push) Failing after 2m22s

### 🔄 Изменения
- **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:
2025-08-18 14:25:25 +03:00
parent 9a2b792f08
commit 1b48675b92
78 changed files with 1658 additions and 1050 deletions

View File

@@ -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:
"""
Извлекает токен авторизации из запроса.