auth fixes, search connected
This commit is contained in:
parent
32bc1276e0
commit
ab39b534fe
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -1,5 +1,27 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Добавлено
|
||||||
|
- Статистика пользователя (shouts, followers, authors, comments) в ответе метода `getSession`
|
||||||
|
- Интеграция с функцией `get_with_stat` для единого подхода к получению статистики
|
||||||
|
|
||||||
|
### Исправлено
|
||||||
|
- Ошибка в функции `unpublish_shout`:
|
||||||
|
- Исправлена проверка наличия связанного черновика: `if shout.draft is not None`
|
||||||
|
- Правильное получение черновика через его ID с загрузкой связей
|
||||||
|
- Добавлена реализация функции `unpublish_draft`:
|
||||||
|
- Корректная работа с идентификаторами draft и связанного shout
|
||||||
|
- Снятие shout с публикации по ID черновика
|
||||||
|
- Обновление кэша после снятия с публикации
|
||||||
|
- Ошибка в функции `get_shouts_with_links`:
|
||||||
|
- Добавлена корректная обработка полей `updated_by` и `deleted_by`, которые могут быть null
|
||||||
|
- Исправлена ошибка "Cannot return null for non-nullable field Author.id"
|
||||||
|
- Добавлена проверка существования авторов для полей `updated_by` и `deleted_by`
|
||||||
|
- Ошибка в функции `get_reactions_with_stat`:
|
||||||
|
- Добавлен вызов метода `distinct()` перед применением `limit` и `offset` для предотвращения дублирования результатов
|
||||||
|
- Улучшена документация функции с описанием обработки результатов запроса
|
||||||
|
- Оптимизирована сортировка и группировка результатов для корректной работы с joined eager loads
|
||||||
|
|
||||||
#### [0.4.23] - 2025-05-25
|
#### [0.4.23] - 2025-05-25
|
||||||
|
|
||||||
|
|
|
@ -376,16 +376,13 @@ def login_accepted(func):
|
||||||
try:
|
try:
|
||||||
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
||||||
info.context["author"] = author.dict()
|
info.context["author"] = author.dict()
|
||||||
info.context["user_id"] = author.id
|
|
||||||
logger.debug(f"[login_accepted] Пользователь авторизован: {author.id}")
|
logger.debug(f"[login_accepted] Пользователь авторизован: {author.id}")
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
logger.warning(f"[login_accepted] Пользователь с ID {auth.author_id} не найден в базе данных")
|
logger.warning(f"[login_accepted] Пользователь с ID {auth.author_id} не найден в базе данных")
|
||||||
info.context["author"] = None
|
info.context["author"] = None
|
||||||
info.context["user_id"] = None
|
|
||||||
else:
|
else:
|
||||||
# Если пользователь не авторизован, устанавливаем пустые значения
|
# Если пользователь не авторизован, устанавливаем пустые значения
|
||||||
info.context["author"] = None
|
info.context["author"] = None
|
||||||
info.context["user_id"] = None
|
|
||||||
logger.debug("[login_accepted] Пользователь не авторизован")
|
logger.debug("[login_accepted] Пользователь не авторизован")
|
||||||
|
|
||||||
return await func(parent, info, *args, **kwargs)
|
return await func(parent, info, *args, **kwargs)
|
||||||
|
|
54
auth/handler.py
Normal file
54
auth/handler.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
from ariadne.asgi.handlers import GraphQLHTTPHandler
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import Response, JSONResponse
|
||||||
|
from auth.middleware import auth_middleware
|
||||||
|
from utils.logger import root_logger as logger
|
||||||
|
|
||||||
|
class EnhancedGraphQLHTTPHandler(GraphQLHTTPHandler):
|
||||||
|
"""
|
||||||
|
Улучшенный GraphQL HTTP обработчик с поддержкой cookie и авторизации.
|
||||||
|
|
||||||
|
Расширяет стандартный GraphQLHTTPHandler для:
|
||||||
|
1. Создания расширенного контекста запроса с авторизационными данными
|
||||||
|
2. Корректной обработки ответов с cookie и headers
|
||||||
|
3. Интеграции с AuthMiddleware
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def get_context_for_request(self, request: Request, data: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Расширяем контекст для GraphQL запросов.
|
||||||
|
|
||||||
|
Добавляет к стандартному контексту:
|
||||||
|
- Объект response для установки cookie
|
||||||
|
- Интеграцию с AuthMiddleware
|
||||||
|
- Расширения для управления авторизацией
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Starlette Request объект
|
||||||
|
data: данные запроса
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: контекст с дополнительными данными для авторизации и cookie
|
||||||
|
"""
|
||||||
|
# Получаем стандартный контекст от базового класса
|
||||||
|
context = await super().get_context_for_request(request, data)
|
||||||
|
|
||||||
|
# Создаем объект ответа для установки cookie
|
||||||
|
response = JSONResponse({})
|
||||||
|
context["response"] = response
|
||||||
|
|
||||||
|
# Интегрируем с AuthMiddleware
|
||||||
|
auth_middleware.set_context(context)
|
||||||
|
context["extensions"] = auth_middleware
|
||||||
|
|
||||||
|
# Добавляем данные авторизации только если они доступны
|
||||||
|
# Без проверки hasattr, так как это вызывает ошибку до обработки AuthenticationMiddleware
|
||||||
|
if hasattr(request, "auth") and request.auth:
|
||||||
|
# Используем request.auth вместо request.user, так как user еще не доступен
|
||||||
|
context["auth"] = request.auth
|
||||||
|
# Безопасно логируем информацию о типе объекта auth
|
||||||
|
logger.debug(f"[graphql] Добавлены данные авторизации в контекст: {type(request.auth).__name__}")
|
||||||
|
|
||||||
|
logger.debug(f"[graphql] Подготовлен расширенный контекст для запроса")
|
||||||
|
|
||||||
|
return context
|
|
@ -1,11 +1,13 @@
|
||||||
"""
|
"""
|
||||||
Middleware для обработки авторизации в GraphQL запросах
|
Middleware для обработки авторизации в GraphQL запросах
|
||||||
"""
|
"""
|
||||||
|
from typing import Any, Dict
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse, Response
|
||||||
from starlette.datastructures import Headers
|
from starlette.datastructures import Headers
|
||||||
from starlette.types import ASGIApp, Scope, Receive, Send
|
from starlette.types import ASGIApp, Scope, Receive, Send
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
from settings import SESSION_TOKEN_HEADER, SESSION_COOKIE_NAME
|
from settings import SESSION_COOKIE_HTTPONLY, SESSION_COOKIE_MAX_AGE, SESSION_COOKIE_SAMESITE, SESSION_COOKIE_SECURE, SESSION_TOKEN_HEADER, SESSION_COOKIE_NAME
|
||||||
|
|
||||||
|
|
||||||
class AuthMiddleware:
|
class AuthMiddleware:
|
||||||
|
@ -197,3 +199,76 @@ class AuthMiddleware:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[AuthMiddleware] Ошибка в GraphQL resolve: {str(e)}")
|
logger.error(f"[AuthMiddleware] Ошибка в GraphQL resolve: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
async def process_result(self, request: Request, result: Any) -> Response:
|
||||||
|
"""
|
||||||
|
Обрабатывает результат GraphQL запроса, поддерживая установку cookie
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Starlette Request объект
|
||||||
|
result: результат GraphQL запроса (dict или Response)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: HTTP-ответ с результатом и cookie (если необходимо)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Проверяем, является ли result уже объектом Response
|
||||||
|
if isinstance(result, Response):
|
||||||
|
response = result
|
||||||
|
# Пытаемся получить данные из response для проверки логина/логаута
|
||||||
|
result_data = {}
|
||||||
|
if isinstance(result, JSONResponse):
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
result_data = json.loads(result.body.decode('utf-8'))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[process_result] Не удалось извлечь данные из JSONResponse: {str(e)}")
|
||||||
|
else:
|
||||||
|
response = JSONResponse(result)
|
||||||
|
result_data = result
|
||||||
|
|
||||||
|
# Проверяем, был ли токен в запросе или ответе
|
||||||
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
op_name = data.get("operationName", "").lower()
|
||||||
|
|
||||||
|
# Если это операция логина или обновления токена, и в ответе есть токен
|
||||||
|
if op_name in ["login", "refreshtoken"]:
|
||||||
|
token = None
|
||||||
|
# Пытаемся извлечь токен из данных ответа
|
||||||
|
if result_data and isinstance(result_data, dict):
|
||||||
|
data_obj = result_data.get("data", {})
|
||||||
|
if isinstance(data_obj, dict) and op_name in data_obj:
|
||||||
|
op_result = data_obj.get(op_name, {})
|
||||||
|
if isinstance(op_result, dict) and "token" in op_result:
|
||||||
|
token = op_result.get("token")
|
||||||
|
|
||||||
|
if token:
|
||||||
|
# Устанавливаем cookie с токеном
|
||||||
|
response.set_cookie(
|
||||||
|
key=SESSION_COOKIE_NAME,
|
||||||
|
value=token,
|
||||||
|
httponly=SESSION_COOKIE_HTTPONLY,
|
||||||
|
secure=SESSION_COOKIE_SECURE,
|
||||||
|
samesite=SESSION_COOKIE_SAMESITE,
|
||||||
|
max_age=SESSION_COOKIE_MAX_AGE,
|
||||||
|
)
|
||||||
|
logger.debug(f"[graphql_handler] Установлена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
||||||
|
|
||||||
|
# Если это операция logout, удаляем cookie
|
||||||
|
elif op_name == "logout":
|
||||||
|
response.delete_cookie(
|
||||||
|
key=SESSION_COOKIE_NAME,
|
||||||
|
secure=SESSION_COOKIE_SECURE,
|
||||||
|
httponly=SESSION_COOKIE_HTTPONLY,
|
||||||
|
samesite=SESSION_COOKIE_SAMESITE
|
||||||
|
)
|
||||||
|
logger.debug(f"[graphql_handler] Удалена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[process_result] Ошибка при обработке POST запроса: {str(e)}")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Создаем единый экземпляр AuthMiddleware для использования с GraphQL
|
||||||
|
auth_middleware = AuthMiddleware(lambda scope, receive, send: None)
|
7
cache/cache.py
vendored
7
cache/cache.py
vendored
|
@ -301,7 +301,7 @@ async def get_cached_follower_topics(author_id: int):
|
||||||
|
|
||||||
|
|
||||||
# Get author by user ID from cache
|
# Get author by user ID from cache
|
||||||
async def get_cached_author_by_user_id(user_id: str, get_with_stat):
|
async def get_cached_author_by_id(user_id: str, get_with_stat):
|
||||||
"""
|
"""
|
||||||
Retrieve author information by user_id, checking the cache first, then the database.
|
Retrieve author information by user_id, checking the cache first, then the database.
|
||||||
|
|
||||||
|
@ -312,7 +312,7 @@ async def get_cached_author_by_user_id(user_id: str, get_with_stat):
|
||||||
dict: Dictionary with author data or None if not found.
|
dict: Dictionary with author data or None if not found.
|
||||||
"""
|
"""
|
||||||
# Attempt to find author ID by user_id in Redis cache
|
# Attempt to find author ID by user_id in Redis cache
|
||||||
author_id = await redis.execute("GET", f"author:user:{user_id.strip()}")
|
author_id = await redis.execute("GET", f"author:user:{author_id}")
|
||||||
if author_id:
|
if author_id:
|
||||||
# If ID is found, get full author data by ID
|
# If ID is found, get full author data by ID
|
||||||
author_data = await redis.execute("GET", f"author:id:{author_id}")
|
author_data = await redis.execute("GET", f"author:id:{author_id}")
|
||||||
|
@ -320,14 +320,13 @@ async def get_cached_author_by_user_id(user_id: str, get_with_stat):
|
||||||
return orjson.loads(author_data)
|
return orjson.loads(author_data)
|
||||||
|
|
||||||
# If data is not found in cache, query the database
|
# If data is not found in cache, query the database
|
||||||
author_query = select(Author).where(Author.id == user_id)
|
author_query = select(Author).where(Author.id == author_id)
|
||||||
authors = get_with_stat(author_query)
|
authors = get_with_stat(author_query)
|
||||||
if authors:
|
if authors:
|
||||||
# Cache the retrieved author data
|
# Cache the retrieved author data
|
||||||
author = authors[0]
|
author = authors[0]
|
||||||
author_dict = author.dict()
|
author_dict = author.dict()
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
redis.execute("SET", f"author:user:{user_id.strip()}", str(author.id)),
|
|
||||||
redis.execute("SET", f"author:id:{author.id}", orjson.dumps(author_dict)),
|
redis.execute("SET", f"author:id:{author.id}", orjson.dumps(author_dict)),
|
||||||
)
|
)
|
||||||
return author_dict
|
return author_dict
|
||||||
|
|
347
main.py
347
main.py
|
@ -5,35 +5,32 @@ from os.path import exists, join
|
||||||
|
|
||||||
from ariadne import load_schema_from_path, make_executable_schema
|
from ariadne import load_schema_from_path, make_executable_schema
|
||||||
from ariadne.asgi import GraphQL
|
from ariadne.asgi import GraphQL
|
||||||
from ariadne.asgi.handlers import GraphQLHTTPHandler
|
|
||||||
|
from auth.handler import EnhancedGraphQLHTTPHandler
|
||||||
|
from auth.internal import InternalAuthentication
|
||||||
|
from auth.middleware import auth_middleware, AuthMiddleware
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||||
from starlette.middleware import Middleware
|
from starlette.middleware import Middleware
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import FileResponse, JSONResponse, Response
|
from starlette.responses import JSONResponse, Response
|
||||||
from starlette.routing import Route, Mount
|
from starlette.routing import Route, Mount
|
||||||
from starlette.staticfiles import StaticFiles
|
from starlette.staticfiles import StaticFiles
|
||||||
from starlette.types import ASGIApp
|
|
||||||
|
|
||||||
from cache.precache import precache_data
|
from cache.precache import precache_data
|
||||||
from cache.revalidator import revalidation_manager
|
from cache.revalidator import revalidation_manager
|
||||||
from services.exception import ExceptionHandlerMiddleware
|
from services.exception import ExceptionHandlerMiddleware
|
||||||
from services.redis import redis
|
from services.redis import redis
|
||||||
from services.schema import create_all_tables, resolvers
|
from services.schema import create_all_tables, resolvers
|
||||||
from services.search import search_service, initialize_search_index
|
from services.search import check_search_service, initialize_search_index_background, search_service
|
||||||
|
from services.viewed import ViewedStorage
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
from auth.internal import InternalAuthentication
|
from settings import DEV_SERVER_PID_FILE_NAME
|
||||||
from auth.middleware import AuthMiddleware
|
|
||||||
from settings import (
|
DEVMODE = os.getenv("DOKKU_APP_TYPE", "false").lower() == "false"
|
||||||
SESSION_COOKIE_NAME,
|
DIST_DIR = join(os.path.dirname(__file__), "dist") # Директория для собранных файлов
|
||||||
SESSION_COOKIE_HTTPONLY,
|
INDEX_HTML = join(os.path.dirname(__file__), "index.html")
|
||||||
SESSION_COOKIE_SECURE,
|
|
||||||
SESSION_COOKIE_SAMESITE,
|
|
||||||
SESSION_COOKIE_MAX_AGE,
|
|
||||||
SESSION_TOKEN_HEADER,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Импортируем резолверы
|
# Импортируем резолверы
|
||||||
import_module("resolvers")
|
import_module("resolvers")
|
||||||
|
@ -41,118 +38,6 @@ import_module("resolvers")
|
||||||
# Создаем схему GraphQL
|
# Создаем схему GraphQL
|
||||||
schema = make_executable_schema(load_schema_from_path("schema/"), resolvers)
|
schema = make_executable_schema(load_schema_from_path("schema/"), resolvers)
|
||||||
|
|
||||||
# Пути к клиентским файлам
|
|
||||||
DIST_DIR = join(os.path.dirname(__file__), "dist") # Директория для собранных файлов
|
|
||||||
INDEX_HTML = join(os.path.dirname(__file__), "index.html")
|
|
||||||
|
|
||||||
|
|
||||||
async def check_search_service():
|
|
||||||
"""Check if search service is available and log result"""
|
|
||||||
info = await search_service.info()
|
|
||||||
if info.get("status") in ["error", "unavailable"]:
|
|
||||||
print(f"[WARNING] Search service unavailable: {info.get('message', 'unknown reason')}")
|
|
||||||
else:
|
|
||||||
print(f"[INFO] Search service is available: {info}")
|
|
||||||
|
|
||||||
|
|
||||||
async def index_handler(request: Request):
|
|
||||||
"""
|
|
||||||
Раздача основного HTML файла
|
|
||||||
"""
|
|
||||||
return FileResponse(INDEX_HTML)
|
|
||||||
|
|
||||||
|
|
||||||
# Создаем единый экземпляр AuthMiddleware для использования с GraphQL
|
|
||||||
auth_middleware = AuthMiddleware(lambda scope, receive, send: None)
|
|
||||||
|
|
||||||
|
|
||||||
class EnhancedGraphQLHTTPHandler(GraphQLHTTPHandler):
|
|
||||||
"""
|
|
||||||
Улучшенный GraphQL HTTP обработчик с поддержкой cookie и авторизации
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def get_context_for_request(self, request: Request, data: dict) -> dict:
|
|
||||||
"""
|
|
||||||
Расширяем контекст для GraphQL запросов
|
|
||||||
"""
|
|
||||||
# Получаем стандартный контекст от базового класса
|
|
||||||
context = await super().get_context_for_request(request, data)
|
|
||||||
|
|
||||||
# Создаем объект ответа для установки cookie
|
|
||||||
response = JSONResponse({})
|
|
||||||
context["response"] = response
|
|
||||||
|
|
||||||
# Интегрируем с AuthMiddleware
|
|
||||||
auth_middleware.set_context(context)
|
|
||||||
context["extensions"] = auth_middleware
|
|
||||||
|
|
||||||
logger.debug(f"[graphql] Подготовлен расширенный контекст для запроса")
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
async def process_result(self, request: Request, result: dict) -> Response:
|
|
||||||
"""
|
|
||||||
Обрабатывает результат GraphQL запроса, поддерживая установку cookie
|
|
||||||
"""
|
|
||||||
# Получаем контекст запроса
|
|
||||||
context = getattr(request, "context", {})
|
|
||||||
|
|
||||||
# Получаем заранее созданный response из контекста
|
|
||||||
response = context.get("response")
|
|
||||||
|
|
||||||
if not response or not isinstance(response, Response):
|
|
||||||
# Если response не найден или не является объектом Response, создаем новый
|
|
||||||
response = await super().process_result(request, result)
|
|
||||||
else:
|
|
||||||
# Обновляем тело ответа данными из результата GraphQL
|
|
||||||
response.body = self.encode_json(result)
|
|
||||||
response.headers["content-type"] = "application/json"
|
|
||||||
response.headers["content-length"] = str(len(response.body))
|
|
||||||
|
|
||||||
logger.debug(f"[graphql] Подготовлен ответ с типом {type(response).__name__}")
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
# Функция запуска сервера
|
|
||||||
async def start():
|
|
||||||
"""Запуск сервера и инициализация данных"""
|
|
||||||
# Инициализируем соединение с Redis
|
|
||||||
await redis.connect()
|
|
||||||
logger.info("Установлено соединение с Redis")
|
|
||||||
|
|
||||||
# Создаем все таблицы в БД
|
|
||||||
create_all_tables()
|
|
||||||
|
|
||||||
# Запускаем предварительное кеширование данных
|
|
||||||
asyncio.create_task(precache_data())
|
|
||||||
|
|
||||||
# Запускаем задачу ревалидации кеша
|
|
||||||
asyncio.create_task(revalidation_manager.start())
|
|
||||||
|
|
||||||
# Выводим сообщение о запуске сервера и доступности API
|
|
||||||
logger.info("Сервер запущен и готов принимать запросы")
|
|
||||||
logger.info("GraphQL API доступно по адресу: /graphql")
|
|
||||||
logger.info("Админ-панель доступна по адресу: http://127.0.0.1:8000/")
|
|
||||||
|
|
||||||
|
|
||||||
# Функция остановки сервера
|
|
||||||
async def shutdown():
|
|
||||||
"""Остановка сервера и освобождение ресурсов"""
|
|
||||||
logger.info("Остановка сервера")
|
|
||||||
|
|
||||||
# Закрываем соединение с Redis
|
|
||||||
await redis.disconnect()
|
|
||||||
|
|
||||||
# Останавливаем поисковый сервис
|
|
||||||
search_service.close()
|
|
||||||
|
|
||||||
# Удаляем PID-файл, если он существует
|
|
||||||
from settings import DEV_SERVER_PID_FILE_NAME
|
|
||||||
if exists(DEV_SERVER_PID_FILE_NAME):
|
|
||||||
os.unlink(DEV_SERVER_PID_FILE_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
# Создаем middleware с правильным порядком
|
# Создаем middleware с правильным порядком
|
||||||
middleware = [
|
middleware = [
|
||||||
# Начинаем с обработки ошибок
|
# Начинаем с обработки ошибок
|
||||||
|
@ -172,9 +57,9 @@ middleware = [
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
),
|
),
|
||||||
# После CORS идёт обработка авторизации
|
# Сначала AuthMiddleware (для обработки токенов)
|
||||||
Middleware(AuthMiddleware),
|
Middleware(AuthMiddleware),
|
||||||
# И затем аутентификация
|
# Затем AuthenticationMiddleware (для создания request.user на основе токена)
|
||||||
Middleware(AuthenticationMiddleware, backend=InternalAuthentication()),
|
Middleware(AuthenticationMiddleware, backend=InternalAuthentication()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -182,81 +67,169 @@ middleware = [
|
||||||
# Создаем экземпляр GraphQL с улучшенным обработчиком
|
# Создаем экземпляр GraphQL с улучшенным обработчиком
|
||||||
graphql_app = GraphQL(
|
graphql_app = GraphQL(
|
||||||
schema,
|
schema,
|
||||||
debug=True,
|
debug=DEVMODE,
|
||||||
http_handler=EnhancedGraphQLHTTPHandler()
|
http_handler=EnhancedGraphQLHTTPHandler()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
|
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
|
||||||
async def graphql_handler(request: Request):
|
async def graphql_handler(request: Request):
|
||||||
|
"""
|
||||||
|
Обработчик GraphQL запросов с поддержкой middleware и обработкой ошибок.
|
||||||
|
|
||||||
|
Выполняет:
|
||||||
|
1. Проверку метода запроса (GET, POST, OPTIONS)
|
||||||
|
2. Обработку GraphQL запроса через ariadne
|
||||||
|
3. Применение middleware для корректной обработки cookie и авторизации
|
||||||
|
4. Обработку исключений и формирование ответа
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Starlette Request объект
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: объект ответа (обычно JSONResponse)
|
||||||
|
"""
|
||||||
if request.method not in ["GET", "POST", "OPTIONS"]:
|
if request.method not in ["GET", "POST", "OPTIONS"]:
|
||||||
return JSONResponse({"error": "Method Not Allowed by main.py"}, status_code=405)
|
return JSONResponse({"error": "Method Not Allowed by main.py"}, status_code=405)
|
||||||
|
|
||||||
|
# Проверяем, что все необходимые middleware корректно отработали
|
||||||
|
if not hasattr(request, "scope") or "auth" not in request.scope:
|
||||||
|
logger.warning("[graphql] AuthMiddleware не обработал запрос перед GraphQL обработчиком")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Обрабатываем CORS для OPTIONS запросов
|
# Обрабатываем запрос через GraphQL приложение
|
||||||
if request.method == "OPTIONS":
|
|
||||||
response = JSONResponse({})
|
|
||||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
|
||||||
response.headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS"
|
|
||||||
response.headers["Access-Control-Allow-Headers"] = "*"
|
|
||||||
response.headers["Access-Control-Allow-Credentials"] = "true"
|
|
||||||
response.headers["Access-Control-Max-Age"] = "86400" # 24 hours
|
|
||||||
return response
|
|
||||||
|
|
||||||
result = await graphql_app.handle_request(request)
|
result = await graphql_app.handle_request(request)
|
||||||
|
|
||||||
# Если результат не является Response, преобразуем его в JSONResponse
|
# Применяем middleware для установки cookie
|
||||||
if not isinstance(result, Response):
|
# Используем метод process_result из auth_middleware для корректной обработки
|
||||||
response = JSONResponse(result)
|
# cookie на основе результатов операций login/logout
|
||||||
|
response = await auth_middleware.process_result(request, result)
|
||||||
# Проверяем, был ли токен в запросе или ответе
|
return response
|
||||||
if request.method == "POST" and isinstance(result, dict):
|
|
||||||
data = await request.json()
|
|
||||||
op_name = data.get("operationName", "").lower()
|
|
||||||
|
|
||||||
# Если это операция логина или обновления токена, и в ответе есть токен
|
|
||||||
if (op_name in ["login", "refreshtoken"]) and result.get("data", {}).get(op_name, {}).get("token"):
|
|
||||||
token = result["data"][op_name]["token"]
|
|
||||||
# Устанавливаем cookie с токеном
|
|
||||||
response.set_cookie(
|
|
||||||
key=SESSION_COOKIE_NAME,
|
|
||||||
value=token,
|
|
||||||
httponly=SESSION_COOKIE_HTTPONLY,
|
|
||||||
secure=SESSION_COOKIE_SECURE,
|
|
||||||
samesite=SESSION_COOKIE_SAMESITE,
|
|
||||||
max_age=SESSION_COOKIE_MAX_AGE,
|
|
||||||
)
|
|
||||||
logger.debug(f"[graphql_handler] Установлена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
|
||||||
|
|
||||||
# Если это операция logout, удаляем cookie
|
|
||||||
elif op_name == "logout":
|
|
||||||
response.delete_cookie(
|
|
||||||
key=SESSION_COOKIE_NAME,
|
|
||||||
secure=SESSION_COOKIE_SECURE,
|
|
||||||
httponly=SESSION_COOKIE_HTTPONLY,
|
|
||||||
samesite=SESSION_COOKIE_SAMESITE
|
|
||||||
)
|
|
||||||
logger.debug(f"[graphql_handler] Удалена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
return result
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
return JSONResponse({"error": "Request cancelled"}, status_code=499)
|
return JSONResponse({"error": "Request cancelled"}, status_code=499)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"GraphQL error: {str(e)}")
|
logger.error(f"GraphQL error: {str(e)}")
|
||||||
|
# Логируем более подробную информацию для отладки
|
||||||
|
import traceback
|
||||||
|
logger.debug(f"GraphQL error traceback: {traceback.format_exc()}")
|
||||||
return JSONResponse({"error": str(e)}, status_code=500)
|
return JSONResponse({"error": str(e)}, status_code=500)
|
||||||
|
|
||||||
# Добавляем маршруты, порядок имеет значение
|
|
||||||
routes = [
|
|
||||||
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
|
|
||||||
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True)),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Создаем приложение Starlette с маршрутами и middleware
|
|
||||||
|
async def shutdown():
|
||||||
|
"""Остановка сервера и освобождение ресурсов"""
|
||||||
|
logger.info("Остановка сервера")
|
||||||
|
|
||||||
|
# Закрываем соединение с Redis
|
||||||
|
await redis.disconnect()
|
||||||
|
|
||||||
|
# Останавливаем поисковый сервис
|
||||||
|
search_service.close()
|
||||||
|
|
||||||
|
# Удаляем PID-файл, если он существует
|
||||||
|
from settings import DEV_SERVER_PID_FILE_NAME
|
||||||
|
if exists(DEV_SERVER_PID_FILE_NAME):
|
||||||
|
os.unlink(DEV_SERVER_PID_FILE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
async def dev_start():
|
||||||
|
"""
|
||||||
|
Инициализация сервера в DEV режиме.
|
||||||
|
|
||||||
|
Функция:
|
||||||
|
1. Проверяет наличие DEV режима
|
||||||
|
2. Создает PID-файл для отслеживания процесса
|
||||||
|
3. Логирует информацию о старте сервера
|
||||||
|
|
||||||
|
Используется только при запуске сервера с флагом "dev".
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
pid_path = DEV_SERVER_PID_FILE_NAME
|
||||||
|
# Если PID-файл уже существует, проверяем, не запущен ли уже сервер с этим PID
|
||||||
|
if exists(pid_path):
|
||||||
|
try:
|
||||||
|
with open(pid_path, "r", encoding="utf-8") as f:
|
||||||
|
old_pid = int(f.read().strip())
|
||||||
|
# Проверяем, существует ли процесс с таким PID
|
||||||
|
import signal
|
||||||
|
try:
|
||||||
|
os.kill(old_pid, 0) # Сигнал 0 только проверяет существование процесса
|
||||||
|
print(f"[warning] DEV server already running with PID {old_pid}")
|
||||||
|
except OSError:
|
||||||
|
print(f"[info] Stale PID file found, previous process {old_pid} not running")
|
||||||
|
except (ValueError, FileNotFoundError):
|
||||||
|
print(f"[warning] Invalid PID file found, recreating")
|
||||||
|
|
||||||
|
# Создаем или перезаписываем PID-файл
|
||||||
|
with open(pid_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(str(os.getpid()))
|
||||||
|
print(f"[main] process started in DEV mode with PID {os.getpid()}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[main] Error during server startup: {str(e)}")
|
||||||
|
# Не прерываем запуск сервера из-за ошибки в этой функции
|
||||||
|
print(f"[warning] Error during DEV mode initialization: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
async def lifespan(_app):
|
||||||
|
"""
|
||||||
|
Функция жизненного цикла приложения.
|
||||||
|
|
||||||
|
Обеспечивает:
|
||||||
|
1. Инициализацию всех необходимых сервисов и компонентов
|
||||||
|
2. Предзагрузку кеша данных
|
||||||
|
3. Подключение к Redis и поисковому сервису
|
||||||
|
4. Корректное завершение работы при остановке сервера
|
||||||
|
|
||||||
|
Args:
|
||||||
|
_app: экземпляр Starlette приложения
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
None: генератор для управления жизненным циклом
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print("[lifespan] Starting application initialization")
|
||||||
|
create_all_tables()
|
||||||
|
await asyncio.gather(
|
||||||
|
redis.connect(),
|
||||||
|
precache_data(),
|
||||||
|
ViewedStorage.init(),
|
||||||
|
check_search_service(),
|
||||||
|
revalidation_manager.start(),
|
||||||
|
)
|
||||||
|
if DEVMODE:
|
||||||
|
await dev_start()
|
||||||
|
print("[lifespan] Basic initialization complete")
|
||||||
|
|
||||||
|
# Add a delay before starting the intensive search indexing
|
||||||
|
print("[lifespan] Waiting for system stabilization before search indexing...")
|
||||||
|
await asyncio.sleep(10) # 10-second delay to let the system stabilize
|
||||||
|
|
||||||
|
# Start search indexing as a background task with lower priority
|
||||||
|
asyncio.create_task(initialize_search_index_background())
|
||||||
|
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
print("[lifespan] Shutting down application services")
|
||||||
|
tasks = [redis.disconnect(), ViewedStorage.stop(), revalidation_manager.stop()]
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
print("[lifespan] Shutdown complete")
|
||||||
|
|
||||||
|
# Обновляем маршрут в Starlette
|
||||||
app = Starlette(
|
app = Starlette(
|
||||||
routes=routes,
|
routes=[
|
||||||
middleware=middleware,
|
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
|
||||||
on_startup=[start],
|
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True))
|
||||||
on_shutdown=[shutdown],
|
],
|
||||||
|
lifespan=lifespan,
|
||||||
|
middleware=middleware, # Явно указываем список middleware
|
||||||
|
debug=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if DEVMODE:
|
||||||
|
# Для DEV режима регистрируем дополнительный CORS middleware только для localhost
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["https://localhost:3000"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
|
@ -5,7 +5,6 @@ from resolvers.author import ( # search_authors,
|
||||||
get_author_follows,
|
get_author_follows,
|
||||||
get_author_follows_authors,
|
get_author_follows_authors,
|
||||||
get_author_follows_topics,
|
get_author_follows_topics,
|
||||||
get_author_id,
|
|
||||||
get_authors_all,
|
get_authors_all,
|
||||||
load_authors_by,
|
load_authors_by,
|
||||||
load_authors_search,
|
load_authors_search,
|
||||||
|
@ -18,6 +17,7 @@ from resolvers.draft import (
|
||||||
load_drafts,
|
load_drafts,
|
||||||
publish_draft,
|
publish_draft,
|
||||||
update_draft,
|
update_draft,
|
||||||
|
unpublish_draft,
|
||||||
)
|
)
|
||||||
from resolvers.editor import (
|
from resolvers.editor import (
|
||||||
unpublish_shout,
|
unpublish_shout,
|
||||||
|
@ -91,7 +91,6 @@ __all__ = [
|
||||||
|
|
||||||
# author
|
# author
|
||||||
"get_author",
|
"get_author",
|
||||||
"get_author_id",
|
|
||||||
"get_author_followers",
|
"get_author_followers",
|
||||||
"get_author_follows",
|
"get_author_follows",
|
||||||
"get_author_follows_topics",
|
"get_author_follows_topics",
|
||||||
|
@ -161,7 +160,6 @@ __all__ = [
|
||||||
"update_draft",
|
"update_draft",
|
||||||
"delete_draft",
|
"delete_draft",
|
||||||
"publish_draft",
|
"publish_draft",
|
||||||
"publish_shout",
|
|
||||||
"unpublish_shout",
|
"unpublish_shout",
|
||||||
"unpublish_draft",
|
"unpublish_draft",
|
||||||
]
|
]
|
||||||
|
|
|
@ -42,11 +42,11 @@ async def get_current_user(_, info):
|
||||||
info: Контекст GraphQL запроса
|
info: Контекст GraphQL запроса
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Объект с токеном и данными автора
|
dict: Объект с токеном и данными автора с добавленной статистикой
|
||||||
"""
|
"""
|
||||||
# Получаем данные авторизации из контекста запроса
|
# Получаем данные авторизации из контекста запроса
|
||||||
user_id = info.context.get("user_id")
|
author_id = info.context.get("author", {}).get("id")
|
||||||
if not user_id:
|
if not author_id:
|
||||||
logger.error("[getSession] Пользователь не авторизован")
|
logger.error("[getSession] Пользователь не авторизован")
|
||||||
from graphql.error import GraphQLError
|
from graphql.error import GraphQLError
|
||||||
raise GraphQLError("Требуется авторизация")
|
raise GraphQLError("Требуется авторизация")
|
||||||
|
@ -60,19 +60,50 @@ async def get_current_user(_, info):
|
||||||
# Получаем данные автора
|
# Получаем данные автора
|
||||||
author = info.context.get("author")
|
author = info.context.get("author")
|
||||||
|
|
||||||
# Если автор не найден в контексте, пробуем получить из БД
|
# Если автор не найден в контексте, пробуем получить из БД с добавлением статистики
|
||||||
if not author:
|
if not author:
|
||||||
logger.debug(f"[getSession] Автор не найден в контексте для пользователя {user_id}, получаем из БД")
|
logger.debug(f"[getSession] Автор не найден в контексте для пользователя {user_id}, получаем из БД")
|
||||||
with local_session() as session:
|
|
||||||
try:
|
try:
|
||||||
db_author = session.query(Author).filter(Author.id == user_id).one()
|
# Используем функцию get_with_stat для получения автора со статистикой
|
||||||
db_author.last_seen = int(time.time())
|
from sqlalchemy import select
|
||||||
session.commit()
|
from resolvers.stat import get_with_stat
|
||||||
author = db_author
|
|
||||||
except Exception as e:
|
q = select(Author).where(Author.id == user_id)
|
||||||
logger.error(f"[getSession] Ошибка при получении автора из БД: {e}")
|
authors_with_stat = get_with_stat(q)
|
||||||
|
|
||||||
|
if authors_with_stat and len(authors_with_stat) > 0:
|
||||||
|
author = authors_with_stat[0]
|
||||||
|
|
||||||
|
# Обновляем last_seen отдельной транзакцией
|
||||||
|
with local_session() as session:
|
||||||
|
author_db = session.query(Author).filter(Author.id == user_id).first()
|
||||||
|
if author_db:
|
||||||
|
author_db.last_seen = int(time.time())
|
||||||
|
session.commit()
|
||||||
|
else:
|
||||||
|
logger.error(f"[getSession] Автор с ID {user_id} не найден в БД")
|
||||||
from graphql.error import GraphQLError
|
from graphql.error import GraphQLError
|
||||||
raise GraphQLError("Ошибка при получении данных пользователя")
|
raise GraphQLError("Пользователь не найден")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[getSession] Ошибка при получении автора из БД: {e}", exc_info=True)
|
||||||
|
from graphql.error import GraphQLError
|
||||||
|
raise GraphQLError("Ошибка при получении данных пользователя")
|
||||||
|
else:
|
||||||
|
# Если автор уже есть в контексте, добавляем статистику
|
||||||
|
try:
|
||||||
|
from sqlalchemy import select
|
||||||
|
from resolvers.stat import get_with_stat
|
||||||
|
|
||||||
|
q = select(Author).where(Author.id == user_id)
|
||||||
|
authors_with_stat = get_with_stat(q)
|
||||||
|
|
||||||
|
if authors_with_stat and len(authors_with_stat) > 0:
|
||||||
|
# Обновляем только статистику
|
||||||
|
author.stat = authors_with_stat[0].stat
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[getSession] Не удалось добавить статистику к автору: {e}")
|
||||||
|
|
||||||
# Возвращаем данные сессии
|
# Возвращаем данные сессии
|
||||||
logger.info(f"[getSession] Успешно получена сессия для пользователя {user_id}")
|
logger.info(f"[getSession] Успешно получена сессия для пользователя {user_id}")
|
||||||
|
|
|
@ -8,7 +8,6 @@ from cache.cache import (
|
||||||
cache_author,
|
cache_author,
|
||||||
cached_query,
|
cached_query,
|
||||||
get_cached_author,
|
get_cached_author,
|
||||||
get_cached_author_by_user_id,
|
|
||||||
get_cached_author_followers,
|
get_cached_author_followers,
|
||||||
get_cached_follower_authors,
|
get_cached_follower_authors,
|
||||||
get_cached_follower_topics,
|
get_cached_follower_topics,
|
||||||
|
@ -205,25 +204,24 @@ async def invalidate_authors_cache(author_id=None):
|
||||||
@mutation.field("update_author")
|
@mutation.field("update_author")
|
||||||
@login_required
|
@login_required
|
||||||
async def update_author(_, info, profile):
|
async def update_author(_, info, profile):
|
||||||
user_id = info.context.get("user_id")
|
author_id = info.context.get("author", {}).get("id")
|
||||||
is_admin = info.context.get("is_admin", False)
|
is_admin = info.context.get("is_admin", False)
|
||||||
|
if not author_id:
|
||||||
if not user_id:
|
|
||||||
return {"error": "unauthorized", "author": None}
|
return {"error": "unauthorized", "author": None}
|
||||||
try:
|
try:
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
author = session.query(Author).where(Author.id == user_id).first()
|
author = session.query(Author).where(Author.id == author_id).first()
|
||||||
if author:
|
if author:
|
||||||
Author.update(author, profile)
|
Author.update(author, profile)
|
||||||
session.add(author)
|
session.add(author)
|
||||||
session.commit()
|
session.commit()
|
||||||
author_query = select(Author).where(Author.id == user_id)
|
author_query = select(Author).where(Author.id == author_id)
|
||||||
result = get_with_stat(author_query)
|
result = get_with_stat(author_query)
|
||||||
if result:
|
if result:
|
||||||
author_with_stat = result[0]
|
author_with_stat = result[0]
|
||||||
if isinstance(author_with_stat, Author):
|
if isinstance(author_with_stat, Author):
|
||||||
# Кэшируем полную версию для админов
|
# Кэшируем полную версию для админов
|
||||||
author_dict = author_with_stat.dict(is_admin=True)
|
author_dict = author_with_stat.dict(access=is_admin)
|
||||||
asyncio.create_task(cache_author(author_dict))
|
asyncio.create_task(cache_author(author_dict))
|
||||||
|
|
||||||
# Возвращаем обычную полную версию, т.к. это владелец
|
# Возвращаем обычную полную версию, т.к. это владелец
|
||||||
|
@ -244,16 +242,16 @@ async def get_authors_all(_, info):
|
||||||
list: Список всех авторов
|
list: Список всех авторов
|
||||||
"""
|
"""
|
||||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||||
current_user_id = info.context.get("user_id") if hasattr(info, "context") else None
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
authors = await get_all_authors(current_user_id, False)
|
is_admin = info.context.get("is_admin", False)
|
||||||
|
authors = await get_all_authors(viewer_id, is_admin)
|
||||||
return authors
|
return authors
|
||||||
|
|
||||||
|
|
||||||
@query.field("get_author")
|
@query.field("get_author")
|
||||||
async def get_author(_, info, slug="", author_id=0):
|
async def get_author(_, info, slug="", author_id=0):
|
||||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||||
current_user_id = info.context.get("user_id") if hasattr(info, "context") else None
|
is_admin = info.context.get("is_admin", False)
|
||||||
is_admin = info.context.get("is_admin", False) if hasattr(info, "context") else False
|
|
||||||
|
|
||||||
author_dict = None
|
author_dict = None
|
||||||
try:
|
try:
|
||||||
|
@ -272,7 +270,7 @@ async def get_author(_, info, slug="", author_id=0):
|
||||||
if hasattr(temp_author, key):
|
if hasattr(temp_author, key):
|
||||||
setattr(temp_author, key, value)
|
setattr(temp_author, key, value)
|
||||||
# Получаем отфильтрованную версию
|
# Получаем отфильтрованную версию
|
||||||
author_dict = temp_author.dict(current_user_id, is_admin)
|
author_dict = temp_author.dict(access=is_admin)
|
||||||
# Добавляем статистику, которая могла быть в кэшированной версии
|
# Добавляем статистику, которая могла быть в кэшированной версии
|
||||||
if "stat" in cached_author:
|
if "stat" in cached_author:
|
||||||
author_dict["stat"] = cached_author["stat"]
|
author_dict["stat"] = cached_author["stat"]
|
||||||
|
@ -285,11 +283,11 @@ async def get_author(_, info, slug="", author_id=0):
|
||||||
author_with_stat = result[0]
|
author_with_stat = result[0]
|
||||||
if isinstance(author_with_stat, Author):
|
if isinstance(author_with_stat, Author):
|
||||||
# Кэшируем полные данные для админов
|
# Кэшируем полные данные для админов
|
||||||
original_dict = author_with_stat.dict(is_admin=True)
|
original_dict = author_with_stat.dict(access=True)
|
||||||
asyncio.create_task(cache_author(original_dict))
|
asyncio.create_task(cache_author(original_dict))
|
||||||
|
|
||||||
# Возвращаем отфильтрованную версию
|
# Возвращаем отфильтрованную версию
|
||||||
author_dict = author_with_stat.dict(current_user_id, is_admin)
|
author_dict = author_with_stat.dict(access=is_admin)
|
||||||
# Добавляем статистику
|
# Добавляем статистику
|
||||||
if hasattr(author_with_stat, "stat"):
|
if hasattr(author_with_stat, "stat"):
|
||||||
author_dict["stat"] = author_with_stat.stat
|
author_dict["stat"] = author_with_stat.stat
|
||||||
|
@ -302,42 +300,6 @@ async def get_author(_, info, slug="", author_id=0):
|
||||||
return author_dict
|
return author_dict
|
||||||
|
|
||||||
|
|
||||||
@query.field("get_author_id")
|
|
||||||
async def get_author_id(_, info, user: str):
|
|
||||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
|
||||||
current_user_id = info.context.get("user_id") if hasattr(info, "context") else None
|
|
||||||
is_admin = info.context.get("is_admin", False) if hasattr(info, "context") else False
|
|
||||||
|
|
||||||
user_id = user.strip()
|
|
||||||
logger.info(f"getting author id for {user_id}")
|
|
||||||
author = None
|
|
||||||
try:
|
|
||||||
cached_author = await get_cached_author_by_user_id(user_id, get_with_stat)
|
|
||||||
if cached_author:
|
|
||||||
# Создаем объект автора для использования метода dict
|
|
||||||
temp_author = Author()
|
|
||||||
for key, value in cached_author.items():
|
|
||||||
if hasattr(temp_author, key):
|
|
||||||
setattr(temp_author, key, value)
|
|
||||||
# Возвращаем отфильтрованную версию
|
|
||||||
return temp_author.dict(current_user_id, is_admin)
|
|
||||||
|
|
||||||
author_query = select(Author).filter(Author.id == user_id)
|
|
||||||
result = get_with_stat(author_query)
|
|
||||||
if result:
|
|
||||||
author_with_stat = result[0]
|
|
||||||
if isinstance(author_with_stat, Author):
|
|
||||||
# Кэшируем полную версию данных
|
|
||||||
original_dict = author_with_stat.dict(is_admin=True)
|
|
||||||
asyncio.create_task(cache_author(original_dict))
|
|
||||||
|
|
||||||
# Возвращаем отфильтрованную версию
|
|
||||||
return author_with_stat.dict(current_user_id, is_admin)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error(f"Error getting author: {exc}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@query.field("load_authors_by")
|
@query.field("load_authors_by")
|
||||||
async def load_authors_by(_, info, by, limit, offset):
|
async def load_authors_by(_, info, by, limit, offset):
|
||||||
"""
|
"""
|
||||||
|
@ -352,11 +314,11 @@ async def load_authors_by(_, info, by, limit, offset):
|
||||||
list: Список авторов с учетом критерия
|
list: Список авторов с учетом критерия
|
||||||
"""
|
"""
|
||||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||||
current_user_id = info.context.get("user_id") if hasattr(info, "context") else None
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
is_admin = info.context.get("is_admin", False) if hasattr(info, "context") else False
|
is_admin = info.context.get("is_admin", False)
|
||||||
|
|
||||||
# Используем оптимизированную функцию для получения авторов
|
# Используем оптимизированную функцию для получения авторов
|
||||||
return await get_authors_with_stats(limit, offset, by, current_user_id, is_admin)
|
return await get_authors_with_stats(limit, offset, by, viewer_id, is_admin)
|
||||||
|
|
||||||
|
|
||||||
@query.field("load_authors_search")
|
@query.field("load_authors_search")
|
||||||
|
@ -423,8 +385,8 @@ def get_author_id_from(slug="", user=None, author_id=None):
|
||||||
@query.field("get_author_follows")
|
@query.field("get_author_follows")
|
||||||
async def get_author_follows(_, info, slug="", user=None, author_id=0):
|
async def get_author_follows(_, info, slug="", user=None, author_id=0):
|
||||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||||
current_user_id = info.context.get("user_id") if hasattr(info, "context") else None
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
is_admin = info.context.get("is_admin", False) if hasattr(info, "context") else False
|
is_admin = info.context.get("is_admin", False)
|
||||||
|
|
||||||
logger.debug(f"getting follows for @{slug}")
|
logger.debug(f"getting follows for @{slug}")
|
||||||
author_id = get_author_id_from(slug=slug, user=user, author_id=author_id)
|
author_id = get_author_id_from(slug=slug, user=user, author_id=author_id)
|
||||||
|
@ -447,7 +409,7 @@ async def get_author_follows(_, info, slug="", user=None, author_id=0):
|
||||||
# temp_author - это объект Author, который мы хотим сериализовать
|
# temp_author - это объект Author, который мы хотим сериализовать
|
||||||
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||||
# is_admin - булево значение, является ли текущий пользователь админом
|
# is_admin - булево значение, является ли текущий пользователь админом
|
||||||
has_access = is_admin or (current_user_id is not None and str(current_user_id) == str(temp_author.id))
|
has_access = is_admin or (viewer_id is not None and str(viewer_id) == str(temp_author.id))
|
||||||
followed_authors.append(temp_author.dict(access=has_access))
|
followed_authors.append(temp_author.dict(access=has_access))
|
||||||
|
|
||||||
# TODO: Get followed communities too
|
# TODO: Get followed communities too
|
||||||
|
@ -472,13 +434,13 @@ async def get_author_follows_topics(_, _info, slug="", user=None, author_id=None
|
||||||
@query.field("get_author_follows_authors")
|
@query.field("get_author_follows_authors")
|
||||||
async def get_author_follows_authors(_, info, slug="", user=None, author_id=None):
|
async def get_author_follows_authors(_, info, slug="", user=None, author_id=None):
|
||||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||||
current_user_id = info.context.get("user_id") if hasattr(info, "context") else None
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
is_admin = info.context.get("is_admin", False) if hasattr(info, "context") else False
|
is_admin = info.context.get("is_admin", False)
|
||||||
|
|
||||||
logger.debug(f"getting followed authors for @{slug}")
|
logger.debug(f"getting followed authors for @{slug}")
|
||||||
author_id = get_author_id_from(slug=slug, user=user, author_id=author_id)
|
|
||||||
if not author_id:
|
if not author_id:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
# Получаем данные из кэша
|
# Получаем данные из кэша
|
||||||
followed_authors_raw = await get_cached_follower_authors(author_id)
|
followed_authors_raw = await get_cached_follower_authors(author_id)
|
||||||
|
@ -495,7 +457,7 @@ async def get_author_follows_authors(_, info, slug="", user=None, author_id=None
|
||||||
# temp_author - это объект Author, который мы хотим сериализовать
|
# temp_author - это объект Author, который мы хотим сериализовать
|
||||||
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||||
# is_admin - булево значение, является ли текущий пользователь админом
|
# is_admin - булево значение, является ли текущий пользователь админом
|
||||||
has_access = is_admin or (current_user_id is not None and str(current_user_id) == str(temp_author.id))
|
has_access = is_admin or (viewer_id is not None and str(viewer_id) == str(temp_author.id))
|
||||||
followed_authors.append(temp_author.dict(access=has_access))
|
followed_authors.append(temp_author.dict(access=has_access))
|
||||||
|
|
||||||
return followed_authors
|
return followed_authors
|
||||||
|
@ -517,8 +479,8 @@ def create_author(user_id: str, slug: str, name: str = ""):
|
||||||
@query.field("get_author_followers")
|
@query.field("get_author_followers")
|
||||||
async def get_author_followers(_, info, slug: str = "", user: str = "", author_id: int = 0):
|
async def get_author_followers(_, info, slug: str = "", user: str = "", author_id: int = 0):
|
||||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||||
current_user_id = info.context.get("user_id") if hasattr(info, "context") else None
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
is_admin = info.context.get("is_admin", False) if hasattr(info, "context") else False
|
is_admin = info.context.get("is_admin", False)
|
||||||
|
|
||||||
logger.debug(f"getting followers for author @{slug} or ID:{author_id}")
|
logger.debug(f"getting followers for author @{slug} or ID:{author_id}")
|
||||||
author_id = get_author_id_from(slug=slug, user=user, author_id=author_id)
|
author_id = get_author_id_from(slug=slug, user=user, author_id=author_id)
|
||||||
|
@ -540,7 +502,7 @@ async def get_author_followers(_, info, slug: str = "", user: str = "", author_i
|
||||||
# temp_author - это объект Author, который мы хотим сериализовать
|
# temp_author - это объект Author, который мы хотим сериализовать
|
||||||
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||||
# is_admin - булево значение, является ли текущий пользователь админом
|
# is_admin - булево значение, является ли текущий пользователь админом
|
||||||
has_access = is_admin or (current_user_id is not None and str(current_user_id) == str(temp_author.id))
|
has_access = is_admin or (viewer_id is not None and str(viewer_id) == str(temp_author.id))
|
||||||
followers.append(temp_author.dict(access=has_access))
|
followers.append(temp_author.dict(access=has_access))
|
||||||
|
|
||||||
return followers
|
return followers
|
||||||
|
|
|
@ -9,7 +9,6 @@ from services.schema import mutation
|
||||||
@mutation.field("accept_invite")
|
@mutation.field("accept_invite")
|
||||||
@login_required
|
@login_required
|
||||||
async def accept_invite(_, info, invite_id: int):
|
async def accept_invite(_, info, invite_id: int):
|
||||||
info.context["user_id"]
|
|
||||||
author_dict = info.context["author"]
|
author_dict = info.context["author"]
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
if author_id:
|
if author_id:
|
||||||
|
@ -41,7 +40,6 @@ async def accept_invite(_, info, invite_id: int):
|
||||||
@mutation.field("reject_invite")
|
@mutation.field("reject_invite")
|
||||||
@login_required
|
@login_required
|
||||||
async def reject_invite(_, info, invite_id: int):
|
async def reject_invite(_, info, invite_id: int):
|
||||||
info.context["user_id"]
|
|
||||||
author_dict = info.context["author"]
|
author_dict = info.context["author"]
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
|
|
||||||
|
@ -64,14 +62,17 @@ async def reject_invite(_, info, invite_id: int):
|
||||||
@mutation.field("create_invite")
|
@mutation.field("create_invite")
|
||||||
@login_required
|
@login_required
|
||||||
async def create_invite(_, info, slug: str = "", author_id: int = 0):
|
async def create_invite(_, info, slug: str = "", author_id: int = 0):
|
||||||
user_id = info.context["user_id"]
|
|
||||||
author_dict = info.context["author"]
|
author_dict = info.context["author"]
|
||||||
author_id = author_dict.get("id")
|
viewer_id = author_dict.get("id")
|
||||||
|
roles = info.context.get("roles", [])
|
||||||
|
is_admin = info.context.get("is_admin", False)
|
||||||
|
if not viewer_id and not is_admin and "admin" not in roles and "editor" not in roles:
|
||||||
|
return {"error": "Access denied"}
|
||||||
if author_id:
|
if author_id:
|
||||||
# Check if the inviter is the owner of the shout
|
# Check if the inviter is the owner of the shout
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
shout = session.query(Shout).filter(Shout.slug == slug).first()
|
shout = session.query(Shout).filter(Shout.slug == slug).first()
|
||||||
inviter = session.query(Author).filter(Author.id == user_id).first()
|
inviter = session.query(Author).filter(Author.id == viewer_id).first()
|
||||||
if inviter and shout and shout.authors and inviter.id is shout.created_by:
|
if inviter and shout and shout.authors and inviter.id is shout.created_by:
|
||||||
# Check if an invite already exists
|
# Check if an invite already exists
|
||||||
existing_invite = (
|
existing_invite = (
|
||||||
|
@ -89,7 +90,7 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0):
|
||||||
|
|
||||||
# Create a new invite
|
# Create a new invite
|
||||||
new_invite = Invite(
|
new_invite = Invite(
|
||||||
inviter_id=user_id,
|
inviter_id=viewer_id,
|
||||||
author_id=author_id,
|
author_id=author_id,
|
||||||
shout_id=shout.id,
|
shout_id=shout.id,
|
||||||
status=InviteStatus.PENDING.value,
|
status=InviteStatus.PENDING.value,
|
||||||
|
@ -107,9 +108,13 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0):
|
||||||
@mutation.field("remove_author")
|
@mutation.field("remove_author")
|
||||||
@login_required
|
@login_required
|
||||||
async def remove_author(_, info, slug: str = "", author_id: int = 0):
|
async def remove_author(_, info, slug: str = "", author_id: int = 0):
|
||||||
user_id = info.context["user_id"]
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
|
is_admin = info.context.get("is_admin", False)
|
||||||
|
roles = info.context.get("roles", [])
|
||||||
|
if not viewer_id and not is_admin and "admin" not in roles and "editor" not in roles:
|
||||||
|
return {"error": "Access denied"}
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
author = session.query(Author).filter(Author.id == user_id).first()
|
author = session.query(Author).filter(Author.id == author_id).first()
|
||||||
if author:
|
if author:
|
||||||
shout = session.query(Shout).filter(Shout.slug == slug).first()
|
shout = session.query(Shout).filter(Shout.slug == slug).first()
|
||||||
# NOTE: owner should be first in a list
|
# NOTE: owner should be first in a list
|
||||||
|
@ -123,8 +128,6 @@ async def remove_author(_, info, slug: str = "", author_id: int = 0):
|
||||||
@mutation.field("remove_invite")
|
@mutation.field("remove_invite")
|
||||||
@login_required
|
@login_required
|
||||||
async def remove_invite(_, info, invite_id: int):
|
async def remove_invite(_, info, invite_id: int):
|
||||||
info.context["user_id"]
|
|
||||||
|
|
||||||
author_dict = info.context["author"]
|
author_dict = info.context["author"]
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
if isinstance(author_id, int):
|
if isinstance(author_id, int):
|
||||||
|
|
|
@ -78,12 +78,11 @@ async def load_drafts(_, info):
|
||||||
Returns:
|
Returns:
|
||||||
dict: Список черновиков или сообщение об ошибке
|
dict: Список черновиков или сообщение об ошибке
|
||||||
"""
|
"""
|
||||||
user_id = info.context.get("user_id")
|
|
||||||
author_dict = info.context.get("author", {})
|
author_dict = info.context.get("author", {})
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
|
|
||||||
if not user_id or not author_id:
|
if not author_id:
|
||||||
return {"error": "User ID and author ID are required"}
|
return {"error": "Author ID is required"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
|
@ -152,11 +151,10 @@ async def create_draft(_, info, draft_input):
|
||||||
... assert result['draft'].title == 'Test'
|
... assert result['draft'].title == 'Test'
|
||||||
... return result
|
... return result
|
||||||
"""
|
"""
|
||||||
user_id = info.context.get("user_id")
|
|
||||||
author_dict = info.context.get("author", {})
|
author_dict = info.context.get("author", {})
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
|
|
||||||
if not user_id or not author_id:
|
if not author_id:
|
||||||
return {"error": "Author ID is required"}
|
return {"error": "Author ID is required"}
|
||||||
|
|
||||||
# Проверяем обязательные поля
|
# Проверяем обязательные поля
|
||||||
|
@ -227,11 +225,10 @@ async def update_draft(_, info, draft_id: int, draft_input):
|
||||||
Returns:
|
Returns:
|
||||||
dict: Обновленный черновик или сообщение об ошибке
|
dict: Обновленный черновик или сообщение об ошибке
|
||||||
"""
|
"""
|
||||||
user_id = info.context.get("user_id")
|
|
||||||
author_dict = info.context.get("author", {})
|
author_dict = info.context.get("author", {})
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
|
|
||||||
if not user_id or not author_id:
|
if not author_id:
|
||||||
return {"error": "Author ID are required"}
|
return {"error": "Author ID are required"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -389,11 +386,10 @@ async def publish_draft(_, info, draft_id: int):
|
||||||
Returns:
|
Returns:
|
||||||
dict: Результат публикации с shout или сообщением об ошибке
|
dict: Результат публикации с shout или сообщением об ошибке
|
||||||
"""
|
"""
|
||||||
user_id = info.context.get("user_id")
|
|
||||||
author_dict = info.context.get("author", {})
|
author_dict = info.context.get("author", {})
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
|
|
||||||
if not user_id or not author_id:
|
if not author_id:
|
||||||
return {"error": "Author ID is required"}
|
return {"error": "Author ID is required"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -469,7 +465,7 @@ async def publish_draft(_, info, draft_id: int):
|
||||||
await notify_shout(shout.id)
|
await notify_shout(shout.id)
|
||||||
|
|
||||||
# Обновляем поисковый индекс
|
# Обновляем поисковый индекс
|
||||||
search_service.index_shout(shout)
|
search_service.perform_index(shout)
|
||||||
|
|
||||||
logger.info(f"Successfully published shout #{shout.id} from draft #{draft_id}")
|
logger.info(f"Successfully published shout #{shout.id} from draft #{draft_id}")
|
||||||
logger.debug(f"Shout data: {shout.dict()}")
|
logger.debug(f"Shout data: {shout.dict()}")
|
||||||
|
@ -479,3 +475,74 @@ async def publish_draft(_, info, draft_id: int):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to publish draft {draft_id}: {e}", exc_info=True)
|
logger.error(f"Failed to publish draft {draft_id}: {e}", exc_info=True)
|
||||||
return {"error": f"Failed to publish draft: {str(e)}"}
|
return {"error": f"Failed to publish draft: {str(e)}"}
|
||||||
|
|
||||||
|
|
||||||
|
@mutation.field("unpublish_draft")
|
||||||
|
@login_required
|
||||||
|
async def unpublish_draft(_, info, draft_id: int):
|
||||||
|
"""
|
||||||
|
Снимает с публикации черновик, обновляя связанный Shout.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
draft_id (int): ID черновика, публикацию которого нужно снять
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Результат операции с информацией о черновике или сообщением об ошибке
|
||||||
|
"""
|
||||||
|
author_dict = info.context.get("author", {})
|
||||||
|
author_id = author_dict.get("id")
|
||||||
|
|
||||||
|
if author_id:
|
||||||
|
return {"error": "Author ID is required"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with local_session() as session:
|
||||||
|
# Загружаем черновик со связанной публикацией
|
||||||
|
draft = (
|
||||||
|
session.query(Draft)
|
||||||
|
.options(
|
||||||
|
joinedload(Draft.publication),
|
||||||
|
joinedload(Draft.authors),
|
||||||
|
joinedload(Draft.topics)
|
||||||
|
)
|
||||||
|
.filter(Draft.id == draft_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not draft:
|
||||||
|
return {"error": "Draft not found"}
|
||||||
|
|
||||||
|
# Проверяем, есть ли публикация
|
||||||
|
if not draft.publication:
|
||||||
|
return {"error": "This draft is not published yet"}
|
||||||
|
|
||||||
|
shout = draft.publication
|
||||||
|
|
||||||
|
# Снимаем с публикации
|
||||||
|
shout.published_at = None
|
||||||
|
shout.updated_at = int(time.time())
|
||||||
|
shout.updated_by = author_id
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Инвалидируем кэш
|
||||||
|
cache_keys = [f"shouts:{shout.id}"]
|
||||||
|
await invalidate_shouts_cache(cache_keys)
|
||||||
|
await invalidate_shout_related_cache(shout, author_id)
|
||||||
|
|
||||||
|
# Формируем результат
|
||||||
|
draft_dict = draft.dict()
|
||||||
|
# Добавляем информацию о публикации
|
||||||
|
draft_dict["publication"] = {
|
||||||
|
"id": shout.id,
|
||||||
|
"slug": shout.slug,
|
||||||
|
"published_at": None
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Successfully unpublished shout #{shout.id} for draft #{draft_id}")
|
||||||
|
|
||||||
|
return {"draft": draft_dict}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to unpublish draft {draft_id}: {e}", exc_info=True)
|
||||||
|
return {"error": f"Failed to unpublish draft: {str(e)}"}
|
||||||
|
|
|
@ -86,12 +86,11 @@ async def get_my_shout(_, info, shout_id: int):
|
||||||
... assert result['shout'].id == 1
|
... assert result['shout'].id == 1
|
||||||
... return result
|
... return result
|
||||||
"""
|
"""
|
||||||
user_id = info.context.get("user_id", "")
|
|
||||||
author_dict = info.context.get("author", {})
|
author_dict = info.context.get("author", {})
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
roles = info.context.get("roles", [])
|
roles = info.context.get("roles", [])
|
||||||
shout = None
|
shout = None
|
||||||
if not user_id or not author_id:
|
if not author_id:
|
||||||
return {"error": "unauthorized", "shout": None}
|
return {"error": "unauthorized", "shout": None}
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
shout = (
|
shout = (
|
||||||
|
@ -136,7 +135,6 @@ async def get_my_shout(_, info, shout_id: int):
|
||||||
@query.field("get_shouts_drafts")
|
@query.field("get_shouts_drafts")
|
||||||
@login_required
|
@login_required
|
||||||
async def get_shouts_drafts(_, info):
|
async def get_shouts_drafts(_, info):
|
||||||
# user_id = info.context.get("user_id")
|
|
||||||
author_dict = info.context.get("author")
|
author_dict = info.context.get("author")
|
||||||
if not author_dict:
|
if not author_dict:
|
||||||
return {"error": "author profile was not found"}
|
return {"error": "author profile was not found"}
|
||||||
|
@ -160,16 +158,15 @@ async def get_shouts_drafts(_, info):
|
||||||
# @login_required
|
# @login_required
|
||||||
async def create_shout(_, info, inp):
|
async def create_shout(_, info, inp):
|
||||||
logger.info(f"Starting create_shout with input: {inp}")
|
logger.info(f"Starting create_shout with input: {inp}")
|
||||||
user_id = info.context.get("user_id")
|
|
||||||
author_dict = info.context.get("author")
|
author_dict = info.context.get("author")
|
||||||
logger.debug(f"Context user_id: {user_id}, author: {author_dict}")
|
logger.debug(f"Context author: {author_dict}")
|
||||||
|
|
||||||
if not author_dict:
|
if not author_dict:
|
||||||
logger.error("Author profile not found in context")
|
logger.error("Author profile not found in context")
|
||||||
return {"error": "author profile was not found"}
|
return {"error": "author profile was not found"}
|
||||||
|
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
if user_id and author_id:
|
if author_id:
|
||||||
try:
|
try:
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
author_id = int(author_id)
|
author_id = int(author_id)
|
||||||
|
@ -268,7 +265,7 @@ async def create_shout(_, info, inp):
|
||||||
logger.error(f"Unexpected error in create_shout: {e}", exc_info=True)
|
logger.error(f"Unexpected error in create_shout: {e}", exc_info=True)
|
||||||
return {"error": f"Unexpected error: {str(e)}"}
|
return {"error": f"Unexpected error: {str(e)}"}
|
||||||
|
|
||||||
error_msg = "cant create shout" if user_id else "unauthorized"
|
error_msg = "cant create shout" if author_id else "unauthorized"
|
||||||
logger.error(f"Create shout failed: {error_msg}")
|
logger.error(f"Create shout failed: {error_msg}")
|
||||||
return {"error": error_msg}
|
return {"error": error_msg}
|
||||||
|
|
||||||
|
@ -394,26 +391,19 @@ def patch_topics(session, shout, topics_input):
|
||||||
# @mutation.field("update_shout")
|
# @mutation.field("update_shout")
|
||||||
# @login_required
|
# @login_required
|
||||||
async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
|
async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
|
||||||
logger.info(f"Starting update_shout with id={shout_id}, publish={publish}")
|
author_id = info.context.get("author").get("id")
|
||||||
logger.debug(f"Full shout_input: {shout_input}") # DraftInput
|
if not author_id:
|
||||||
|
|
||||||
user_id = info.context.get("user_id")
|
|
||||||
roles = info.context.get("roles", [])
|
|
||||||
author_dict = info.context.get("author")
|
|
||||||
if not author_dict:
|
|
||||||
logger.error("Author profile not found")
|
|
||||||
return {"error": "author profile was not found"}
|
|
||||||
|
|
||||||
author_id = author_dict.get("id")
|
|
||||||
shout_input = shout_input or {}
|
|
||||||
current_time = int(time.time())
|
|
||||||
shout_id = shout_id or shout_input.get("id", shout_id)
|
|
||||||
slug = shout_input.get("slug")
|
|
||||||
|
|
||||||
if not user_id:
|
|
||||||
logger.error("Unauthorized update attempt")
|
logger.error("Unauthorized update attempt")
|
||||||
return {"error": "unauthorized"}
|
return {"error": "unauthorized"}
|
||||||
|
|
||||||
|
logger.info(f"Starting update_shout with id={shout_id}, publish={publish}")
|
||||||
|
logger.debug(f"Full shout_input: {shout_input}") # DraftInput
|
||||||
|
roles = info.context.get("roles", [])
|
||||||
|
current_time = int(time.time())
|
||||||
|
shout_input = shout_input or {}
|
||||||
|
shout_id = shout_id or shout_input.get("id", shout_id)
|
||||||
|
slug = shout_input.get("slug")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
if author_id:
|
if author_id:
|
||||||
|
@ -620,13 +610,12 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
|
||||||
# @mutation.field("delete_shout")
|
# @mutation.field("delete_shout")
|
||||||
# @login_required
|
# @login_required
|
||||||
async def delete_shout(_, info, shout_id: int):
|
async def delete_shout(_, info, shout_id: int):
|
||||||
user_id = info.context.get("user_id")
|
|
||||||
roles = info.context.get("roles", [])
|
|
||||||
author_dict = info.context.get("author")
|
author_dict = info.context.get("author")
|
||||||
if not author_dict:
|
if not author_dict:
|
||||||
return {"error": "author profile was not found"}
|
return {"error": "author profile was not found"}
|
||||||
author_id = author_dict.get("id")
|
author_id = author_dict.get("id")
|
||||||
if user_id and author_id:
|
roles = info.context.get("roles", [])
|
||||||
|
if author_id:
|
||||||
author_id = int(author_id)
|
author_id = int(author_id)
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
||||||
|
@ -643,7 +632,6 @@ async def delete_shout(_, info, shout_id: int):
|
||||||
for author in shout.authors:
|
for author in shout.authors:
|
||||||
await cache_by_id(Author, author.id, cache_author)
|
await cache_by_id(Author, author.id, cache_author)
|
||||||
info.context["author"] = author.dict()
|
info.context["author"] = author.dict()
|
||||||
info.context["user_id"] = author.id
|
|
||||||
unfollow(None, info, "shout", shout.slug)
|
unfollow(None, info, "shout", shout.slug)
|
||||||
|
|
||||||
for topic in shout.topics:
|
for topic in shout.topics:
|
||||||
|
@ -746,7 +734,7 @@ async def unpublish_shout(_, info, shout_id: int):
|
||||||
return {"error": "Shout not found"}
|
return {"error": "Shout not found"}
|
||||||
|
|
||||||
# Если у публикации есть связанный черновик, загружаем его с relationships
|
# Если у публикации есть связанный черновик, загружаем его с relationships
|
||||||
if shout.draft:
|
if shout.draft is not None:
|
||||||
# Отдельно загружаем черновик с его связями
|
# Отдельно загружаем черновик с его связями
|
||||||
draft = (
|
draft = (
|
||||||
session.query(Draft)
|
session.query(Draft)
|
||||||
|
|
|
@ -27,12 +27,14 @@ from utils.logger import root_logger as logger
|
||||||
@login_required
|
@login_required
|
||||||
async def follow(_, info, what, slug="", entity_id=0):
|
async def follow(_, info, what, slug="", entity_id=0):
|
||||||
logger.debug("Начало выполнения функции 'follow'")
|
logger.debug("Начало выполнения функции 'follow'")
|
||||||
user_id = info.context.get("user_id")
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
|
if not viewer_id:
|
||||||
|
return {"error": "Access denied"}
|
||||||
follower_dict = info.context.get("author")
|
follower_dict = info.context.get("author")
|
||||||
logger.debug(f"follower: {follower_dict}")
|
logger.debug(f"follower: {follower_dict}")
|
||||||
|
|
||||||
if not user_id or not follower_dict:
|
if not viewer_id or not follower_dict:
|
||||||
return GraphQLError("unauthorized")
|
return GraphQLError("Access denied")
|
||||||
|
|
||||||
follower_id = follower_dict.get("id")
|
follower_id = follower_dict.get("id")
|
||||||
logger.debug(f"follower_id: {follower_id}")
|
logger.debug(f"follower_id: {follower_id}")
|
||||||
|
@ -107,7 +109,6 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||||
# Если это авторы, получаем безопасную версию
|
# Если это авторы, получаем безопасную версию
|
||||||
if what == "AUTHOR":
|
if what == "AUTHOR":
|
||||||
# Получаем ID текущего пользователя и фильтруем данные
|
# Получаем ID текущего пользователя и фильтруем данные
|
||||||
current_user_id = user_id
|
|
||||||
follows_filtered = []
|
follows_filtered = []
|
||||||
|
|
||||||
for author_data in existing_follows:
|
for author_data in existing_follows:
|
||||||
|
@ -117,7 +118,7 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||||
if hasattr(temp_author, key):
|
if hasattr(temp_author, key):
|
||||||
setattr(temp_author, key, value)
|
setattr(temp_author, key, value)
|
||||||
# Добавляем отфильтрованную версию
|
# Добавляем отфильтрованную версию
|
||||||
follows_filtered.append(temp_author.dict(current_user_id, False))
|
follows_filtered.append(temp_author.dict(viewer_id, False))
|
||||||
|
|
||||||
if not existing_sub:
|
if not existing_sub:
|
||||||
# Создаем объект автора для entity_dict
|
# Создаем объект автора для entity_dict
|
||||||
|
@ -126,7 +127,7 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||||
if hasattr(temp_author, key):
|
if hasattr(temp_author, key):
|
||||||
setattr(temp_author, key, value)
|
setattr(temp_author, key, value)
|
||||||
# Добавляем отфильтрованную версию
|
# Добавляем отфильтрованную версию
|
||||||
follows = [*follows_filtered, temp_author.dict(current_user_id, False)]
|
follows = [*follows_filtered, temp_author.dict(viewer_id, False)]
|
||||||
else:
|
else:
|
||||||
follows = follows_filtered
|
follows = follows_filtered
|
||||||
else:
|
else:
|
||||||
|
@ -149,13 +150,15 @@ async def follow(_, info, what, slug="", entity_id=0):
|
||||||
@login_required
|
@login_required
|
||||||
async def unfollow(_, info, what, slug="", entity_id=0):
|
async def unfollow(_, info, what, slug="", entity_id=0):
|
||||||
logger.debug("Начало выполнения функции 'unfollow'")
|
logger.debug("Начало выполнения функции 'unfollow'")
|
||||||
user_id = info.context.get("user_id")
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
|
if not viewer_id:
|
||||||
|
return GraphQLError("Access denied")
|
||||||
follower_dict = info.context.get("author")
|
follower_dict = info.context.get("author")
|
||||||
logger.debug(f"follower: {follower_dict}")
|
logger.debug(f"follower: {follower_dict}")
|
||||||
|
|
||||||
if not user_id or not follower_dict:
|
if not viewer_id or not follower_dict:
|
||||||
logger.warning("Неавторизованный доступ при попытке отписаться")
|
logger.warning("Неавторизованный доступ при попытке отписаться")
|
||||||
return {"error": "unauthorized"}
|
return GraphQLError("Unauthorized")
|
||||||
|
|
||||||
follower_id = follower_dict.get("id")
|
follower_id = follower_dict.get("id")
|
||||||
logger.debug(f"follower_id: {follower_id}")
|
logger.debug(f"follower_id: {follower_id}")
|
||||||
|
@ -219,7 +222,6 @@ async def unfollow(_, info, what, slug="", entity_id=0):
|
||||||
# Если это авторы, получаем безопасную версию
|
# Если это авторы, получаем безопасную версию
|
||||||
if what == "AUTHOR":
|
if what == "AUTHOR":
|
||||||
# Получаем ID текущего пользователя и фильтруем данные
|
# Получаем ID текущего пользователя и фильтруем данные
|
||||||
current_user_id = user_id
|
|
||||||
follows_filtered = []
|
follows_filtered = []
|
||||||
|
|
||||||
for author_data in existing_follows:
|
for author_data in existing_follows:
|
||||||
|
@ -232,7 +234,7 @@ async def unfollow(_, info, what, slug="", entity_id=0):
|
||||||
if hasattr(temp_author, key):
|
if hasattr(temp_author, key):
|
||||||
setattr(temp_author, key, value)
|
setattr(temp_author, key, value)
|
||||||
# Добавляем отфильтрованную версию
|
# Добавляем отфильтрованную версию
|
||||||
follows_filtered.append(temp_author.dict(current_user_id, False))
|
follows_filtered.append(temp_author.dict(viewer_id, False))
|
||||||
|
|
||||||
follows = follows_filtered
|
follows = follows_filtered
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -96,7 +96,6 @@ async def get_my_rates_shouts(_, info, shouts):
|
||||||
@mutation.field("rate_author")
|
@mutation.field("rate_author")
|
||||||
@login_required
|
@login_required
|
||||||
async def rate_author(_, info, rated_slug, value):
|
async def rate_author(_, info, rated_slug, value):
|
||||||
info.context["user_id"]
|
|
||||||
rater_id = info.context.get("author", {}).get("id")
|
rater_id = info.context.get("author", {}).get("id")
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
rater_id = int(rater_id)
|
rater_id = int(rater_id)
|
||||||
|
|
|
@ -383,11 +383,11 @@ async def update_reaction(_, info, reaction):
|
||||||
:param reaction: Dictionary with reaction data.
|
:param reaction: Dictionary with reaction data.
|
||||||
:return: Dictionary with updated reaction data or error.
|
:return: Dictionary with updated reaction data or error.
|
||||||
"""
|
"""
|
||||||
user_id = info.context.get("user_id")
|
author_id = info.context.get("author", {}).get("id")
|
||||||
roles = info.context.get("roles")
|
roles = info.context.get("roles")
|
||||||
rid = reaction.get("id")
|
rid = reaction.get("id")
|
||||||
|
|
||||||
if not rid or not user_id or not roles:
|
if not rid or not author_id or not roles:
|
||||||
return {"error": "Invalid input data"}
|
return {"error": "Invalid input data"}
|
||||||
|
|
||||||
del reaction["id"]
|
del reaction["id"]
|
||||||
|
@ -437,16 +437,15 @@ async def delete_reaction(_, info, reaction_id: int):
|
||||||
:param reaction_id: Reaction ID to delete.
|
:param reaction_id: Reaction ID to delete.
|
||||||
:return: Dictionary with deleted reaction data or error.
|
:return: Dictionary with deleted reaction data or error.
|
||||||
"""
|
"""
|
||||||
user_id = info.context.get("user_id")
|
|
||||||
author_id = info.context.get("author", {}).get("id")
|
author_id = info.context.get("author", {}).get("id")
|
||||||
roles = info.context.get("roles", [])
|
roles = info.context.get("roles", [])
|
||||||
|
|
||||||
if not user_id:
|
if not author_id:
|
||||||
return {"error": "Unauthorized"}
|
return {"error": "Unauthorized"}
|
||||||
|
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
try:
|
try:
|
||||||
author = session.query(Author).filter(Author.id == user_id).one()
|
author = session.query(Author).filter(Author.id == author_id).one()
|
||||||
r = session.query(Reaction).filter(Reaction.id == reaction_id).one()
|
r = session.query(Reaction).filter(Reaction.id == reaction_id).one()
|
||||||
|
|
||||||
if r.created_by != author_id and "editor" not in roles:
|
if r.created_by != author_id and "editor" not in roles:
|
||||||
|
@ -463,7 +462,7 @@ async def delete_reaction(_, info, reaction_id: int):
|
||||||
session.commit()
|
session.commit()
|
||||||
# TODO: add more reaction types here
|
# TODO: add more reaction types here
|
||||||
else:
|
else:
|
||||||
logger.debug(f"{user_id} user removing his #{reaction_id} reaction")
|
logger.debug(f"{author_id} user removing his #{reaction_id} reaction")
|
||||||
session.delete(r)
|
session.delete(r)
|
||||||
session.commit()
|
session.commit()
|
||||||
if check_to_unfeature(session, r):
|
if check_to_unfeature(session, r):
|
||||||
|
|
|
@ -217,6 +217,7 @@ def get_shouts_with_links(info, q, limit=20, offset=0):
|
||||||
shout_id = int(f"{shout.id}")
|
shout_id = int(f"{shout.id}")
|
||||||
shout_dict = shout.dict()
|
shout_dict = shout.dict()
|
||||||
|
|
||||||
|
# Обработка поля created_by
|
||||||
if has_field(info, "created_by") and shout_dict.get("created_by"):
|
if has_field(info, "created_by") and shout_dict.get("created_by"):
|
||||||
main_author_id = shout_dict.get("created_by")
|
main_author_id = shout_dict.get("created_by")
|
||||||
a = session.query(Author).filter(Author.id == main_author_id).first()
|
a = session.query(Author).filter(Author.id == main_author_id).first()
|
||||||
|
@ -226,6 +227,44 @@ def get_shouts_with_links(info, q, limit=20, offset=0):
|
||||||
"slug": a.slug,
|
"slug": a.slug,
|
||||||
"pic": a.pic,
|
"pic": a.pic,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Обработка поля updated_by
|
||||||
|
if has_field(info, "updated_by"):
|
||||||
|
if shout_dict.get("updated_by"):
|
||||||
|
updated_by_id = shout_dict.get("updated_by")
|
||||||
|
updated_author = session.query(Author).filter(Author.id == updated_by_id).first()
|
||||||
|
if updated_author:
|
||||||
|
shout_dict["updated_by"] = {
|
||||||
|
"id": updated_author.id,
|
||||||
|
"name": updated_author.name,
|
||||||
|
"slug": updated_author.slug,
|
||||||
|
"pic": updated_author.pic,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Если автор не найден, устанавливаем поле в null
|
||||||
|
shout_dict["updated_by"] = None
|
||||||
|
else:
|
||||||
|
# Если updated_by не указан, устанавливаем поле в null
|
||||||
|
shout_dict["updated_by"] = None
|
||||||
|
|
||||||
|
# Обработка поля deleted_by
|
||||||
|
if has_field(info, "deleted_by"):
|
||||||
|
if shout_dict.get("deleted_by"):
|
||||||
|
deleted_by_id = shout_dict.get("deleted_by")
|
||||||
|
deleted_author = session.query(Author).filter(Author.id == deleted_by_id).first()
|
||||||
|
if deleted_author:
|
||||||
|
shout_dict["deleted_by"] = {
|
||||||
|
"id": deleted_author.id,
|
||||||
|
"name": deleted_author.name,
|
||||||
|
"slug": deleted_author.slug,
|
||||||
|
"pic": deleted_author.pic,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Если автор не найден, устанавливаем поле в null
|
||||||
|
shout_dict["deleted_by"] = None
|
||||||
|
else:
|
||||||
|
# Если deleted_by не указан, устанавливаем поле в null
|
||||||
|
shout_dict["deleted_by"] = None
|
||||||
|
|
||||||
if has_field(info, "stat"):
|
if has_field(info, "stat"):
|
||||||
stat = {}
|
stat = {}
|
||||||
|
|
|
@ -315,12 +315,12 @@ async def update_topic(_, _info, topic_input):
|
||||||
@mutation.field("delete_topic")
|
@mutation.field("delete_topic")
|
||||||
@login_required
|
@login_required
|
||||||
async def delete_topic(_, info, slug: str):
|
async def delete_topic(_, info, slug: str):
|
||||||
user_id = info.context["user_id"]
|
viewer_id = info.context.get("author", {}).get("id")
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
t: Topic = session.query(Topic).filter(Topic.slug == slug).first()
|
t: Topic = session.query(Topic).filter(Topic.slug == slug).first()
|
||||||
if not t:
|
if not t:
|
||||||
return {"error": "invalid topic slug"}
|
return {"error": "invalid topic slug"}
|
||||||
author = session.query(Author).filter(Author.id == user_id).first()
|
author = session.query(Author).filter(Author.id == viewer_id).first()
|
||||||
if author:
|
if author:
|
||||||
if t.created_by != author.id:
|
if t.created_by != author.id:
|
||||||
return {"error": "access denied"}
|
return {"error": "access denied"}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
type Query {
|
type Query {
|
||||||
# author
|
# author
|
||||||
get_author(slug: String, author_id: Int): Author
|
get_author(slug: String, author_id: Int): Author
|
||||||
get_author_id(user: String!): Author
|
|
||||||
get_authors_all: [Author]
|
get_authors_all: [Author]
|
||||||
load_authors_by(by: AuthorsBy!, limit: Int, offset: Int): [Author]
|
load_authors_by(by: AuthorsBy!, limit: Int, offset: Int): [Author]
|
||||||
load_authors_search(text: String!, limit: Int, offset: Int): [Author!] # Search for authors by name or bio
|
load_authors_search(text: String!, limit: Int, offset: Int): [Author!] # Search for authors by name or bio
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import Tuple
|
||||||
|
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from cache.cache import get_cached_author_by_user_id
|
from cache.cache import get_cached_author_by_id
|
||||||
from resolvers.stat import get_with_stat
|
from resolvers.stat import get_with_stat
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
from auth.internal import verify_internal_auth
|
from auth.internal import verify_internal_auth
|
||||||
|
@ -147,13 +147,12 @@ def login_required(f):
|
||||||
raise GraphQLError("У вас нет необходимых прав для доступа")
|
raise GraphQLError("У вас нет необходимых прав для доступа")
|
||||||
|
|
||||||
logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}")
|
logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}")
|
||||||
info.context["user_id"] = user_id.strip()
|
|
||||||
info.context["roles"] = user_roles
|
info.context["roles"] = user_roles
|
||||||
|
|
||||||
# Проверяем права администратора
|
# Проверяем права администратора
|
||||||
info.context["is_admin"] = is_admin
|
info.context["is_admin"] = is_admin
|
||||||
|
|
||||||
author = await get_cached_author_by_user_id(user_id, get_with_stat)
|
author = await get_cached_author_by_id(user_id, get_with_stat)
|
||||||
if not author:
|
if not author:
|
||||||
logger.error(f"Профиль автора не найден для пользователя {user_id}")
|
logger.error(f"Профиль автора не найден для пользователя {user_id}")
|
||||||
info.context["author"] = author
|
info.context["author"] = author
|
||||||
|
@ -177,14 +176,13 @@ def login_accepted(f):
|
||||||
|
|
||||||
if user_id and user_roles:
|
if user_id and user_roles:
|
||||||
logger.info(f"login_accepted: Пользователь авторизован: {user_id} с ролями {user_roles}")
|
logger.info(f"login_accepted: Пользователь авторизован: {user_id} с ролями {user_roles}")
|
||||||
info.context["user_id"] = user_id.strip()
|
|
||||||
info.context["roles"] = user_roles
|
info.context["roles"] = user_roles
|
||||||
|
|
||||||
# Проверяем права администратора
|
# Проверяем права администратора
|
||||||
info.context["is_admin"] = is_admin
|
info.context["is_admin"] = is_admin
|
||||||
|
|
||||||
# Пробуем получить профиль автора
|
# Пробуем получить профиль автора
|
||||||
author = await get_cached_author_by_user_id(user_id, get_with_stat)
|
author = await get_cached_author_by_id(user_id, get_with_stat)
|
||||||
if author:
|
if author:
|
||||||
logger.debug(f"login_accepted: Найден профиль автора: {author}")
|
logger.debug(f"login_accepted: Найден профиль автора: {author}")
|
||||||
# Используем флаг is_admin из контекста или передаем права владельца для собственных данных
|
# Используем флаг is_admin из контекста или передаем права владельца для собственных данных
|
||||||
|
@ -196,7 +194,6 @@ def login_accepted(f):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.")
|
logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.")
|
||||||
info.context["user_id"] = None
|
|
||||||
info.context["roles"] = None
|
info.context["roles"] = None
|
||||||
info.context["author"] = None
|
info.context["author"] = None
|
||||||
info.context["is_admin"] = False
|
info.context["is_admin"] = False
|
||||||
|
|
|
@ -63,16 +63,16 @@ class EnvManager:
|
||||||
},
|
},
|
||||||
"APP": {
|
"APP": {
|
||||||
"pattern": r"^(APP|PORT|HOST|DEBUG|DOMAIN|ENVIRONMENT|ENV|FRONTEND)_",
|
"pattern": r"^(APP|PORT|HOST|DEBUG|DOMAIN|ENVIRONMENT|ENV|FRONTEND)_",
|
||||||
"name": "Приложение",
|
"name": "Общие настройки",
|
||||||
"description": "Основные настройки приложения"
|
"description": "Общие настройки приложения"
|
||||||
},
|
},
|
||||||
"LOGGING": {
|
"LOGGING": {
|
||||||
"pattern": r"^(LOG|LOGGING|SENTRY|GLITCH|GLITCHTIP)_",
|
"pattern": r"^(LOG|LOGGING|SENTRY|GLITCH|GLITCHTIP)_",
|
||||||
"name": "Логирование",
|
"name": "Мониторинг",
|
||||||
"description": "Настройки логирования и мониторинга"
|
"description": "Настройки логирования и мониторинга"
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"pattern": r"^(MAIL|EMAIL|SMTP)_",
|
"pattern": r"^(MAIL|EMAIL|SMTP|IMAP|POP3|POST)_",
|
||||||
"name": "Электронная почта",
|
"name": "Электронная почта",
|
||||||
"description": "Настройки отправки электронной почты"
|
"description": "Настройки отправки электронной почты"
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,13 +5,12 @@ import os
|
||||||
import httpx
|
import httpx
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
from collections import defaultdict
|
from settings import TXTAI_SERVICE_URL
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
# Set up proper logging
|
# Set up proper logging
|
||||||
logger = logging.getLogger("search")
|
logger = logging.getLogger("search")
|
||||||
logger.setLevel(logging.INFO) # Change to INFO to see more details
|
logger.setLevel(logging.INFO) # Change to INFO to see more details
|
||||||
# Disable noise HTTP client logging
|
# Disable noise HTTP cltouchient logging
|
||||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
@ -19,7 +18,7 @@ logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||||
SEARCH_ENABLED = bool(
|
SEARCH_ENABLED = bool(
|
||||||
os.environ.get("SEARCH_ENABLED", "true").lower() in ["true", "1", "yes"]
|
os.environ.get("SEARCH_ENABLED", "true").lower() in ["true", "1", "yes"]
|
||||||
)
|
)
|
||||||
TXTAI_SERVICE_URL = os.environ.get("TXTAI_SERVICE_URL", "none")
|
|
||||||
MAX_BATCH_SIZE = int(os.environ.get("SEARCH_MAX_BATCH_SIZE", "25"))
|
MAX_BATCH_SIZE = int(os.environ.get("SEARCH_MAX_BATCH_SIZE", "25"))
|
||||||
|
|
||||||
# Search cache configuration
|
# Search cache configuration
|
||||||
|
@ -948,3 +947,48 @@ async def initialize_search_index(shouts_data):
|
||||||
categories.add(getattr(matching_shouts[0], "category", "unknown"))
|
categories.add(getattr(matching_shouts[0], "category", "unknown"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def check_search_service():
|
||||||
|
info = await search_service.info()
|
||||||
|
if info.get("status") in ["error", "unavailable"]:
|
||||||
|
print(f"[WARNING] Search service unavailable: {info.get('message', 'unknown reason')}")
|
||||||
|
else:
|
||||||
|
print(f"[INFO] Search service is available: {info}")
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize search index in the background
|
||||||
|
async def initialize_search_index_background():
|
||||||
|
"""
|
||||||
|
Запускает индексацию поиска в фоновом режиме с низким приоритетом.
|
||||||
|
|
||||||
|
Эта функция:
|
||||||
|
1. Загружает все shouts из базы данных
|
||||||
|
2. Индексирует их в поисковом сервисе
|
||||||
|
3. Выполняется асинхронно, не блокируя основной поток
|
||||||
|
4. Обрабатывает возможные ошибки, не прерывая работу приложения
|
||||||
|
|
||||||
|
Индексация запускается с задержкой после инициализации сервера,
|
||||||
|
чтобы не создавать дополнительную нагрузку при запуске.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print("[search] Starting background search indexing process")
|
||||||
|
from services.db import fetch_all_shouts
|
||||||
|
|
||||||
|
# Get total count first (optional)
|
||||||
|
all_shouts = await fetch_all_shouts()
|
||||||
|
total_count = len(all_shouts) if all_shouts else 0
|
||||||
|
print(f"[search] Fetched {total_count} shouts for background indexing")
|
||||||
|
|
||||||
|
if not all_shouts:
|
||||||
|
print("[search] No shouts found for indexing, skipping search index initialization")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Start the indexing process with the fetched shouts
|
||||||
|
print("[search] Beginning background search index initialization...")
|
||||||
|
await initialize_search_index(all_shouts)
|
||||||
|
print("[search] Background search index initialization complete")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[search] Error in background search indexing: {str(e)}")
|
||||||
|
# Логируем детали ошибки для диагностики
|
||||||
|
logger.exception("[search] Detailed search indexing error")
|
||||||
|
|
|
@ -70,3 +70,6 @@ SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 days
|
||||||
|
|
||||||
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY", "")
|
MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY", "")
|
||||||
MAILGUN_DOMAIN = os.getenv("MAILGUN_DOMAIN", "discours.io")
|
MAILGUN_DOMAIN = os.getenv("MAILGUN_DOMAIN", "discours.io")
|
||||||
|
|
||||||
|
|
||||||
|
TXTAI_SERVICE_URL = os.environ.get("TXTAI_SERVICE_URL", "none")
|
Loading…
Reference in New Issue
Block a user