From b01de1fdc1c546c28436baabe68ed9c29f7913c7 Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 30 Jun 2025 23:10:48 +0300 Subject: [PATCH] changelog-restored+internal-auth-fix --- CHANGELOG.md | 190 +++++++++++++++++++++++++++++-- auth/decorators.py | 64 +++++++++-- auth/internal.py | 136 ++++++++-------------- panel/graphql/index.ts | 8 +- panel/modals/InviteEditModal.tsx | 22 ++-- panel/routes/invites.tsx | 24 +++- resolvers/admin.py | 6 +- 7 files changed, 322 insertions(+), 128 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d6a7cc..d8043ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -799,17 +799,10 @@ - error logging added via `logger.error()` #### [0.4.6] -- login_accepted decorator added - `docs` added - optimized and unified `load_shouts_*` resolvers with `LoadShoutsOptions` - `load_shouts_bookmarked` resolver fixed -- resolvers updates: - - new resolvers group `feed` - - `load_shouts_authored_by` resolver added - - `load_shouts_with_topic` resolver added - - `load_shouts_followed` removed - - `load_shouts_random_topic` removed - - `get_topics_random` removed +- refactored with `resolvers/feed` - model updates: - `ShoutsOrderBy` enum added - `Shout.main_topic` from `ShoutTopic.main` as `Topic` type output @@ -827,4 +820,183 @@ - `CommunityFollowerRole` enum added - `InviteStatus` enum added - `Topic.parents` ids added -- ` +- `get_shout` resolver accepts slug or shout_id + +#### [0.4.4] +- `followers_stat` removed for shout +- sqlite3 support added +- `rating_stat` and `commented_stat` fixes + +#### [0.4.3] +- cache reimplemented +- load shouts queries unified +- `followers_stat` removed from shout + +#### [0.4.2] +- reactions load resolvers separated for ratings (no stats) and comments +- reactions stats improved +- `load_comment_ratings` separate resolver + +#### [0.4.1] +- follow/unfollow logic updated and unified with cache + +#### [0.4.0] +- chore: version migrator synced +- feat: precache_data on start +- fix: store id list for following cache data +- fix: shouts stat filter out deleted + +#### [0.3.5] +- cache isolated to services +- topics followers and authors cached +- redis stores lists of ids + +#### [0.3.4] +- `load_authors_by` from cache + +#### [0.3.3] +- feat: sentry integration enabled with glitchtip +- fix: reindex on update shout +- packages upgrade, isort +- separated stats queries for author and topic +- fix: feed featured filter +- fts search removed + +#### [0.3.2] +- redis cache for what author follows +- redis cache for followers +- graphql add query: get topic followers + +#### [0.3.1] +- enabling sentry +- long query log report added +- editor fixes +- authors links cannot be updated by `update_shout` anymore + +#### [0.3.0] +- `Shout.featured_at` timestamp of the frontpage featuring event +- added proposal accepting logics +- schema modulized +- Shout.visibility removed + +#### [0.2.22] +- added precommit hook +- fmt +- granian asgi + +#### [0.2.21] +- fix: rating logix +- fix: `load_top_random_shouts` +- resolvers: `add_stat_*` refactored +- services: use google analytics +- services: minor fixes search + +#### [0.2.20] +- services: ackee removed +- services: following manager fixed +- services: import views.json + +#### [0.2.19] +- fix: adding `author` role +- fix: stripping `user_id` in auth connector + +#### [0.2.18] +- schema: added `Shout.seo` string field +- resolvers: added `/new-author` webhook resolver +- resolvers: added reader.load_shouts_top_random +- resolvers: added reader.load_shouts_unrated +- resolvers: community follower id property name is `.author` +- resolvers: `get_authors_all` and `load_authors_by` +- services: auth connector upgraded + +#### [0.2.17] +- schema: enum types workaround, `ReactionKind`, `InviteStatus`, `ShoutVisibility` +- schema: `Shout.created_by`, `Shout.updated_by` +- schema: `Shout.authors` can be empty +- resolvers: optimized `reacted_shouts_updates` query + +#### [0.2.16] +- resolvers: collab inviting logics +- resolvers: queries and mutations revision and renaming +- resolvers: `delete_topic(slug)` implemented +- resolvers: added `get_shout_followers` +- resolvers: `load_shouts_by` filters implemented +- orm: invite entity +- schema: `Reaction.range` -> `Reaction.quote` +- filters: `time_ago` -> `after` +- httpx -> aiohttp + +#### [0.2.15] +- schema: `Shout.created_by` removed +- schema: `Shout.mainTopic` removed +- services: cached elasticsearch connector +- services: auth is using `user_id` from authorizer +- resolvers: `notify_*` usage fixes +- resolvers: `getAuthor` now accepts slug, `user_id` or `author_id` +- resolvers: login_required usage fixes + +#### [0.2.14] +- schema: some fixes from migrator +- schema: `.days` -> `.time_ago` +- schema: `excludeLayout` + `layout` in filters -> `layouts` +- services: db access simpler, no contextmanager +- services: removed Base.create() method +- services: rediscache updated +- resolvers: get_reacted_shouts_updates as followedReactions query + +#### [0.2.13] +- services: db context manager +- services: `ViewedStorage` fixes +- services: views are not stored in core db anymore +- schema: snake case in model fields names +- schema: no DateTime scalar +- resolvers: `get_my_feed` comments filter reactions body.is_not('') +- resolvers: `get_my_feed` query fix +- resolvers: `LoadReactionsBy.days` -> `LoadReactionsBy.time_ago` +- resolvers: `LoadShoutsBy.days` -> `LoadShoutsBy.time_ago` + +#### [0.2.12] +- `Author.userpic` -> `Author.pic` +- `CommunityFollower.role` is string now +- `Author.user` is string now + +#### [0.2.11] +- redis interface updated +- `viewed` interface updated +- `presence` interface updated +- notify on create, update, delete for reaction and shout +- notify on follow / unfollow author +- use pyproject +- devmode fixed + +#### [0.2.10] +- community resolvers connected + +#### [0.2.9] +- starlette is back, aiohttp removed +- aioredis replaced with aredis + +#### [0.2.8] +- refactored + + +#### [0.2.7] +- `loadFollowedReactions` now with `login_required` +- notifier service api draft +- added `shout` visibility kind in schema +- community isolated from author in orm + + +#### [0.2.6] +- redis connection pool +- auth context fixes +- communities orm, resolvers, schema + + +#### [0.2.5] +- restructured +- all users have their profiles as authors in core +- `gittask`, `inbox` and `auth` logics removed +- `settings` moved to base and now smaller +- new outside auth schema +- removed `gittask`, `auth`, `inbox`, `migration` diff --git a/auth/decorators.py b/auth/decorators.py index a929f653..3d931c55 100644 --- a/auth/decorators.py +++ b/auth/decorators.py @@ -141,29 +141,39 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None: Raises: GraphQLError: если контекст невалиден или пользователь не авторизован """ + # Подробное логирование для диагностики + logger.debug("[validate_graphql_context] Начало проверки контекста и авторизации") + # Проверка базовой структуры контекста if info is None or not hasattr(info, "context"): - logger.error("[decorators] Missing GraphQL context information") + logger.error("[validate_graphql_context] Missing GraphQL context information") msg = "Internal server error: missing context" raise GraphQLError(msg) request = info.context.get("request") if not request: - logger.error("[decorators] Missing request in context") + logger.error("[validate_graphql_context] Missing request in context") msg = "Internal server error: missing request" raise GraphQLError(msg) + # Логируем детали запроса + client_info = { + "ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown", + "headers_keys": list(get_safe_headers(request).keys()), + } + logger.debug(f"[validate_graphql_context] Детали запроса: {client_info}") + # Проверяем auth из контекста - если уже авторизован, просто возвращаем auth = getattr(request, "auth", None) if auth and auth.logged_in: - logger.debug(f"[decorators] Пользователь уже авторизован: {auth.author_id}") + logger.debug(f"[validate_graphql_context] Пользователь уже авторизован через request.auth: {auth.author_id}") return # Если аутентификации нет в request.auth, пробуем получить ее из scope if hasattr(request, "scope") and "auth" in request.scope: auth_cred = request.scope.get("auth") if isinstance(auth_cred, AuthCredentials) and auth_cred.logged_in: - logger.debug(f"[decorators] Пользователь авторизован через scope: {auth_cred.author_id}") + logger.debug(f"[validate_graphql_context] Пользователь авторизован через scope: {auth_cred.author_id}") # Больше не устанавливаем request.auth напрямую return @@ -175,16 +185,22 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None: "ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown", "headers": get_safe_headers(request), } - logger.warning(f"[decorators] Токен авторизации не найден: {client_info}") + logger.warning(f"[validate_graphql_context] Токен авторизации не найден: {client_info}") msg = "Unauthorized - please login" raise GraphQLError(msg) + # Логируем информацию о найденном токене + logger.debug(f"[validate_graphql_context] Токен найден, длина: {len(token)}") + # Используем единый механизм проверки токена из auth.internal auth_state = await authenticate(request) + logger.debug( + f"[validate_graphql_context] Результат аутентификации: logged_in={auth_state.logged_in}, author_id={auth_state.author_id}, error={auth_state.error}" + ) if not auth_state.logged_in: error_msg = auth_state.error or "Invalid or expired token" - logger.warning(f"[decorators] Недействительный токен: {error_msg}") + logger.warning(f"[validate_graphql_context] Недействительный токен: {error_msg}") msg = f"Unauthorized - {error_msg}" raise GraphQLError(msg) @@ -192,6 +208,8 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None: with local_session() as session: try: author = session.query(Author).filter(Author.id == auth_state.author_id).one() + logger.debug(f"[validate_graphql_context] Найден автор: id={author.id}, email={author.email}") + # Получаем разрешения из ролей scopes = author.get_permissions() @@ -209,12 +227,12 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None: if hasattr(request, "scope") and isinstance(request.scope, dict): request.scope["auth"] = auth_cred logger.debug( - f"[decorators] Токен успешно проверен и установлен для пользователя {auth_state.author_id}" + f"[validate_graphql_context] Токен успешно проверен и установлен для пользователя {auth_state.author_id}" ) else: - logger.error("[decorators] Не удалось установить auth: отсутствует request.scope") + logger.error("[validate_graphql_context] Не удалось установить auth: отсутствует request.scope") except exc.NoResultFound: - logger.error(f"[decorators] Пользователь с ID {auth_state.author_id} не найден в базе данных") + logger.error(f"[validate_graphql_context] Пользователь с ID {auth_state.author_id} не найден в базе данных") msg = "Unauthorized - user not found" raise GraphQLError(msg) from None @@ -244,18 +262,43 @@ def admin_auth_required(resolver: Callable) -> Callable: @wraps(resolver) async def wrapper(root: Any = None, info: Optional[GraphQLResolveInfo] = None, **kwargs: dict[str, Any]) -> Any: try: + # Подробное логирование для диагностики + logger.debug(f"[admin_auth_required] Начало проверки авторизации для {resolver.__name__}") + # Проверяем авторизацию пользователя if info is None: logger.error("[admin_auth_required] GraphQL info is None") msg = "Invalid GraphQL context" raise GraphQLError(msg) + # Логируем детали запроса + request = info.context.get("request") + client_info = { + "ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown", + "headers": {k: v for k, v in get_safe_headers(request).items() if k not in ["authorization", "cookie"]}, + } + logger.debug(f"[admin_auth_required] Детали запроса: {client_info}") + + # Проверяем наличие токена до validate_graphql_context + token = get_auth_token(request) + logger.debug(f"[admin_auth_required] Токен найден: {bool(token)}, длина: {len(token) if token else 0}") + + # Проверяем авторизацию await validate_graphql_context(info) + logger.debug("[admin_auth_required] validate_graphql_context успешно пройден") + if info: # Получаем объект авторизации auth = None if hasattr(info.context["request"], "scope") and "auth" in info.context["request"].scope: auth = info.context["request"].scope.get("auth") + logger.debug(f"[admin_auth_required] Auth из scope: {auth.author_id if auth else None}") + elif hasattr(info.context["request"], "auth"): + auth = info.context["request"].auth + logger.debug(f"[admin_auth_required] Auth из request: {auth.author_id if auth else None}") + else: + logger.error("[admin_auth_required] Auth не найден ни в scope, ни в request") + if not auth or not getattr(auth, "logged_in", False): logger.error("[admin_auth_required] Пользователь не авторизован после validate_graphql_context") msg = "Unauthorized - please login" @@ -272,6 +315,7 @@ def admin_auth_required(resolver: Callable) -> Callable: raise GraphQLError(msg) author = session.query(Author).filter(Author.id == author_id).one() + logger.debug(f"[admin_auth_required] Найден автор: {author.id}, {author.email}") # Проверяем, является ли пользователь администратором if author.email in ADMIN_EMAILS: @@ -281,6 +325,7 @@ def admin_auth_required(resolver: Callable) -> Callable: # Проверяем роли пользователя admin_roles = ["admin", "super"] user_roles = [role.id for role in author.roles] if author.roles else [] + logger.debug(f"[admin_auth_required] Роли пользователя: {user_roles}") if any(role in admin_roles for role in user_roles): logger.info( @@ -303,6 +348,7 @@ def admin_auth_required(resolver: Callable) -> Callable: if not isinstance(e, GraphQLError): error_msg = f"Admin access error: {error_msg}" logger.error(f"Error in admin_auth_required: {error_msg}") + logger.error(f"[admin_auth_required] Ошибка авторизации: {error_msg}") raise GraphQLError(error_msg) from e return wrapper diff --git a/auth/internal.py b/auth/internal.py index 3d2f3684..1a33efe5 100644 --- a/auth/internal.py +++ b/auth/internal.py @@ -4,17 +4,15 @@ """ import time -from typing import Any, Optional +from typing import Optional from sqlalchemy.orm import exc -from auth.credentials import AuthCredentials from auth.orm import Author from auth.state import AuthState from auth.tokens.storage import TokenStorage as TokenManager from services.db import local_session 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 ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",") @@ -90,99 +88,63 @@ async def create_internal_session(author: Author, device_info: Optional[dict] = ) -async def authenticate(request: Any) -> AuthState: +async def authenticate(request) -> AuthState: """ - Аутентифицирует запрос по токену из разных источников. - Порядок проверки: - 1. Проверяет токен в заголовке Authorization - 2. Проверяет токен в cookie + Аутентифицирует пользователя по токену из запроса. Args: - request: Запрос (обычно из middleware) + request: Объект запроса Returns: - AuthState: Состояние авторизации + AuthState: Состояние аутентификации """ - state = AuthState() - state.logged_in = False # Изначально считаем, что пользователь не авторизован - token = None + from auth.decorators import get_auth_token + from auth.tokens.sessions import SessionTokenManager + from utils.logger import root_logger as logger - # Проверяем наличие auth в scope (установлено middleware) - if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope: - auth_info = request.scope.get("auth", {}) - if isinstance(auth_info, dict) and "token" in auth_info: - token = auth_info["token"] - logger.debug("[auth.authenticate] Извлечен токен из request.scope['auth']") + logger.debug("[authenticate] Начало аутентификации") - # Если токен не найден в scope, проверяем заголовок + # Получаем токен из запроса + token = get_auth_token(request) if not token: - try: - headers = {} - if hasattr(request, "headers"): - headers = dict(request.headers()) if callable(request.headers) else dict(request.headers) + logger.warning("[authenticate] Токен не найден в запросе") + auth_state = AuthState() + auth_state.logged_in = False + auth_state.author_id = None + auth_state.error = "No authentication token provided" + auth_state.token = None + return auth_state - auth_header = headers.get(SESSION_TOKEN_HEADER, "") - if auth_header and auth_header.startswith("Bearer "): - token = auth_header[7:].strip() - logger.debug(f"[auth.authenticate] Токен получен из заголовка {SESSION_TOKEN_HEADER}") - elif auth_header: - token = auth_header.strip() - logger.debug(f"[auth.authenticate] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}") - except Exception as e: - logger.error(f"[auth.authenticate] Ошибка при доступе к заголовкам: {e}") + logger.debug(f"[authenticate] Токен найден, длина: {len(token)}") - # Если и в заголовке не найден, проверяем cookie - if not token and hasattr(request, "cookies") and request.cookies: - token = request.cookies.get(SESSION_COOKIE_NAME) - if token: - logger.debug(f"[auth.authenticate] Токен получен из cookie {SESSION_COOKIE_NAME}") + # Проверяем токен + try: + # Создаем экземпляр SessionTokenManager + session_manager = SessionTokenManager() + # Проверяем токен + auth_result = await session_manager.verify_session(token) - # Если токен все еще не найден, возвращаем не авторизованное состояние - if not token: - logger.debug("[auth.authenticate] Токен не найден") - return state - - # Проверяем токен через TokenStorage, который теперь совместим с TokenStorage - payload = await TokenManager.verify_session(token) - if not payload: - logger.warning("[auth.authenticate] Токен не валиден: не найдена сессия") - state.error = "Invalid or expired token" - return state - - # Создаем успешное состояние авторизации - state.logged_in = True - state.author_id = payload.user_id - state.token = token - state.username = payload.username - - # Если запрос имеет атрибут auth, устанавливаем в него авторизационные данные - if hasattr(request, "scope") and isinstance(request.scope, dict): - try: - # Получаем информацию о пользователе для создания AuthCredentials - with local_session() as session: - author = session.query(Author).filter(Author.id == payload.user_id).one_or_none() - if author: - # Получаем разрешения из ролей - scopes = author.get_permissions() - - # Создаем объект авторизации - auth_cred = AuthCredentials( - author_id=author.id, - scopes=scopes, - logged_in=True, - email=author.email, - token=token, - error_message="", - ) - - # Устанавливаем auth в request.scope вместо прямого присваивания к request.auth - request.scope["auth"] = auth_cred - logger.debug( - f"[auth.authenticate] Авторизационные данные установлены в request.scope['auth'] для {payload.user_id}" - ) - except Exception as e: - logger.error(f"[auth.authenticate] Ошибка при установке auth в request.scope: {e}") - - logger.info(f"[auth.authenticate] Успешная аутентификация пользователя {state.author_id}") - - return state + if auth_result and hasattr(auth_result, "user_id"): + logger.debug(f"[authenticate] Успешная аутентификация, user_id: {auth_result.user_id}") + auth_state = AuthState() + auth_state.logged_in = True + auth_state.author_id = auth_result.user_id + auth_state.error = None + auth_state.token = token + return auth_state + error_msg = "Invalid or expired token" + logger.warning(f"[authenticate] Недействительный токен: {error_msg}") + auth_state = AuthState() + auth_state.logged_in = False + auth_state.author_id = None + auth_state.error = error_msg + auth_state.token = None + return auth_state + except Exception as e: + logger.error(f"[authenticate] Ошибка при проверке токена: {e}") + auth_state = AuthState() + auth_state.logged_in = False + auth_state.author_id = None + auth_state.error = f"Authentication error: {e!s}" + auth_state.token = None + return auth_state diff --git a/panel/graphql/index.ts b/panel/graphql/index.ts index d1b06d69..0dfa93b0 100644 --- a/panel/graphql/index.ts +++ b/panel/graphql/index.ts @@ -66,9 +66,15 @@ export async function query( console.log(`[GraphQL] Making request to ${endpoint}`) console.log(`[GraphQL] Query: ${query.substring(0, 100)}...`) + // Используем существующую функцию для получения всех необходимых заголовков + const headers = getRequestHeaders() + console.log( + `[GraphQL] Заголовки установлены, Authorization: ${headers['Authorization'] ? 'присутствует' : 'отсутствует'}` + ) + const response = await fetch(endpoint, { method: 'POST', - headers: getRequestHeaders(), + headers, credentials: 'include', body: JSON.stringify({ query, diff --git a/panel/modals/InviteEditModal.tsx b/panel/modals/InviteEditModal.tsx index 00b0306c..9e8bb681 100644 --- a/panel/modals/InviteEditModal.tsx +++ b/panel/modals/InviteEditModal.tsx @@ -132,15 +132,13 @@ const InviteEditModal: Component = (props) => { updateField('inviter_id', parseInt(e.target.value) || 0)} + onInput={(e) => updateField('inviter_id', Number.parseInt(e.target.value) || 0)} class={`${formStyles.input} ${errors().inviter_id ? formStyles.inputError : ''}`} placeholder="1" required disabled={!isCreating()} // При редактировании ID нельзя менять /> -
- ID автора, который отправляет приглашение -
+
ID автора, который отправляет приглашение
{errors().inviter_id &&
{errors().inviter_id}
} @@ -151,15 +149,13 @@ const InviteEditModal: Component = (props) => { updateField('author_id', parseInt(e.target.value) || 0)} + onInput={(e) => updateField('author_id', Number.parseInt(e.target.value) || 0)} class={`${formStyles.input} ${errors().author_id ? formStyles.inputError : ''}`} placeholder="2" required disabled={!isCreating()} // При редактировании ID нельзя менять /> -
- ID автора, которого приглашают к сотрудничеству -
+
ID автора, которого приглашают к сотрудничеству
{errors().author_id &&
{errors().author_id}
} @@ -170,15 +166,13 @@ const InviteEditModal: Component = (props) => { updateField('shout_id', parseInt(e.target.value) || 0)} + onInput={(e) => updateField('shout_id', Number.parseInt(e.target.value) || 0)} class={`${formStyles.input} ${errors().shout_id ? formStyles.inputError : ''}`} placeholder="123" required disabled={!isCreating()} // При редактировании ID нельзя менять /> -
- ID публикации, к которой приглашают на сотрудничество -
+
ID публикации, к которой приглашают на сотрудничество
{errors().shout_id &&
{errors().shout_id}
} @@ -196,9 +190,7 @@ const InviteEditModal: Component = (props) => { -
- Текущий статус приглашения -
+
Текущий статус приглашения
{/* Информация о связанных объектах при редактировании */} diff --git a/panel/routes/invites.tsx b/panel/routes/invites.tsx index 8c0aeeea..b460eaf8 100644 --- a/panel/routes/invites.tsx +++ b/panel/routes/invites.tsx @@ -10,6 +10,7 @@ import styles from '../styles/Table.module.css' import Button from '../ui/Button' import Modal from '../ui/Modal' import Pagination from '../ui/Pagination' +import { getAuthTokenFromCookie } from '../utils/auth' /** * Интерфейсы для приглашений @@ -74,16 +75,21 @@ const InvitesRoute: Component = (props) => { /** * Загружает список приглашений с учетом фильтров и пагинации */ - const loadInvites = async (page: number = 1) => { + const loadInvites = async (page = 1) => { setLoading(true) try { const limit = pagination().perPage const offset = (page - 1) * limit + // Получаем токен авторизации из localStorage или cookie + const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie() + console.log(`[InvitesRoute] Загрузка приглашений, токен: ${authToken ? 'найден' : 'не найден'}`) + const response = await fetch('/graphql', { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: authToken ? `Bearer ${authToken}` : '' }, body: JSON.stringify({ query: ADMIN_GET_INVITES_QUERY, @@ -177,10 +183,15 @@ const InvitesRoute: Component = (props) => { const isCreating = !editModal().invite && createModal().show const mutation = isCreating ? ADMIN_CREATE_INVITE_MUTATION : ADMIN_UPDATE_INVITE_MUTATION + // Получаем токен авторизации из localStorage или cookie + const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie() + console.log(`[InvitesRoute] Сохранение приглашения, токен: ${authToken ? 'найден' : 'не найден'}`) + const response = await fetch('/graphql', { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: authToken ? `Bearer ${authToken}` : '' }, body: JSON.stringify({ query: mutation, @@ -215,10 +226,15 @@ const InvitesRoute: Component = (props) => { */ const deleteInvite = async (invite: Invite) => { try { + // Получаем токен авторизации из localStorage или cookie + const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie() + console.log(`[InvitesRoute] Удаление приглашения, токен: ${authToken ? 'найден' : 'не найден'}`) + const response = await fetch('/graphql', { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Authorization: authToken ? `Bearer ${authToken}` : '' }, body: JSON.stringify({ query: ADMIN_DELETE_INVITE_MUTATION, diff --git a/resolvers/admin.py b/resolvers/admin.py index 81157449..2b8cbe1d 100644 --- a/resolvers/admin.py +++ b/resolvers/admin.py @@ -3,7 +3,7 @@ from typing import Any from graphql import GraphQLResolveInfo from graphql.error import GraphQLError -from sqlalchemy import String, cast, or_ +from sqlalchemy import String, cast, null, or_ from sqlalchemy.orm import joinedload from sqlalchemy.sql import func, select @@ -670,8 +670,8 @@ async def admin_restore_shout(_: None, info: GraphQLResolveInfo, shout_id: int) return {"success": False, "error": "Публикация не была удалена"} # Сбрасываем время удаления - shout.deleted_at = None - shout.deleted_by = None + shout.deleted_at = null() + shout.deleted_by = null() session.commit()