changelog-restored+internal-auth-fix
All checks were successful
Deploy on push / deploy (push) Successful in 6s

This commit is contained in:
Untone 2025-06-30 23:10:48 +03:00
parent ab65fd4fd8
commit b01de1fdc1
7 changed files with 322 additions and 128 deletions

View File

@ -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`

View File

@ -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

View File

@ -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

View File

@ -66,9 +66,15 @@ export async function query<T = unknown>(
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,

View File

@ -132,15 +132,13 @@ const InviteEditModal: Component<InviteEditModalProps> = (props) => {
<input
type="number"
value={formData().inviter_id}
onInput={(e) => 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 нельзя менять
/>
<div class={formStyles.fieldHint}>
ID автора, который отправляет приглашение
</div>
<div class={formStyles.fieldHint}>ID автора, который отправляет приглашение</div>
{errors().inviter_id && <div class={formStyles.fieldError}>{errors().inviter_id}</div>}
</div>
@ -151,15 +149,13 @@ const InviteEditModal: Component<InviteEditModalProps> = (props) => {
<input
type="number"
value={formData().author_id}
onInput={(e) => 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 нельзя менять
/>
<div class={formStyles.fieldHint}>
ID автора, которого приглашают к сотрудничеству
</div>
<div class={formStyles.fieldHint}>ID автора, которого приглашают к сотрудничеству</div>
{errors().author_id && <div class={formStyles.fieldError}>{errors().author_id}</div>}
</div>
@ -170,15 +166,13 @@ const InviteEditModal: Component<InviteEditModalProps> = (props) => {
<input
type="number"
value={formData().shout_id}
onInput={(e) => 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 нельзя менять
/>
<div class={formStyles.fieldHint}>
ID публикации, к которой приглашают на сотрудничество
</div>
<div class={formStyles.fieldHint}>ID публикации, к которой приглашают на сотрудничество</div>
{errors().shout_id && <div class={formStyles.fieldError}>{errors().shout_id}</div>}
</div>
@ -196,9 +190,7 @@ const InviteEditModal: Component<InviteEditModalProps> = (props) => {
<option value="ACCEPTED">Принято</option>
<option value="REJECTED">Отклонено</option>
</select>
<div class={formStyles.fieldHint}>
Текущий статус приглашения
</div>
<div class={formStyles.fieldHint}>Текущий статус приглашения</div>
</div>
{/* Информация о связанных объектах при редактировании */}

View File

@ -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<InvitesRouteProps> = (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<InvitesRouteProps> = (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<InvitesRouteProps> = (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,

View File

@ -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()