Improve topic sorting: add popular sorting by publications and authors count

This commit is contained in:
2025-06-02 02:56:11 +03:00
parent baca19a4d5
commit 3327976586
113 changed files with 7238 additions and 3739 deletions

View File

@@ -1,5 +1,6 @@
from collections.abc import Callable
from functools import wraps
from typing import Any, Callable, Dict, Optional
from typing import Any, Optional
from graphql import GraphQLError, GraphQLResolveInfo
from sqlalchemy import exc
@@ -7,12 +8,8 @@ from sqlalchemy import exc
from auth.credentials import AuthCredentials
from auth.exceptions import OperationNotAllowed
from auth.internal import authenticate
from auth.jwtcodec import ExpiredToken, InvalidToken, JWTCodec
from auth.orm import Author
from auth.sessions import SessionManager
from auth.tokenstorage import TokenStorage
from services.db import local_session
from services.redis import redis
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
from settings import SESSION_COOKIE_NAME, SESSION_TOKEN_HEADER
from utils.logger import root_logger as logger
@@ -20,7 +17,7 @@ from utils.logger import root_logger as logger
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
def get_safe_headers(request: Any) -> Dict[str, str]:
def get_safe_headers(request: Any) -> dict[str, str]:
"""
Безопасно получает заголовки запроса.
@@ -107,10 +104,9 @@ def get_auth_token(request: Any) -> Optional[str]:
token = auth_header[7:].strip()
logger.debug(f"[decorators] Токен получен из заголовка {SESSION_TOKEN_HEADER}: {len(token)}")
return token
else:
token = auth_header.strip()
logger.debug(f"[decorators] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}: {len(token)}")
return token
token = auth_header.strip()
logger.debug(f"[decorators] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}: {len(token)}")
return token
# Затем проверяем стандартный заголовок Authorization, если основной не определен
if SESSION_TOKEN_HEADER.lower() != "authorization":
@@ -135,7 +131,7 @@ def get_auth_token(request: Any) -> Optional[str]:
return None
async def validate_graphql_context(info: Any) -> None:
async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
"""
Проверяет валидность GraphQL контекста и проверяет авторизацию.
@@ -148,12 +144,14 @@ async def validate_graphql_context(info: Any) -> None:
# Проверка базовой структуры контекста
if info is None or not hasattr(info, "context"):
logger.error("[decorators] Missing GraphQL context information")
raise GraphQLError("Internal server error: missing context")
msg = "Internal server error: missing context"
raise GraphQLError(msg)
request = info.context.get("request")
if not request:
logger.error("[decorators] Missing request in context")
raise GraphQLError("Internal server error: missing request")
msg = "Internal server error: missing request"
raise GraphQLError(msg)
# Проверяем auth из контекста - если уже авторизован, просто возвращаем
auth = getattr(request, "auth", None)
@@ -179,7 +177,8 @@ async def validate_graphql_context(info: Any) -> None:
"headers": get_safe_headers(request),
}
logger.warning(f"[decorators] Токен авторизации не найден: {client_info}")
raise GraphQLError("Unauthorized - please login")
msg = "Unauthorized - please login"
raise GraphQLError(msg)
# Используем единый механизм проверки токена из auth.internal
auth_state = await authenticate(request)
@@ -187,7 +186,8 @@ async def validate_graphql_context(info: Any) -> None:
if not auth_state.logged_in:
error_msg = auth_state.error or "Invalid or expired token"
logger.warning(f"[decorators] Недействительный токен: {error_msg}")
raise GraphQLError(f"Unauthorized - {error_msg}")
msg = f"Unauthorized - {error_msg}"
raise GraphQLError(msg)
# Если все проверки пройдены, создаем AuthCredentials и устанавливаем в request.auth
with local_session() as session:
@@ -198,7 +198,12 @@ async def validate_graphql_context(info: Any) -> None:
# Создаем объект авторизации
auth_cred = AuthCredentials(
author_id=author.id, scopes=scopes, logged_in=True, email=author.email, token=auth_state.token
author_id=author.id,
scopes=scopes,
logged_in=True,
error_message="",
email=author.email,
token=auth_state.token,
)
# Устанавливаем auth в request
@@ -206,7 +211,8 @@ async def validate_graphql_context(info: Any) -> None:
logger.debug(f"[decorators] Токен успешно проверен и установлен для пользователя {auth_state.author_id}")
except exc.NoResultFound:
logger.error(f"[decorators] Пользователь с ID {auth_state.author_id} не найден в базе данных")
raise GraphQLError("Unauthorized - user not found")
msg = "Unauthorized - user not found"
raise GraphQLError(msg)
return
@@ -232,48 +238,59 @@ def admin_auth_required(resolver: Callable) -> Callable:
"""
@wraps(resolver)
async def wrapper(root: Any = None, info: Any = None, **kwargs):
async def wrapper(root: Any = None, info: Optional[GraphQLResolveInfo] = None, **kwargs):
try:
# Проверяем авторизацию пользователя
if info is None:
logger.error("[admin_auth_required] GraphQL info is None")
msg = "Invalid GraphQL context"
raise GraphQLError(msg)
await validate_graphql_context(info)
if info:
# Получаем объект авторизации
auth = info.context["request"].auth
if not auth or not auth.logged_in:
logger.error("[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
msg = "Unauthorized - please login"
raise GraphQLError(msg)
# Получаем объект авторизации
auth = info.context["request"].auth
if not auth or not auth.logged_in:
logger.error(f"[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
raise GraphQLError("Unauthorized - please login")
# Проверяем, является ли пользователь администратором
with local_session() as session:
try:
# Преобразуем author_id в int для совместимости с базой данных
author_id = int(auth.author_id) if auth and auth.author_id else None
if not author_id:
logger.error(f"[admin_auth_required] ID автора не определен: {auth}")
msg = "Unauthorized - invalid user ID"
raise GraphQLError(msg)
# Проверяем, является ли пользователь администратором
with local_session() as session:
try:
# Преобразуем author_id в int для совместимости с базой данных
author_id = int(auth.author_id) if auth and auth.author_id else None
if not author_id:
logger.error(f"[admin_auth_required] ID автора не определен: {auth}")
raise GraphQLError("Unauthorized - invalid user ID")
author = session.query(Author).filter(Author.id == author_id).one()
author = session.query(Author).filter(Author.id == author_id).one()
# Проверяем, является ли пользователь администратором
if author.email in ADMIN_EMAILS:
logger.info(f"Admin access granted for {author.email} (ID: {author.id})")
return await resolver(root, info, **kwargs)
# Проверяем, является ли пользователь администратором
if author.email in ADMIN_EMAILS:
logger.info(f"Admin access granted for {author.email} (ID: {author.id})")
return await resolver(root, info, **kwargs)
# Проверяем роли пользователя
admin_roles = ["admin", "super"]
user_roles = [role.id for role in author.roles] if author.roles else []
# Проверяем роли пользователя
admin_roles = ["admin", "super"]
user_roles = [role.id for role in author.roles] if author.roles else []
if any(role in admin_roles for role in user_roles):
logger.info(
f"Admin access granted for {author.email} (ID: {author.id}) with role: {user_roles}"
)
return await resolver(root, info, **kwargs)
if any(role in admin_roles for role in user_roles):
logger.info(
f"Admin access granted for {author.email} (ID: {author.id}) with role: {user_roles}"
logger.warning(f"Admin access denied for {author.email} (ID: {author.id}). Roles: {user_roles}")
msg = "Unauthorized - not an admin"
raise GraphQLError(msg)
except exc.NoResultFound:
logger.error(
f"[admin_auth_required] Пользователь с ID {auth.author_id} не найден в базе данных"
)
return await resolver(root, info, **kwargs)
logger.warning(f"Admin access denied for {author.email} (ID: {author.id}). Roles: {user_roles}")
raise GraphQLError("Unauthorized - not an admin")
except exc.NoResultFound:
logger.error(f"[admin_auth_required] Пользователь с ID {auth.author_id} не найден в базе данных")
raise GraphQLError("Unauthorized - user not found")
msg = "Unauthorized - user not found"
raise GraphQLError(msg)
except Exception as e:
error_msg = str(e)
@@ -285,18 +302,18 @@ def admin_auth_required(resolver: Callable) -> Callable:
return wrapper
def permission_required(resource: str, operation: str, func):
def permission_required(resource: str, operation: str, func: Callable) -> Callable:
"""
Декоратор для проверки разрешений.
Args:
resource (str): Ресурс для проверки
operation (str): Операция для проверки
resource: Ресурс для проверки
operation: Операция для проверки
func: Декорируемая функция
"""
@wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
async def wrap(parent: Any, info: GraphQLResolveInfo, *args: Any, **kwargs: Any) -> Any:
# Сначала проверяем авторизацию
await validate_graphql_context(info)
@@ -304,8 +321,9 @@ def permission_required(resource: str, operation: str, func):
logger.debug(f"[permission_required] Контекст: {info.context}")
auth = info.context["request"].auth
if not auth or not auth.logged_in:
logger.error(f"[permission_required] Пользователь не авторизован после validate_graphql_context")
raise OperationNotAllowed("Требуются права доступа")
logger.error("[permission_required] Пользователь не авторизован после validate_graphql_context")
msg = "Требуются права доступа"
raise OperationNotAllowed(msg)
# Проверяем разрешения
with local_session() as session:
@@ -313,10 +331,9 @@ def permission_required(resource: str, operation: str, func):
author = session.query(Author).filter(Author.id == auth.author_id).one()
# Проверяем базовые условия
if not author.is_active:
raise OperationNotAllowed("Account is not active")
if author.is_locked():
raise OperationNotAllowed("Account is locked")
msg = "Account is locked"
raise OperationNotAllowed(msg)
# Проверяем, является ли пользователь администратором (у них есть все разрешения)
if author.email in ADMIN_EMAILS:
@@ -338,7 +355,8 @@ def permission_required(resource: str, operation: str, func):
logger.warning(
f"[permission_required] У пользователя {author.email} нет разрешения {operation} на {resource}"
)
raise OperationNotAllowed(f"No permission for {operation} on {resource}")
msg = f"No permission for {operation} on {resource}"
raise OperationNotAllowed(msg)
logger.debug(
f"[permission_required] Пользователь {author.email} имеет разрешение {operation} на {resource}"
@@ -346,12 +364,13 @@ def permission_required(resource: str, operation: str, func):
return await func(parent, info, *args, **kwargs)
except exc.NoResultFound:
logger.error(f"[permission_required] Пользователь с ID {auth.author_id} не найден в базе данных")
raise OperationNotAllowed("User not found")
msg = "User not found"
raise OperationNotAllowed(msg)
return wrap
def login_accepted(func):
def login_accepted(func: Callable) -> Callable:
"""
Декоратор для резолверов, которые могут работать как с авторизованными,
так и с неавторизованными пользователями.
@@ -363,7 +382,7 @@ def login_accepted(func):
"""
@wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
async def wrap(parent: Any, info: GraphQLResolveInfo, *args: Any, **kwargs: Any) -> Any:
try:
# Пробуем проверить авторизацию, но не выбрасываем исключение, если пользователь не авторизован
try: