diff --git a/CHANGELOG.md b/CHANGELOG.md index 0231fe5c..319b46ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### [0.4.6] +- login_accepted decorator added - `docs` added - optimized and unified `load_shouts_*` resolvers with `LoadShoutsOptions` - `load_shouts_bookmarked` resolver fixed diff --git a/auth/authenticate.py b/auth/authenticate.py index 3ccc4756..3ba67832 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -54,12 +54,8 @@ class JWTAuthenticate(AuthenticationBackend): def login_required(func): @wraps(func) async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs): - # debug only - # print('[auth.authenticate] login required for %r with info %r' % (func, info)) auth: AuthCredentials = info.context["request"].auth - # print(auth) if not auth or not auth.logged_in: - # raise Unauthorized(auth.error_message or "Please login") return {"error": "Please login first"} return await func(parent, info, *args, **kwargs) @@ -79,3 +75,22 @@ def permission_required(resource, operation, func): return await func(parent, info, *args, **kwargs) return wrap + + +def login_accepted(func): + @wraps(func) + async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs): + auth: AuthCredentials = info.context["request"].auth + + # Если есть авторизация, добавляем данные автора в контекст + if auth and auth.logged_in: + # Существующие данные auth остаются + pass + else: + # Очищаем данные автора из контекста если авторизация отсутствует + info.context["author"] = None + info.context["user_id"] = None + + return await func(parent, info, *args, **kwargs) + + return wrap diff --git a/auth/usermodel.py b/auth/usermodel.py index ec188d73..804e479c 100644 --- a/auth/usermodel.py +++ b/auth/usermodel.py @@ -95,7 +95,6 @@ class User(Base): ratings = relationship(UserRating, foreign_keys=UserRating.user) roles = relationship(lambda: Role, secondary=UserRole.__tablename__) - def get_permission(self): scope = {} for role in self.roles: diff --git a/resolvers/reaction.py b/resolvers/reaction.py index 6d2bbc9b..755264c9 100644 --- a/resolvers/reaction.py +++ b/resolvers/reaction.py @@ -314,7 +314,7 @@ async def create_reaction(_, info, reaction): shout = session.query(Shout).filter(Shout.id == shout_id).first() if not shout: return {"error": "Shout not found"} - rdict['shout'] = shout.dict() + rdict["shout"] = shout.dict() rdict["created_by"] = author_dict return {"reaction": rdict} except Exception as e: diff --git a/resolvers/reader.py b/resolvers/reader.py index a0508392..a63193c7 100644 --- a/resolvers/reader.py +++ b/resolvers/reader.py @@ -9,6 +9,7 @@ from orm.author import Author from orm.reaction import Reaction, ReactionKind from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.topic import Topic +from services.auth import login_accepted from services.db import json_array_builder, json_builder, local_session from services.schema import query from services.search import search_text @@ -161,8 +162,28 @@ def query_with_stat(info): .group_by(Reaction.shout) .subquery() ) + author_id = info.context.get("author", {}).get("id") + user_reaction_subquery = None + if author_id: + user_reaction_subquery = ( + select(Reaction.shout, Reaction.kind.label("my_rate")) + .where( + and_( + Reaction.created_by == author_id, + Reaction.deleted_at.is_(None), + Reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]), + Reaction.reply_to.is_(None), + ) + ) + .distinct(Reaction.shout) + .subquery() + ) q = q.outerjoin(stats_subquery, stats_subquery.c.shout == Shout.id) + if user_reaction_subquery: + q = q.outerjoin(user_reaction_subquery, user_reaction_subquery.c.shout == Shout.id) + + # Добавляем поле my_rate в JSON q = q.add_columns( json_builder( "comments_count", @@ -171,6 +192,8 @@ def query_with_stat(info): stats_subquery.c.rating, "last_reacted_at", stats_subquery.c.last_reacted_at, + "my_rate", + user_reaction_subquery.c.my_rate if user_reaction_subquery else None, ).label("stat") ) @@ -337,6 +360,7 @@ def apply_sorting(q, options): @query.field("load_shouts_by") +@login_accepted async def load_shouts_by(_, info, options): """ Загрузка публикаций с фильтрацией, сортировкой и пагинацией. @@ -346,7 +370,7 @@ async def load_shouts_by(_, info, options): :param options: Опции фильтрации и сортировки. :return: Список публикаций, удовлетворяющих критериям. """ - # Базовый запрос: если запрашиваются статистические данные, используем специальный запрос с статистикой + # Базовый запрос: используем специальный запрос с статистикой q = query_with_stat(info) q, limit, offset = apply_options(q, options) diff --git a/schema/type.graphql b/schema/type.graphql index f1523eb1..0b80689e 100644 --- a/schema/type.graphql +++ b/schema/type.graphql @@ -112,6 +112,7 @@ type Stat { viewed: Int # followed: Int last_reacted_at: Int + my_rate: ReactionKind } type CommunityStat { diff --git a/services/auth.py b/services/auth.py index bf48b76a..f7e307c8 100644 --- a/services/auth.py +++ b/services/auth.py @@ -94,3 +94,39 @@ def login_required(f): return await f(*args, **kwargs) return decorated_function + + +def login_accepted(f): + """ + Декоратор, который добавляет данные авторизации в контекст, если они доступны, + но не блокирует доступ для неавторизованных пользователей. + """ + + @wraps(f) + async def decorated_function(*args, **kwargs): + info = args[1] + req = info.context.get("request") + + # Пробуем получить данные авторизации + user_id, user_roles = await check_auth(req) + + if user_id and user_roles: + # Если пользователь авторизован, добавляем его данные в контекст + logger.info(f" got {user_id} roles: {user_roles}") + info.context["user_id"] = user_id.strip() + info.context["roles"] = user_roles + + # Пробуем получить профиль автора + author = await get_cached_author_by_user_id(user_id, get_with_stat) + if not author: + logger.warning(f"author profile not found for user {user_id}") + info.context["author"] = author + else: + # Для неавторизованных пользователей очищаем контекст + info.context["user_id"] = None + info.context["roles"] = None + info.context["author"] = None + + return await f(*args, **kwargs) + + return decorated_function