0.7.5-topicfix

This commit is contained in:
2025-07-03 00:20:10 +03:00
parent 27c5a57709
commit 441cca8045
19 changed files with 2008 additions and 3213 deletions

View File

@@ -1,253 +1,718 @@
"""
Сервис аутентификации с бизнес-логикой для регистрации,
входа и управления сессиями и декорраторами для GraphQL.
"""
import json
import secrets
import time
from functools import wraps
from typing import Any, Callable, Optional
from sqlalchemy import exc
from starlette.requests import Request
from auth.email import send_auth_email
from auth.exceptions import InvalidPassword, InvalidToken, ObjectNotExist
from auth.identity import Identity, Password
from auth.internal import verify_internal_auth
from auth.jwtcodec import JWTCodec
from auth.orm import Author
from auth.tokens.storage import TokenStorage
from cache.cache import get_cached_author_by_id
from resolvers.stat import get_with_stat
from orm.community import Community, CommunityAuthor, CommunityFollower
from services.db import local_session
from settings import SESSION_TOKEN_HEADER
from services.redis import redis
from settings import (
ADMIN_EMAILS,
SESSION_COOKIE_NAME,
SESSION_TOKEN_HEADER,
)
from utils.generate_slug import generate_unique_slug
from utils.logger import root_logger as logger
# Список разрешенных заголовков
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
async def check_auth(req: Request) -> tuple[int, list[str], bool]:
"""
Проверка авторизации пользователя.
class AuthService:
"""Сервис аутентификации с бизнес-логикой"""
Проверяет токен и получает данные из локальной БД.
async def check_auth(self, req: Request) -> tuple[int, list[str], bool]:
"""
Проверка авторизации пользователя.
Параметры:
- req: Входящий GraphQL запрос, содержащий заголовок авторизации.
Проверяет токен и получает данные из локальной БД.
"""
logger.debug("[check_auth] Проверка авторизации...")
Возвращает:
- user_id: str - Идентификатор пользователя
- user_roles: list[str] - Список ролей пользователя
- is_admin: bool - Флаг наличия у пользователя административных прав
"""
logger.debug("[check_auth] Проверка авторизации...")
# Получаем заголовок авторизации
token = None
# Получаем заголовок авторизации
token = None
# Если req is None (в тестах), возвращаем пустые данные
if not req:
logger.debug("[check_auth] Запрос отсутствует (тестовое окружение)")
return 0, [], False
# Если req is None (в тестах), возвращаем пустые данные
if not req:
logger.debug("[check_auth] Запрос отсутствует (тестовое окружение)")
return 0, [], False
# Проверяем заголовок с учетом регистра
headers_dict = dict(req.headers.items())
logger.debug(f"[check_auth] Все заголовки: {headers_dict}")
# Проверяем заголовок с учетом регистра
headers_dict = dict(req.headers.items())
logger.debug(f"[check_auth] Все заголовки: {headers_dict}")
# Ищем заголовок Authorization независимо от регистра
for header_name, header_value in headers_dict.items():
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
token = header_value
logger.debug(f"[check_auth] Найден заголовок {header_name}: {token[:10]}...")
break
# Ищем заголовок Authorization независимо от регистра
for header_name, header_value in headers_dict.items():
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
token = header_value
logger.debug(f"[check_auth] Найден заголовок {header_name}: {token[:10]}...")
break
if not token:
logger.debug("[check_auth] Токен не найден в заголовках")
return 0, [], False
if not token:
logger.debug("[check_auth] Токен не найден в заголовках")
return 0, [], False
# Очищаем токен от префикса Bearer если он есть
if token.startswith("Bearer "):
token = token.split("Bearer ")[-1].strip()
# Очищаем токен от префикса Bearer если он есть
if token.startswith("Bearer "):
token = token.split("Bearer ")[-1].strip()
# Проверяем авторизацию внутренним механизмом
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}"
)
# Проверяем авторизацию внутренним механизмом
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}"
)
# Если в ролях нет админа, но есть ID - проверяем в БД
if user_id and not is_admin:
try:
with local_session() as session:
# Преобразуем user_id в число
try:
if isinstance(user_id, str):
user_id_int = int(user_id.strip())
else:
user_id_int = int(user_id)
except (ValueError, TypeError):
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
else:
# Проверяем наличие админских прав через новую RBAC систему
from orm.community import get_user_roles_in_community
user_roles_in_community = get_user_roles_in_community(user_id_int, community_id=1)
is_admin = any(role in ["admin", "super"] for role in user_roles_in_community)
except Exception as e:
logger.error(f"Ошибка при проверке прав администратора: {e}")
return user_id, user_roles, is_admin
async def add_user_role(self, user_id: str, roles: Optional[list[str]] = None) -> Optional[str]:
"""
Добавление ролей пользователю в локальной БД через CommunityAuthor.
"""
if not roles:
roles = ["author", "reader"]
logger.info(f"Adding roles {roles} to user {user_id}")
from orm.community import assign_role_to_user
logger.debug("Using local authentication with new RBAC system")
with local_session() as session:
try:
author = session.query(Author).filter(Author.id == user_id).one()
# Добавляем роли через новую систему RBAC в дефолтное сообщество (ID=1)
for role_name in roles:
success = assign_role_to_user(int(user_id), role_name, community_id=1)
if success:
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
else:
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
return user_id
except exc.NoResultFound:
logger.error(f"Author {user_id} not found")
return None
def create_user(self, user_dict: dict[str, Any], community_id: int | None = None) -> Author:
"""Создает нового пользователя с дефолтными ролями"""
user = Author(**user_dict)
target_community_id = community_id or 1
with local_session() as session:
session.add(user)
session.flush()
# Получаем сообщество для назначения ролей
community = session.query(Community).filter(Community.id == target_community_id).first()
if not community:
logger.warning(f"Сообщество {target_community_id} не найдено, используем ID=1")
target_community_id = 1
community = session.query(Community).filter(Community.id == target_community_id).first()
if community:
# Инициализируем права сообщества
try:
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(community.initialize_role_permissions())
except Exception as e:
logger.warning(f"Не удалось инициализировать права сообщества: {e}")
# Получаем дефолтные роли
try:
default_roles = community.get_default_roles()
if not default_roles:
default_roles = ["reader", "author"]
except AttributeError:
default_roles = ["reader", "author"]
# Создаем 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)
logger.info(f"Пользователь {user.id} создан с ролями {default_roles}")
session.commit()
return user
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": "Токен истек"}
user_id = payload.user_id
# Получаем автора
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:
user = session.query(Author).filter(Author.email == email).first()
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,
"username": 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)
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:
user = session.query(Author).filter(Author.email == email).first()
if not user:
raise ObjectNotExist("User not found")
try:
from auth.tokens.verification import VerificationTokenManager
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),
username=str(user.username or user.email or user.slug or ""),
device_info={"email": user.email} if hasattr(user, "email") else None,
)
await send_auth_email(user, token, lang, template)
return user
async def confirm_email(self, token: str) -> dict[str, Any]:
"""Подтверждает email по токену"""
try:
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": "Токен не найден или истек"}
user_id = payload.user_id
username = payload.username
with local_session() as session:
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),
username=user.username or user.email or user.slug or username,
device_info=device_info,
)
user.email_verified = True
user.last_seen = int(time.time())
session.add(user)
session.commit()
logger.info(f"Email для пользователя {user_id} подтвержден")
return {"success": True, "token": session_token, "author": user, "error": None}
except InvalidToken as e:
logger.warning(f"Невалидный токен - {e.message}")
return {"success": False, "token": None, "author": None, "error": f"Невалидный токен: {e.message}"}
except Exception as e:
logger.error(f"Ошибка подтверждения email: {e}")
return {"success": False, "token": None, "author": None, "error": f"Ошибка подтверждения email: {e}"}
async def login(self, email: str, password: str, request=None) -> dict[str, Any]:
"""Авторизация пользователя"""
email = email.lower()
logger.info(f"Попытка входа для {email}")
# Если в ролях нет админа, но есть ID - проверяем в БД
if user_id and not is_admin:
try:
with local_session() as session:
# Преобразуем user_id в число
author = session.query(Author).filter(Author.email == email).first()
if not author:
logger.warning(f"Пользователь {email} не найден")
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
# Проверяем роли (упрощенная версия)
has_reader_role = False
if hasattr(author, "roles") and author.roles:
for role in author.roles:
if role.id == "reader":
has_reader_role = True
break
if not has_reader_role and author.email not in ADMIN_EMAILS.split(","):
logger.warning(f"У пользователя {email} нет роли 'reader'")
return {"success": False, "token": None, "author": None, "error": "Нет прав для входа"}
# Проверяем пароль
try:
if isinstance(user_id, str):
user_id_int = int(user_id.strip())
else:
user_id_int = int(user_id)
except (ValueError, TypeError):
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
else:
# Проверяем наличие админских прав через новую RBAC систему
from orm.community import get_user_roles_in_community
valid_author = Identity.password(author, password)
except (InvalidPassword, Exception) as e:
logger.warning(f"Неверный пароль для {email}: {e}")
return {"success": False, "token": None, "author": None, "error": str(e)}
# Создаем токен
username = str(valid_author.username or valid_author.email or valid_author.slug or "")
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,
)
# Обновляем время входа
valid_author.last_seen = int(time.time())
session.commit()
# Устанавливаем cookie если есть request
if request and token:
self._set_auth_cookie(request, token)
try:
author_dict = valid_author.dict(True)
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}
user_roles_in_community = get_user_roles_in_community(user_id_int, community_id=1)
is_admin = any(role in ["admin", "super"] for role in user_roles_in_community)
except Exception as e:
logger.error(f"Ошибка при проверке прав администратора: {e}")
logger.error(f"Ошибка входа для {email}: {e}")
return {"success": False, "token": None, "author": None, "error": f"Ошибка авторизации: {e}"}
return user_id, user_roles, is_admin
async def add_user_role(user_id: str, roles: Optional[list[str]] = None) -> Optional[str]:
"""
Добавление ролей пользователю в локальной БД через CommunityAuthor.
Args:
user_id: ID пользователя
roles: Список ролей для добавления. По умолчанию ["author", "reader"]
"""
if not roles:
roles = ["author", "reader"]
logger.info(f"Adding roles {roles} to user {user_id}")
from orm.community import assign_role_to_user
logger.debug("Using local authentication with new RBAC system")
with local_session() as session:
def _set_auth_cookie(self, request, token: str) -> bool:
"""Устанавливает cookie аутентификации"""
try:
author = session.query(Author).filter(Author.id == user_id).one()
if hasattr(request, "cookies"):
request.cookies[SESSION_COOKIE_NAME] = token
return True
except Exception as e:
logger.error(f"Ошибка установки cookie: {e}")
return False
# Добавляем роли через новую систему RBAC в дефолтное сообщество (ID=1)
for role_name in roles:
success = assign_role_to_user(int(user_id), role_name, community_id=1)
if success:
logger.debug(f"Роль {role_name} добавлена пользователю {user_id}")
else:
logger.warning(f"Не удалось добавить роль {role_name} пользователю {user_id}")
async def logout(self, user_id: str, token: str = None) -> dict[str, Any]:
"""Выход из системы"""
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}"}
return user_id
async def refresh_token(self, user_id: str, old_token: str, device_info: dict = None) -> dict[str, Any]:
"""Обновление токена"""
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": "Не удалось обновить токен"}
except exc.NoResultFound:
logger.error(f"Author {user_id} not found")
return None
# Получаем данные пользователя
with local_session() as session:
author = session.query(Author).filter(Author.id == int(user_id)).first()
if not author:
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
try:
author_dict = author.dict(True)
except Exception:
author_dict = {
"id": author.id,
"email": author.email,
"name": getattr(author, "name", ""),
"slug": getattr(author, "slug", ""),
}
def login_required(f: Callable) -> Callable:
"""Декоратор для проверки авторизации пользователя. Требуется наличие роли 'reader'."""
return {"success": True, "token": new_token, "author": author_dict, "error": None}
@wraps(f)
async def decorated_function(*args: Any, **kwargs: Any) -> Any:
from graphql.error import GraphQLError
except Exception as e:
logger.error(f"Ошибка обновления токена для {user_id}: {e}")
return {"success": False, "token": None, "author": None, "error": str(e)}
info = args[1]
req = info.context.get("request")
async def request_password_reset(self, email: str, lang: str = "ru") -> dict[str, Any]:
"""Запрос сброса пароля"""
try:
email = email.lower()
logger.info(f"Запрос сброса пароля для {email}")
logger.debug(
f"[login_required] Проверка авторизации для запроса: {req.method if req else 'unknown'} {req.url.path if req and hasattr(req, 'url') else 'unknown'}"
)
logger.debug(f"[login_required] Заголовки: {req.headers if req else 'none'}")
with local_session() as session:
author = session.query(Author).filter(Author.email == email).first()
if not author:
logger.warning(f"Пользователь {email} не найден")
return {"success": True} # Для безопасности
# Извлекаем токен из заголовков для сохранения в контексте
token = None
if req:
# Проверяем заголовок с учетом регистра
headers_dict = dict(req.headers.items())
try:
from auth.tokens.verification import VerificationTokenManager
# Ищем заголовок Authorization независимо от регистра
for header_name, header_value in headers_dict.items():
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
token = header_value
logger.debug(
f"[login_required] Найден заголовок {header_name}: {token[:10] if token else 'None'}..."
verification_manager = VerificationTokenManager()
token = await verification_manager.create_verification_token(
str(author.id), "password_reset", {"email": author.email}
)
except (AttributeError, ImportError):
token = await TokenStorage.create_session(
user_id=str(author.id),
username=str(author.username or author.email or author.slug or ""),
device_info={"email": author.email} if hasattr(author, "email") else None,
)
break
# Очищаем токен от префикса Bearer если он есть
if token and token.startswith("Bearer "):
token = token.split("Bearer ")[-1].strip()
await send_auth_email(author, token, lang, "password_reset")
logger.info(f"Письмо сброса пароля отправлено для {email}")
# Для тестового режима: если req отсутствует, но в контексте есть author и roles
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:
# Обычный режим: проверяем через HTTP заголовки
user_id, user_roles, is_admin = await check_auth(req)
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:
user = session.query(Author).filter(Author.email == email).first()
return user is not None
async def update_security(
self, user_id: int, old_password: str, new_password: str = None, email: str = None
) -> dict[str, Any]:
"""Обновление пароля и email"""
try:
with local_session() as session:
author = session.query(Author).filter(Author.id == user_id).first()
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:
existing_user = session.query(Author).filter(Author.email == email).first()
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:
author = session.query(Author).filter(Author.id == user_id).first()
if not author:
return {"success": False, "error": "NOT_AUTHENTICATED", "author": None}
# Проверяем, что новый email не занят
existing_user = session.query(Author).filter(Author.email == new_email).first()
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:
author = session.query(Author).filter(Author.id == user_id).first()
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}
def login_required(self, f: Callable) -> Callable:
"""Декоратор для проверки авторизации пользователя. Требуется наличие роли 'reader'."""
@wraps(f)
async def decorated_function(*args: Any, **kwargs: Any) -> Any:
from graphql.error import GraphQLError
info = args[1]
req = info.context.get("request")
if not user_id:
logger.debug(
f"[login_required] Пользователь не авторизован, req={dict(req) if req else 'None'}, info={info}"
f"[login_required] Проверка авторизации для запроса: {req.method if req else 'unknown'} {req.url.path if req and hasattr(req, 'url') else 'unknown'}"
)
msg = "Требуется авторизация"
raise GraphQLError(msg)
# Проверяем наличие роли reader
if "reader" not in user_roles and not is_admin:
logger.error(f"Пользователь {user_id} не имеет роли 'reader'")
msg = "У вас нет необходимых прав для доступа"
raise GraphQLError(msg)
# Извлекаем токен из заголовков
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
logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}")
info.context["roles"] = user_roles
if token and token.startswith("Bearer "):
token = token.split("Bearer ")[-1].strip()
# Проверяем права администратора
info.context["is_admin"] = is_admin
# Для тестового режима
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 token:
info.context["token"] = token
logger.debug(f"[login_required] Токен сохранен в контексте: {token[:10] if token else 'None'}...")
if not user_id:
msg = "Требуется авторизация"
raise GraphQLError(msg)
# В тестовом режиме автор уже может быть в контексте
if (
not info.context.get("author")
or not isinstance(info.context["author"], dict)
or "dict" not in str(type(info.context["author"]))
):
author = await get_cached_author_by_id(user_id, get_with_stat)
if not author:
logger.error(f"Профиль автора не найден для пользователя {user_id}")
info.context["author"] = author
# Проверяем роль reader
if "reader" not in user_roles and not is_admin:
msg = "У вас нет необходимых прав для доступа"
raise GraphQLError(msg)
return await f(*args, **kwargs)
return decorated_function
def login_accepted(f: Callable) -> Callable:
"""Декоратор для добавления данных авторизации в контекст."""
@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 check_auth(req)
logger.debug(f"login_accepted: user_id={user_id}, user_roles={user_roles}")
if user_id and user_roles:
logger.info(f"login_accepted: Пользователь авторизован: {user_id} с ролями {user_roles}")
logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}")
info.context["roles"] = user_roles
# Проверяем права администратора
info.context["is_admin"] = is_admin
# Пробуем получить профиль автора
author = await get_cached_author_by_id(user_id, get_with_stat)
if author:
logger.debug(f"login_accepted: Найден профиль автора: {author}")
# Используем флаг is_admin из контекста или передаем права владельца для собственных данных
is_owner = True # Пользователь всегда является владельцем собственного профиля
info.context["author"] = author.dict(is_owner or is_admin)
if token:
info.context["token"] = token
# Получаем автора если его нет в контексте
if not info.context.get("author") or not isinstance(info.context["author"], dict):
author = await get_cached_author_by_id(int(user_id), lambda x: x)
if not author:
logger.error(f"Профиль автора не найден для пользователя {user_id}")
info.context["author"] = author
return await f(*args, **kwargs)
return decorated_function
def login_accepted(self, f: Callable) -> Callable:
"""Декоратор для добавления данных авторизации в контекст."""
@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
author = await get_cached_author_by_id(int(user_id), lambda x: x)
if author:
is_owner = True
info.context["author"] = author.dict(is_owner or is_admin)
else:
logger.error(f"login_accepted: Профиль автора не найден для пользователя {user_id}")
else:
logger.error(
f"login_accepted: Профиль автора не найден для пользователя {user_id}. Используем базовые данные."
)
else:
logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.")
info.context["roles"] = None
info.context["author"] = None
info.context["is_admin"] = False
logger.debug("login_accepted: Пользователь не авторизован")
info.context["roles"] = None
info.context["author"] = None
info.context["is_admin"] = False
return await f(*args, **kwargs)
return await f(*args, **kwargs)
return decorated_function
return decorated_function
# Синглтон сервиса
auth_service = AuthService()
# Экспортируем функции для обратной совместимости
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