changelog-restored+internal-auth-fix
All checks were successful
Deploy on push / deploy (push) Successful in 6s
All checks were successful
Deploy on push / deploy (push) Successful in 6s
This commit is contained in:
parent
ab65fd4fd8
commit
b01de1fdc1
190
CHANGELOG.md
190
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`
|
||||
|
|
|
@ -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
|
||||
|
|
136
auth/internal.py
136
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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
||||
{/* Информация о связанных объектах при редактировании */}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user