Files
core/services/auth.py

816 lines
38 KiB
Python
Raw Normal View History

2025-07-03 00:20:10 +03:00
"""
Сервис аутентификации с бизнес-логикой для регистрации,
входа и управления сессиями и декорраторами для GraphQL.
"""
import json
import secrets
import time
2024-01-25 22:41:27 +03:00
from functools import wraps
2025-08-17 16:33:54 +03:00
from typing import Any, Callable
2024-04-08 10:38:58 +03:00
2025-07-31 18:55:59 +03:00
from graphql.error import GraphQLError
2025-05-21 18:29:46 +03:00
from starlette.requests import Request
2025-07-03 00:20:10 +03:00
from auth.email import send_auth_email
2025-07-31 18:55:59 +03:00
from auth.exceptions import InvalidPasswordError, InvalidTokenError, ObjectNotExistError
from auth.identity import Identity
2025-05-29 12:37:39 +03:00
from auth.internal import verify_internal_auth
2025-07-03 00:20:10 +03:00
from auth.jwtcodec import JWTCodec
from auth.tokens.storage import TokenStorage
2025-07-31 18:55:59 +03:00
from auth.tokens.verification import VerificationTokenManager
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **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
2025-08-18 14:25:25 +03:00
from auth.utils import extract_token_from_request
2025-08-17 16:33:54 +03:00
from cache.cache import get_cached_author_by_id
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **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
2025-08-18 14:25:25 +03:00
from orm.author import Author
2025-07-31 18:55:59 +03:00
from orm.community import (
Community,
CommunityAuthor,
CommunityFollower,
2025-08-20 18:33:58 +03:00
)
from rbac.api import (
2025-07-31 18:55:59 +03:00
assign_role_to_user,
get_user_roles_in_community,
)
2025-07-03 00:20:10 +03:00
from settings import (
ADMIN_EMAILS,
SESSION_COOKIE_NAME,
SESSION_TOKEN_HEADER,
)
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **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
2025-08-18 14:25:25 +03:00
from storage.db import local_session
from storage.redis import redis
2025-07-03 00:20:10 +03:00
from utils.generate_slug import generate_unique_slug
2025-05-29 12:37:39 +03:00
from utils.logger import root_logger as logger
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **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
2025-08-18 14:25:25 +03:00
from utils.password import Password
2024-01-13 11:49:12 +03:00
2024-12-24 14:04:52 +03:00
# Список разрешенных заголовков
2025-01-21 10:09:28 +03:00
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
2024-02-21 10:27:16 +03:00
2025-07-03 00:20:10 +03:00
class AuthService:
"""Сервис аутентификации с бизнес-логикой"""
2024-12-11 23:49:58 +03:00
2025-07-31 18:55:59 +03:00
async def check_auth(self, req: Request) -> tuple[int | None, list[str], bool]:
2025-07-03 00:20:10 +03:00
"""
Проверка авторизации пользователя.
2024-12-11 23:49:58 +03:00
2025-07-03 00:20:10 +03:00
Проверяет токен и получает данные из локальной БД.
"""
logger.debug("[check_auth] Проверка авторизации...")
2024-12-11 23:49:58 +03:00
2025-07-03 00:20:10 +03:00
# Получаем заголовок авторизации
token = None
# Если req is None (в тестах), возвращаем пустые данные
if not req:
logger.debug("[check_auth] Запрос отсутствует (тестовое окружение)")
return 0, [], False
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **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
2025-08-18 14:25:25 +03:00
token = await extract_token_from_request(req)
2025-07-03 00:20:10 +03:00
if not token:
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **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
2025-08-18 14:25:25 +03:00
logger.debug("[check_auth] Токен не найден")
2025-07-03 00:20:10 +03:00
return 0, [], False
# Проверяем авторизацию внутренним механизмом
logger.debug("[check_auth] Вызов verify_internal_auth...")
user_id, user_roles, is_admin = await verify_internal_auth(token)
logger.debug(
f"[check_auth] Результат verify_internal_auth: user_id={user_id}, roles={user_roles}, is_admin={is_admin}"
)
2025-07-23 13:29:49 +03:00
# Если в ролях нет админа, но есть ID - проверяем через новую систему RBAC
2025-07-03 00:20:10 +03:00
if user_id and not is_admin:
try:
2025-07-23 13:29:49 +03:00
# Преобразуем user_id в число
try:
2025-07-31 18:55:59 +03:00
user_id_int = int(str(user_id).strip())
2025-07-23 13:29:49 +03:00
except (ValueError, TypeError):
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
return 0, [], False
user_roles_in_community = get_user_roles_in_community(user_id_int, community_id=1)
logger.debug(f"[check_auth] Роли из CommunityAuthor: {user_roles_in_community}")
# Обновляем роли из новой системы
user_roles = user_roles_in_community
is_admin = any(role in ["admin", "super"] for role in user_roles_in_community)
# Проверяем админские права через email если нет роли админа
if not is_admin:
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.id == user_id_int).first()
2025-07-23 13:29:49 +03:00
if author and author.email in ADMIN_EMAILS.split(","):
is_admin = True
logger.debug(
f"[check_auth] Пользователь {author.email} определен как админ через ADMIN_EMAILS"
)
2025-07-03 00:20:10 +03:00
except Exception as e:
logger.error(f"Ошибка при проверке прав администратора: {e}")
2025-07-31 18:55:59 +03:00
return 0, [], False
2025-07-03 00:20:10 +03:00
return user_id, user_roles, is_admin
2025-08-17 16:33:54 +03:00
async def add_user_role(self, user_id: str, roles: list[str] | None = None) -> str | None:
2025-07-03 00:20:10 +03:00
"""
Добавление ролей пользователю в локальной БД через CommunityAuthor.
"""
if not roles:
roles = ["author", "reader"]
logger.info(f"Adding roles {roles} to user {user_id}")
2025-07-23 13:29:49 +03:00
try:
user_id_int = int(user_id)
except (ValueError, TypeError):
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
return None
# Проверяем существующие роли
existing_roles = get_user_roles_in_community(user_id_int, community_id=1)
logger.debug(f"Существующие роли пользователя {user_id}: {existing_roles}")
# Добавляем новые роли через новую систему RBAC
for role_name in roles:
if role_name not in existing_roles:
success = assign_role_to_user(user_id_int, role_name, community_id=1)
if success:
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
else:
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
else:
logger.debug(f"Роль {role_name} уже есть у пользователя {user_id}")
return user_id
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
def create_user(self, user_dict: dict[str, Any], community_id: int | None = None) -> Author:
"""Создает нового пользователя с дефолтными ролями"""
2025-07-25 01:04:15 +03:00
# Нормализуем email
if "email" in user_dict:
user_dict["email"] = user_dict["email"].lower()
# Проверяем уникальность email
with local_session() as session:
2025-07-31 18:55:59 +03:00
existing_user = session.query(Author).where(Author.email == user_dict["email"]).first()
2025-07-25 01:04:15 +03:00
if existing_user:
# Если пользователь с таким email уже существует, возвращаем его
logger.warning(f"Пользователь с email {user_dict['email']} уже существует")
return existing_user
# Генерируем уникальный slug
base_slug = user_dict.get("slug", generate_unique_slug(user_dict.get("name", user_dict.get("email", "user"))))
# Проверяем уникальность slug
with local_session() as session:
# Добавляем суффикс, если slug уже существует
counter = 1
unique_slug = base_slug
2025-07-31 18:55:59 +03:00
while session.query(Author).where(Author.slug == unique_slug).first():
2025-07-25 01:04:15 +03:00
unique_slug = f"{base_slug}-{counter}"
counter += 1
user_dict["slug"] = unique_slug
2025-07-03 00:20:10 +03:00
user = Author(**user_dict)
2025-07-25 01:04:15 +03:00
target_community_id = int(community_id) if community_id is not None else 1
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
with local_session() as session:
session.add(user)
2025-07-25 01:04:15 +03:00
session.flush() # Получаем ID пользователя
2025-07-03 00:20:10 +03:00
# Получаем сообщество для назначения ролей
2025-07-25 01:04:15 +03:00
logger.debug(f"Ищем сообщество с ID {target_community_id}")
2025-07-31 18:55:59 +03:00
community = session.query(Community).where(Community.id == target_community_id).first()
2025-07-25 01:04:15 +03:00
# Отладочная информация
all_communities = session.query(Community).all()
logger.debug(f"Все сообщества в базе: {[c.id for c in all_communities]}")
2025-07-03 00:20:10 +03:00
if not community:
logger.warning(f"Сообщество {target_community_id} не найдено, используем ID=1")
target_community_id = 1
2025-07-31 18:55:59 +03:00
community = session.query(Community).where(Community.id == target_community_id).first()
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
if community:
2025-07-25 01:04:15 +03:00
default_roles = community.get_default_roles() or ["reader", "author"]
2025-07-03 00:20:10 +03:00
# Создаем CommunityAuthor с ролями
community_author = CommunityAuthor(
community_id=target_community_id,
author_id=user.id,
roles=",".join(default_roles),
)
session.add(community_author)
# Создаем подписку на сообщество
follower = CommunityFollower(community=target_community_id, follower=int(user.id))
session.add(follower)
2025-07-25 01:04:15 +03:00
logger.info(
f"Пользователь {user.id} создан с ролями {default_roles} в сообществе {target_community_id}"
)
else:
# Если сообщество не найдено, вызываем исключение
raise ValueError("Сообщество не найдено")
2025-07-03 00:20:10 +03:00
session.commit()
return user
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
async def get_session(self, token: str) -> dict[str, Any]:
"""Получает информацию о текущей сессии по токену"""
try:
# Проверяем токен
payload = JWTCodec.decode(token)
if not payload:
return {"success": False, "token": None, "author": None, "error": "Невалидный токен"}
token_verification = await TokenStorage.verify_session(token)
if not token_verification:
return {"success": False, "token": None, "author": None, "error": "Токен истек"}
2025-07-31 18:55:59 +03:00
user_id = payload.get("user_id")
if user_id is None:
return {"success": False, "token": None, "author": None, "error": "Отсутствует user_id в токене"}
2025-07-03 00:20:10 +03:00
# Получаем автора
author = await get_cached_author_by_id(int(user_id), lambda x: x)
if not author:
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
return {"success": True, "token": token, "author": author, "error": None}
except Exception as e:
logger.error(f"Ошибка получения сессии: {e}")
return {"success": False, "token": None, "author": None, "error": str(e)}
async def register_user(self, email: str, password: str = "", name: str = "") -> dict[str, Any]:
"""Регистрирует нового пользователя"""
email = email.lower()
logger.info(f"Попытка регистрации для {email}")
with local_session() as session:
2025-07-31 18:55:59 +03:00
user = session.query(Author).where(Author.email == email).first()
2025-07-03 00:20:10 +03:00
if user:
logger.warning(f"Пользователь {email} уже существует")
return {"success": False, "token": None, "author": None, "error": "Пользователь уже существует"}
slug = generate_unique_slug(name if name else email.split("@")[0])
user_dict = {
"email": email,
"name": name if name else email.split("@")[0],
"slug": slug,
}
if password:
user_dict["password"] = Password.encode(password)
new_user = self.create_user(user_dict)
2025-01-21 10:09:28 +03:00
2025-07-03 00:20:10 +03:00
try:
await self.send_verification_link(email)
logger.info(f"Пользователь {email} зарегистрирован, ссылка отправлена")
return {
"success": True,
"token": None,
"author": new_user,
"error": "Требуется подтверждение email.",
}
except Exception as e:
logger.error(f"Ошибка отправки ссылки для {email}: {e}")
return {
"success": True,
"token": None,
"author": new_user,
"error": f"Пользователь зарегистрирован, но ошибка отправки ссылки: {e}",
}
async def send_verification_link(self, email: str, lang: str = "ru", template: str = "confirm") -> Author:
"""Отправляет ссылку подтверждения на email"""
email = email.lower()
with local_session() as session:
2025-07-31 18:55:59 +03:00
user = session.query(Author).where(Author.email == email).first()
2025-07-03 00:20:10 +03:00
if not user:
2025-07-31 18:55:59 +03:00
raise ObjectNotExistError("User not found")
2025-07-03 00:20:10 +03:00
try:
verification_manager = VerificationTokenManager()
token = await verification_manager.create_verification_token(
str(user.id), "email_confirmation", {"email": user.email, "template": template}
)
except (AttributeError, ImportError):
token = await TokenStorage.create_session(
user_id=str(user.id),
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
username=str(user.email or user.slug or ""),
2025-07-03 00:20:10 +03:00
device_info={"email": user.email} if hasattr(user, "email") else None,
)
2024-12-11 23:49:58 +03:00
2025-07-03 00:20:10 +03:00
await send_auth_email(user, token, lang, template)
return user
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
async def confirm_email(self, token: str) -> dict[str, Any]:
"""Подтверждает email по токену"""
2025-05-21 18:29:46 +03:00
try:
2025-07-03 00:20:10 +03:00
logger.info("Начало подтверждения email по токену")
payload = JWTCodec.decode(token)
if not payload:
logger.warning("Невалидный токен")
return {"success": False, "token": None, "author": None, "error": "Невалидный токен"}
token_verification = await TokenStorage.verify_session(token)
if not token_verification:
logger.warning("Токен не найден в системе или истек")
return {"success": False, "token": None, "author": None, "error": "Токен не найден или истек"}
2025-07-31 18:55:59 +03:00
user_id = payload.get("user_id")
username = payload.get("username")
2025-07-03 00:20:10 +03:00
2025-05-21 18:29:46 +03:00
with local_session() as session:
2025-07-03 00:20:10 +03:00
user = session.query(Author).where(Author.id == user_id).first()
if not user:
logger.warning(f"Пользователь с ID {user_id} не найден")
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
device_info = {"email": user.email} if hasattr(user, "email") else None
session_token = await TokenStorage.create_session(
user_id=str(user_id),
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
username=user.email or user.slug or username,
2025-07-03 00:20:10 +03:00
device_info=device_info,
)
user.email_verified = True
user.last_seen = int(time.time())
session.add(user)
session.commit()
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
logger.info(f"Email для пользователя {user_id} подтвержден")
return {"success": True, "token": session_token, "author": user, "error": None}
2025-07-31 18:55:59 +03:00
except InvalidTokenError as e:
2025-07-03 00:20:10 +03:00
logger.warning(f"Невалидный токен - {e.message}")
return {"success": False, "token": None, "author": None, "error": f"Невалидный токен: {e.message}"}
2025-05-21 18:29:46 +03:00
except Exception as e:
2025-07-03 00:20:10 +03:00
logger.error(f"Ошибка подтверждения email: {e}")
return {"success": False, "token": None, "author": None, "error": f"Ошибка подтверждения email: {e}"}
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
async def login(self, email: str, password: str, request=None) -> dict[str, Any]:
"""Авторизация пользователя"""
email = email.lower()
logger.info(f"Попытка входа для {email}")
2023-10-23 17:47:11 +03:00
2025-07-03 00:20:10 +03:00
try:
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.email == email).first()
2025-07-03 00:20:10 +03:00
if not author:
logger.warning(f"Пользователь {email} не найден")
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
2025-08-27 18:31:51 +03:00
# 🩵 Проверяем права с обработкой ошибок RBAC
is_admin_email = author.email in ADMIN_EMAILS.split(",")
has_reader_role = False
try:
user_roles = get_user_roles_in_community(int(author.id), community_id=1)
has_reader_role = "reader" in user_roles
logger.debug(f"Роли пользователя {email}: {user_roles}")
except Exception as rbac_error:
logger.warning(f"🧿 RBAC ошибка для {email}: {rbac_error}")
# Если RBAC не работает, разрешаем вход только админам
if not is_admin_email:
logger.warning(f"RBAC недоступен и {email} не админ - запрещаем вход")
return {
"success": False,
"token": None,
"author": None,
"error": "Система ролей временно недоступна. Попробуйте позже.",
}
logger.info(f"🔒 RBAC недоступен, но {email} - админ, разрешаем вход")
# Проверяем права: админы или пользователи с ролью reader
if not has_reader_role and not is_admin_email:
logger.warning(f"У пользователя {email} нет роли 'reader' и он не админ")
2025-07-23 13:29:49 +03:00
return {
"success": False,
"token": None,
"author": None,
"error": "Нет прав для входа. Требуется роль 'reader'.",
}
2025-07-03 00:20:10 +03:00
# Проверяем пароль
try:
valid_author = Identity.password(author, password)
2025-07-31 18:55:59 +03:00
except (InvalidPasswordError, Exception) as e:
2025-07-03 00:20:10 +03:00
logger.warning(f"Неверный пароль для {email}: {e}")
return {"success": False, "token": None, "author": None, "error": str(e)}
# Создаем токен
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
username = str(valid_author.email or valid_author.slug or "")
2025-07-03 00:20:10 +03:00
token = await TokenStorage.create_session(
user_id=str(valid_author.id),
username=username,
device_info={"email": valid_author.email} if hasattr(valid_author, "email") else None,
)
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
# Обновляем время входа
valid_author.last_seen = int(time.time())
session.commit()
2024-01-25 22:41:27 +03:00
2025-07-03 00:20:10 +03:00
# Устанавливаем cookie если есть request
if request and token:
self._set_auth_cookie(request, token)
2024-12-11 23:49:58 +03:00
2025-07-03 00:20:10 +03:00
try:
2025-07-31 18:55:59 +03:00
author_dict = valid_author.dict()
2025-07-03 00:20:10 +03:00
except Exception:
author_dict = {
"id": valid_author.id,
"email": valid_author.email,
"name": getattr(valid_author, "name", ""),
"slug": getattr(valid_author, "slug", ""),
"username": getattr(valid_author, "username", ""),
}
logger.info(f"Успешный вход для {email}")
return {"success": True, "token": token, "author": author_dict, "error": None}
2024-12-11 23:49:58 +03:00
2025-07-03 00:20:10 +03:00
except Exception as e:
logger.error(f"Ошибка входа для {email}: {e}")
return {"success": False, "token": None, "author": None, "error": f"Ошибка авторизации: {e}"}
2025-07-02 22:30:21 +03:00
2025-07-03 00:20:10 +03:00
def _set_auth_cookie(self, request, token: str) -> bool:
"""Устанавливает cookie аутентификации"""
2025-05-16 09:23:48 +03:00
try:
2025-07-03 00:20:10 +03:00
if hasattr(request, "cookies"):
request.cookies[SESSION_COOKIE_NAME] = token
return True
except Exception as e:
logger.error(f"Ошибка установки cookie: {e}")
return False
2024-12-11 23:49:58 +03:00
2025-07-31 18:55:59 +03:00
async def logout(self, user_id: str, token: str | None = None) -> dict[str, Any]:
2025-07-03 00:20:10 +03:00
"""Выход из системы"""
try:
if token:
await TokenStorage.revoke_session(token)
logger.info(f"Пользователь {user_id} вышел из системы")
return {"success": True, "message": "Успешный выход"}
except Exception as e:
logger.error(f"Ошибка выхода для {user_id}: {e}")
return {"success": False, "message": f"Ошибка выхода: {e}"}
2025-05-16 09:23:48 +03:00
2025-07-31 18:55:59 +03:00
async def refresh_token(self, user_id: str, old_token: str, device_info: dict | None = None) -> dict[str, Any]:
2025-07-03 00:20:10 +03:00
"""Обновление токена"""
try:
new_token = await TokenStorage.refresh_session(int(user_id), old_token, device_info or {})
if not new_token:
return {"success": False, "token": None, "author": None, "error": "Не удалось обновить токен"}
2025-05-16 09:23:48 +03:00
2025-07-03 00:20:10 +03:00
# Получаем данные пользователя
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.id == int(user_id)).first()
2025-07-03 00:20:10 +03:00
if not author:
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
2025-05-16 09:23:48 +03:00
2025-07-03 00:20:10 +03:00
try:
2025-07-31 18:55:59 +03:00
author_dict = author.dict()
2025-07-03 00:20:10 +03:00
except Exception:
author_dict = {
"id": author.id,
"email": author.email,
"name": getattr(author, "name", ""),
"slug": getattr(author, "slug", ""),
}
2025-05-16 09:23:48 +03:00
2025-07-03 00:20:10 +03:00
return {"success": True, "token": new_token, "author": author_dict, "error": None}
2024-12-12 01:04:11 +03:00
2025-07-03 00:20:10 +03:00
except Exception as e:
logger.error(f"Ошибка обновления токена для {user_id}: {e}")
return {"success": False, "token": None, "author": None, "error": str(e)}
2025-05-21 01:34:02 +03:00
2025-07-03 00:20:10 +03:00
async def request_password_reset(self, email: str, lang: str = "ru") -> dict[str, Any]:
"""Запрос сброса пароля"""
try:
email = email.lower()
logger.info(f"Запрос сброса пароля для {email}")
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.email == email).first()
2025-07-03 00:20:10 +03:00
if not author:
logger.warning(f"Пользователь {email} не найден")
return {"success": True} # Для безопасности
2025-07-03 00:20:10 +03:00
try:
verification_manager = VerificationTokenManager()
token = await verification_manager.create_verification_token(
str(author.id), "password_reset", {"email": author.email}
2025-06-02 21:50:58 +03:00
)
2025-07-03 00:20:10 +03:00
except (AttributeError, ImportError):
token = await TokenStorage.create_session(
user_id=str(author.id),
[0.9.13] - 2025-08-27 ### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
2025-08-27 12:15:01 +03:00
username=str(author.email or author.slug or ""),
2025-07-03 00:20:10 +03:00
device_info={"email": author.email} if hasattr(author, "email") else None,
)
await send_auth_email(author, token, lang, "password_reset")
logger.info(f"Письмо сброса пароля отправлено для {email}")
return {"success": True}
except Exception as e:
logger.error(f"Ошибка запроса сброса пароля для {email}: {e}")
return {"success": False}
def is_email_used(self, email: str) -> bool:
"""Проверяет, используется ли email"""
email = email.lower()
with local_session() as session:
2025-07-31 18:55:59 +03:00
user = session.query(Author).where(Author.email == email).first()
2025-07-03 00:20:10 +03:00
return user is not None
async def update_security(
2025-07-31 18:55:59 +03:00
self, user_id: int, old_password: str, new_password: str | None = None, email: str | None = None
2025-07-03 00:20:10 +03:00
) -> dict[str, Any]:
"""Обновление пароля и email"""
try:
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.id == user_id).first()
2025-07-03 00:20:10 +03:00
if not author:
return {"success": False, "error": "NOT_AUTHENTICATED", "author": None}
if not author.verify_password(old_password):
return {"success": False, "error": "incorrect old password", "author": None}
if email and email != author.email:
2025-07-31 18:55:59 +03:00
existing_user = session.query(Author).where(Author.email == email).first()
2025-07-03 00:20:10 +03:00
if existing_user:
return {"success": False, "error": "email already exists", "author": None}
changes_made = []
if new_password:
author.set_password(new_password)
changes_made.append("password")
if email and email != author.email:
# Создаем запрос на смену email через Redis
token = secrets.token_urlsafe(32)
email_change_data = {
"user_id": user_id,
"old_email": author.email,
"new_email": email,
"token": token,
"expires_at": int(time.time()) + 3600, # 1 час
}
redis_key = f"email_change:{user_id}"
await redis.execute("SET", redis_key, json.dumps(email_change_data))
await redis.execute("EXPIRE", redis_key, 3600)
changes_made.append("email_pending")
logger.info(f"Email смена инициирована для пользователя {user_id}")
session.commit()
logger.info(f"Безопасность обновлена для {user_id}: {changes_made}")
return {"success": True, "error": None, "author": author}
except Exception as e:
logger.error(f"Ошибка обновления безопасности для {user_id}: {e}")
return {"success": False, "error": str(e), "author": None}
async def confirm_email_change(self, user_id: int, token: str) -> dict[str, Any]:
"""Подтверждение смены email по токену"""
try:
# Получаем данные смены email из Redis
redis_key = f"email_change:{user_id}"
cached_data = await redis.execute("GET", redis_key)
if not cached_data:
return {"success": False, "error": "NO_PENDING_EMAIL", "author": None}
try:
email_change_data = json.loads(cached_data)
except json.JSONDecodeError:
return {"success": False, "error": "INVALID_TOKEN", "author": None}
# Проверяем токен
if email_change_data.get("token") != token:
return {"success": False, "error": "INVALID_TOKEN", "author": None}
# Проверяем срок действия
if email_change_data.get("expires_at", 0) < int(time.time()):
await redis.execute("DEL", redis_key)
return {"success": False, "error": "TOKEN_EXPIRED", "author": None}
new_email = email_change_data.get("new_email")
if not new_email:
return {"success": False, "error": "INVALID_TOKEN", "author": None}
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.id == user_id).first()
2025-07-03 00:20:10 +03:00
if not author:
return {"success": False, "error": "NOT_AUTHENTICATED", "author": None}
# Проверяем, что новый email не занят
2025-07-31 18:55:59 +03:00
existing_user = session.query(Author).where(Author.email == new_email).first()
2025-07-03 00:20:10 +03:00
if existing_user and existing_user.id != author.id:
await redis.execute("DEL", redis_key)
return {"success": False, "error": "email already exists", "author": None}
# Применяем смену email
author.email = new_email
author.email_verified = True
author.updated_at = int(time.time())
session.add(author)
session.commit()
# Удаляем данные из Redis
await redis.execute("DEL", redis_key)
logger.info(f"Email изменен для пользователя {user_id}")
return {"success": True, "error": None, "author": author}
except Exception as e:
logger.error(f"Ошибка подтверждения смены email: {e}")
return {"success": False, "error": str(e), "author": None}
async def cancel_email_change(self, user_id: int) -> dict[str, Any]:
"""Отмена смены email"""
try:
redis_key = f"email_change:{user_id}"
cached_data = await redis.execute("GET", redis_key)
if not cached_data:
return {"success": False, "error": "NO_PENDING_EMAIL", "author": None}
# Удаляем данные из Redis
await redis.execute("DEL", redis_key)
# Получаем текущие данные пользователя
with local_session() as session:
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.id == user_id).first()
2025-07-03 00:20:10 +03:00
if not author:
return {"success": False, "error": "NOT_AUTHENTICATED", "author": None}
logger.info(f"Смена email отменена для пользователя {user_id}")
return {"success": True, "error": None, "author": author}
except Exception as e:
logger.error(f"Ошибка отмены смены email: {e}")
return {"success": False, "error": str(e), "author": None}
2025-08-20 18:33:58 +03:00
async def ensure_user_has_reader_role(self, user_id: int, session=None) -> bool:
2025-07-23 13:29:49 +03:00
"""
Убеждается, что у пользователя есть роль 'reader'.
Если её нет - добавляет автоматически.
Args:
user_id: ID пользователя
2025-08-20 18:33:58 +03:00
session: Сессия БД (опционально)
2025-07-23 13:29:49 +03:00
Returns:
True если роль была добавлена или уже существует
"""
2025-08-20 18:33:58 +03:00
try:
logger.debug(f"[ensure_user_has_reader_role] Проверяем роли для пользователя {user_id}")
2025-07-23 13:29:49 +03:00
2025-08-20 18:33:58 +03:00
# Используем переданную сессию или создаем новую
existing_roles = get_user_roles_in_community(user_id, community_id=1, session=session)
logger.debug(f"[ensure_user_has_reader_role] Существующие роли: {existing_roles}")
2025-07-23 13:29:49 +03:00
2025-08-20 18:33:58 +03:00
if "reader" not in existing_roles:
logger.warning(f"У пользователя {user_id} нет роли 'reader'. Добавляем автоматически.")
success = assign_role_to_user(user_id, "reader", community_id=1, session=session)
logger.debug(f"[ensure_user_has_reader_role] Результат assign_role_to_user: {success}")
if success:
logger.info(f"Роль 'reader' добавлена пользователю {user_id}")
return True
logger.error(f"Не удалось добавить роль 'reader' пользователю {user_id}")
return False
2025-07-23 13:29:49 +03:00
2025-08-20 18:33:58 +03:00
logger.debug(f"[ensure_user_has_reader_role] Роль 'reader' уже есть у пользователя {user_id}")
return True
except Exception as e:
logger.error(f"Ошибка при проверке/добавлении роли reader для пользователя {user_id}: {e}")
# В случае ошибки возвращаем False, чтобы тест мог обработать это
return False
2025-07-23 13:29:49 +03:00
async def fix_all_users_reader_role(self) -> dict[str, int]:
"""
Проверяет всех пользователей и добавляет роль 'reader' тем, у кого её нет.
Returns:
Статистика операции: {"checked": int, "fixed": int, "errors": int}
"""
stats = {"checked": 0, "fixed": 0, "errors": 0}
with local_session() as session:
# Получаем всех пользователей
all_authors = session.query(Author).all()
for author in all_authors:
stats["checked"] += 1
try:
2025-07-25 01:04:15 +03:00
had_reader = await self.ensure_user_has_reader_role(int(author.id))
2025-07-23 13:29:49 +03:00
if not had_reader:
stats["fixed"] += 1
except Exception as e:
logger.error(f"Ошибка при исправлении ролей для пользователя {author.id}: {e}")
stats["errors"] += 1
logger.info(f"Исправление ролей завершено: {stats}")
return stats
2025-07-03 00:20:10 +03:00
def login_required(self, f: Callable) -> Callable:
"""Декоратор для проверки авторизации пользователя. Требуется наличие роли 'reader'."""
@wraps(f)
async def decorated_function(*args: Any, **kwargs: Any) -> Any:
info = args[1]
req = info.context.get("request")
logger.debug(
2025-07-03 00:20:10 +03:00
f"[login_required] Проверка авторизации для запроса: {req.method if req else 'unknown'} {req.url.path if req and hasattr(req, 'url') else 'unknown'}"
)
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
# Извлекаем токен из заголовков
token = None
if req:
headers_dict = dict(req.headers.items())
for header_name, header_value in headers_dict.items():
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
token = header_value
break
if token and token.startswith("Bearer "):
token = token.split("Bearer ")[-1].strip()
# Для тестового режима
if not req and info.context.get("author") and info.context.get("roles"):
logger.debug("[login_required] Тестовый режим")
user_id = info.context["author"]["id"]
user_roles = info.context["roles"]
is_admin = info.context.get("is_admin", False)
if not token:
token = info.context.get("token")
else:
# Обычный режим
user_id, user_roles, is_admin = await self.check_auth(req)
if not user_id:
msg = "Требуется авторизация"
raise GraphQLError(msg)
2023-10-23 17:47:11 +03:00
2025-07-03 00:20:10 +03:00
# Проверяем роль reader
if "reader" not in user_roles and not is_admin:
msg = "У вас нет необходимых прав для доступа"
raise GraphQLError(msg)
2024-11-12 17:56:20 +03:00
2025-07-03 00:20:10 +03:00
logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}")
info.context["roles"] = user_roles
info.context["is_admin"] = is_admin
2024-11-12 17:56:20 +03:00
2025-07-03 00:20:10 +03:00
if token:
info.context["token"] = token
2024-12-12 01:04:11 +03:00
2025-07-03 00:20:10 +03:00
return await f(*args, **kwargs)
2024-11-18 11:31:19 +03:00
2025-07-03 00:20:10 +03:00
return decorated_function
2025-05-29 12:37:39 +03:00
2025-07-03 00:20:10 +03:00
def login_accepted(self, f: Callable) -> Callable:
"""Декоратор для добавления данных авторизации в контекст."""
2024-11-12 17:56:20 +03:00
2025-07-03 00:20:10 +03:00
@wraps(f)
async def decorated_function(*args: Any, **kwargs: Any) -> Any:
info = args[1]
req = info.context.get("request")
logger.debug("login_accepted: Проверка авторизации пользователя.")
user_id, user_roles, is_admin = await self.check_auth(req)
if user_id and user_roles:
logger.info(f"login_accepted: Пользователь авторизован: {user_id} с ролями {user_roles}")
info.context["roles"] = user_roles
info.context["is_admin"] = is_admin
# Автор будет получен в резолвере при необходимости
2024-11-14 14:00:33 +03:00
else:
2025-07-03 00:20:10 +03:00
logger.debug("login_accepted: Пользователь не авторизован")
info.context["roles"] = None
info.context["is_admin"] = False
return await f(*args, **kwargs)
return decorated_function
2025-05-21 01:34:02 +03:00
2025-07-03 00:20:10 +03:00
# Синглтон сервиса
auth_service = AuthService()
2024-11-12 17:56:20 +03:00
2025-07-03 00:20:10 +03:00
# Экспортируем функции для обратной совместимости
check_auth = auth_service.check_auth
add_user_role = auth_service.add_user_role
login_required = auth_service.login_required
login_accepted = auth_service.login_accepted