auth fixes, search connected
This commit is contained in:
parent
32bc1276e0
commit
ab39b534fe
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -1,5 +1,27 @@
|
|||
# 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
|
||||
|
||||
|
|
|
@ -376,16 +376,13 @@ def login_accepted(func):
|
|||
try:
|
||||
author = session.query(Author).filter(Author.id == auth.author_id).one()
|
||||
info.context["author"] = author.dict()
|
||||
info.context["user_id"] = author.id
|
||||
logger.debug(f"[login_accepted] Пользователь авторизован: {author.id}")
|
||||
except exc.NoResultFound:
|
||||
logger.warning(f"[login_accepted] Пользователь с ID {auth.author_id} не найден в базе данных")
|
||||
info.context["author"] = None
|
||||
info.context["user_id"] = None
|
||||
else:
|
||||
# Если пользователь не авторизован, устанавливаем пустые значения
|
||||
info.context["author"] = None
|
||||
info.context["user_id"] = None
|
||||
logger.debug("[login_accepted] Пользователь не авторизован")
|
||||
|
||||
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 запросах
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.datastructures import Headers
|
||||
from starlette.types import ASGIApp, Scope, Receive, Send
|
||||
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:
|
||||
|
@ -197,3 +199,76 @@ class AuthMiddleware:
|
|||
except Exception as e:
|
||||
logger.error(f"[AuthMiddleware] Ошибка в GraphQL resolve: {str(e)}")
|
||||
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
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
"""
|
||||
# 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 ID is found, get full author data by 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)
|
||||
|
||||
# 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)
|
||||
if authors:
|
||||
# Cache the retrieved author data
|
||||
author = authors[0]
|
||||
author_dict = author.dict()
|
||||
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)),
|
||||
)
|
||||
return author_dict
|
||||
|
|
345
main.py
345
main.py
|
@ -5,35 +5,32 @@ from os.path import exists, join
|
|||
|
||||
from ariadne import load_schema_from_path, make_executable_schema
|
||||
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.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||
from starlette.middleware import Middleware
|
||||
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.staticfiles import StaticFiles
|
||||
from starlette.types import ASGIApp
|
||||
|
||||
from cache.precache import precache_data
|
||||
from cache.revalidator import revalidation_manager
|
||||
from services.exception import ExceptionHandlerMiddleware
|
||||
from services.redis import redis
|
||||
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 auth.internal import InternalAuthentication
|
||||
from auth.middleware import AuthMiddleware
|
||||
from settings import (
|
||||
SESSION_COOKIE_NAME,
|
||||
SESSION_COOKIE_HTTPONLY,
|
||||
SESSION_COOKIE_SECURE,
|
||||
SESSION_COOKIE_SAMESITE,
|
||||
SESSION_COOKIE_MAX_AGE,
|
||||
SESSION_TOKEN_HEADER,
|
||||
)
|
||||
from settings import DEV_SERVER_PID_FILE_NAME
|
||||
|
||||
DEVMODE = os.getenv("DOKKU_APP_TYPE", "false").lower() == "false"
|
||||
DIST_DIR = join(os.path.dirname(__file__), "dist") # Директория для собранных файлов
|
||||
INDEX_HTML = join(os.path.dirname(__file__), "index.html")
|
||||
|
||||
# Импортируем резолверы
|
||||
import_module("resolvers")
|
||||
|
@ -41,118 +38,6 @@ import_module("resolvers")
|
|||
# Создаем схему GraphQL
|
||||
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 = [
|
||||
# Начинаем с обработки ошибок
|
||||
|
@ -172,9 +57,9 @@ middleware = [
|
|||
allow_headers=["*"],
|
||||
allow_credentials=True,
|
||||
),
|
||||
# После CORS идёт обработка авторизации
|
||||
# Сначала AuthMiddleware (для обработки токенов)
|
||||
Middleware(AuthMiddleware),
|
||||
# И затем аутентификация
|
||||
# Затем AuthenticationMiddleware (для создания request.user на основе токена)
|
||||
Middleware(AuthenticationMiddleware, backend=InternalAuthentication()),
|
||||
]
|
||||
|
||||
|
@ -182,81 +67,169 @@ middleware = [
|
|||
# Создаем экземпляр GraphQL с улучшенным обработчиком
|
||||
graphql_app = GraphQL(
|
||||
schema,
|
||||
debug=True,
|
||||
debug=DEVMODE,
|
||||
http_handler=EnhancedGraphQLHTTPHandler()
|
||||
)
|
||||
|
||||
|
||||
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
|
||||
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"]:
|
||||
return JSONResponse({"error": "Method Not Allowed by main.py"}, status_code=405)
|
||||
|
||||
try:
|
||||
# Обрабатываем CORS для OPTIONS запросов
|
||||
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
|
||||
# Проверяем, что все необходимые middleware корректно отработали
|
||||
if not hasattr(request, "scope") or "auth" not in request.scope:
|
||||
logger.warning("[graphql] AuthMiddleware не обработал запрос перед GraphQL обработчиком")
|
||||
|
||||
try:
|
||||
# Обрабатываем запрос через GraphQL приложение
|
||||
result = await graphql_app.handle_request(request)
|
||||
|
||||
# Если результат не является Response, преобразуем его в JSONResponse
|
||||
if not isinstance(result, Response):
|
||||
response = JSONResponse(result)
|
||||
|
||||
# Проверяем, был ли токен в запросе или ответе
|
||||
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
|
||||
# Применяем middleware для установки cookie
|
||||
# Используем метод process_result из auth_middleware для корректной обработки
|
||||
# cookie на основе результатов операций login/logout
|
||||
response = await auth_middleware.process_result(request, result)
|
||||
return response
|
||||
except asyncio.CancelledError:
|
||||
return JSONResponse({"error": "Request cancelled"}, status_code=499)
|
||||
except Exception as 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)
|
||||
|
||||
# Добавляем маршруты, порядок имеет значение
|
||||
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(
|
||||
routes=routes,
|
||||
middleware=middleware,
|
||||
on_startup=[start],
|
||||
on_shutdown=[shutdown],
|
||||
routes=[
|
||||
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
|
||||
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True))
|
||||
],
|
||||
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_authors,
|
||||
get_author_follows_topics,
|
||||
get_author_id,
|
||||
get_authors_all,
|
||||
load_authors_by,
|
||||
load_authors_search,
|
||||
|
@ -18,6 +17,7 @@ from resolvers.draft import (
|
|||
load_drafts,
|
||||
publish_draft,
|
||||
update_draft,
|
||||
unpublish_draft,
|
||||
)
|
||||
from resolvers.editor import (
|
||||
unpublish_shout,
|
||||
|
@ -91,7 +91,6 @@ __all__ = [
|
|||
|
||||
# author
|
||||
"get_author",
|
||||
"get_author_id",
|
||||
"get_author_followers",
|
||||
"get_author_follows",
|
||||
"get_author_follows_topics",
|
||||
|
@ -161,7 +160,6 @@ __all__ = [
|
|||
"update_draft",
|
||||
"delete_draft",
|
||||
"publish_draft",
|
||||
"publish_shout",
|
||||
"unpublish_shout",
|
||||
"unpublish_draft",
|
||||
]
|
||||
|
|
|
@ -42,11 +42,11 @@ async def get_current_user(_, info):
|
|||
info: Контекст GraphQL запроса
|
||||
|
||||
Returns:
|
||||
dict: Объект с токеном и данными автора
|
||||
dict: Объект с токеном и данными автора с добавленной статистикой
|
||||
"""
|
||||
# Получаем данные авторизации из контекста запроса
|
||||
user_id = info.context.get("user_id")
|
||||
if not user_id:
|
||||
author_id = info.context.get("author", {}).get("id")
|
||||
if not author_id:
|
||||
logger.error("[getSession] Пользователь не авторизован")
|
||||
from graphql.error import GraphQLError
|
||||
raise GraphQLError("Требуется авторизация")
|
||||
|
@ -60,19 +60,50 @@ async def get_current_user(_, info):
|
|||
# Получаем данные автора
|
||||
author = info.context.get("author")
|
||||
|
||||
# Если автор не найден в контексте, пробуем получить из БД
|
||||
# Если автор не найден в контексте, пробуем получить из БД с добавлением статистики
|
||||
if not author:
|
||||
logger.debug(f"[getSession] Автор не найден в контексте для пользователя {user_id}, получаем из БД")
|
||||
with local_session() as session:
|
||||
try:
|
||||
db_author = session.query(Author).filter(Author.id == user_id).one()
|
||||
db_author.last_seen = int(time.time())
|
||||
session.commit()
|
||||
author = db_author
|
||||
except Exception as e:
|
||||
logger.error(f"[getSession] Ошибка при получении автора из БД: {e}")
|
||||
|
||||
try:
|
||||
# Используем функцию get_with_stat для получения автора со статистикой
|
||||
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 = 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
|
||||
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}")
|
||||
|
|
|
@ -8,7 +8,6 @@ from cache.cache import (
|
|||
cache_author,
|
||||
cached_query,
|
||||
get_cached_author,
|
||||
get_cached_author_by_user_id,
|
||||
get_cached_author_followers,
|
||||
get_cached_follower_authors,
|
||||
get_cached_follower_topics,
|
||||
|
@ -205,25 +204,24 @@ async def invalidate_authors_cache(author_id=None):
|
|||
@mutation.field("update_author")
|
||||
@login_required
|
||||
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)
|
||||
|
||||
if not user_id:
|
||||
if not author_id:
|
||||
return {"error": "unauthorized", "author": None}
|
||||
try:
|
||||
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:
|
||||
Author.update(author, profile)
|
||||
session.add(author)
|
||||
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)
|
||||
if result:
|
||||
author_with_stat = result[0]
|
||||
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))
|
||||
|
||||
# Возвращаем обычную полную версию, т.к. это владелец
|
||||
|
@ -244,16 +242,16 @@ async def get_authors_all(_, info):
|
|||
list: Список всех авторов
|
||||
"""
|
||||
# Получаем ID текущего пользователя и флаг админа из контекста
|
||||
current_user_id = info.context.get("user_id") if hasattr(info, "context") else None
|
||||
authors = await get_all_authors(current_user_id, False)
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
authors = await get_all_authors(viewer_id, is_admin)
|
||||
return authors
|
||||
|
||||
|
||||
@query.field("get_author")
|
||||
async def get_author(_, info, slug="", author_id=0):
|
||||
# Получаем 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
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
author_dict = None
|
||||
try:
|
||||
|
@ -272,7 +270,7 @@ async def get_author(_, info, slug="", author_id=0):
|
|||
if hasattr(temp_author, key):
|
||||
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:
|
||||
author_dict["stat"] = cached_author["stat"]
|
||||
|
@ -285,11 +283,11 @@ async def get_author(_, info, slug="", author_id=0):
|
|||
author_with_stat = result[0]
|
||||
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))
|
||||
|
||||
# Возвращаем отфильтрованную версию
|
||||
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"):
|
||||
author_dict["stat"] = author_with_stat.stat
|
||||
|
@ -302,42 +300,6 @@ async def get_author(_, info, slug="", author_id=0):
|
|||
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")
|
||||
async def load_authors_by(_, info, by, limit, offset):
|
||||
"""
|
||||
|
@ -352,11 +314,11 @@ async def load_authors_by(_, info, by, limit, offset):
|
|||
list: Список авторов с учетом критерия
|
||||
"""
|
||||
# Получаем 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
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
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")
|
||||
|
@ -423,8 +385,8 @@ def get_author_id_from(slug="", user=None, author_id=None):
|
|||
@query.field("get_author_follows")
|
||||
async def get_author_follows(_, info, slug="", user=None, author_id=0):
|
||||
# Получаем 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
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
logger.debug(f"getting follows for @{slug}")
|
||||
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, который мы хотим сериализовать
|
||||
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||
# 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))
|
||||
|
||||
# TODO: Get followed communities too
|
||||
|
@ -472,14 +434,14 @@ async def get_author_follows_topics(_, _info, slug="", user=None, author_id=None
|
|||
@query.field("get_author_follows_authors")
|
||||
async def get_author_follows_authors(_, info, slug="", user=None, author_id=None):
|
||||
# Получаем 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
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
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:
|
||||
return []
|
||||
|
||||
|
||||
# Получаем данные из кэша
|
||||
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, который мы хотим сериализовать
|
||||
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||
# 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))
|
||||
|
||||
return followed_authors
|
||||
|
@ -517,8 +479,8 @@ def create_author(user_id: str, slug: str, name: str = ""):
|
|||
@query.field("get_author_followers")
|
||||
async def get_author_followers(_, info, slug: str = "", user: str = "", author_id: int = 0):
|
||||
# Получаем 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
|
||||
viewer_id = info.context.get("author", {}).get("id")
|
||||
is_admin = info.context.get("is_admin", False)
|
||||
|
||||
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)
|
||||
|
@ -540,7 +502,7 @@ async def get_author_followers(_, info, slug: str = "", user: str = "", author_i
|
|||
# temp_author - это объект Author, который мы хотим сериализовать
|
||||
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||
# 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))
|
||||
|
||||
return followers
|
||||
|
|
|
@ -9,7 +9,6 @@ from services.schema import mutation
|
|||
@mutation.field("accept_invite")
|
||||
@login_required
|
||||
async def accept_invite(_, info, invite_id: int):
|
||||
info.context["user_id"]
|
||||
author_dict = info.context["author"]
|
||||
author_id = author_dict.get("id")
|
||||
if author_id:
|
||||
|
@ -41,7 +40,6 @@ async def accept_invite(_, info, invite_id: int):
|
|||
@mutation.field("reject_invite")
|
||||
@login_required
|
||||
async def reject_invite(_, info, invite_id: int):
|
||||
info.context["user_id"]
|
||||
author_dict = info.context["author"]
|
||||
author_id = author_dict.get("id")
|
||||
|
||||
|
@ -64,14 +62,17 @@ async def reject_invite(_, info, invite_id: int):
|
|||
@mutation.field("create_invite")
|
||||
@login_required
|
||||
async def create_invite(_, info, slug: str = "", author_id: int = 0):
|
||||
user_id = info.context["user_id"]
|
||||
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:
|
||||
# Check if the inviter is the owner of the shout
|
||||
with local_session() as session:
|
||||
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:
|
||||
# Check if an invite already exists
|
||||
existing_invite = (
|
||||
|
@ -89,7 +90,7 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0):
|
|||
|
||||
# Create a new invite
|
||||
new_invite = Invite(
|
||||
inviter_id=user_id,
|
||||
inviter_id=viewer_id,
|
||||
author_id=author_id,
|
||||
shout_id=shout.id,
|
||||
status=InviteStatus.PENDING.value,
|
||||
|
@ -107,9 +108,13 @@ async def create_invite(_, info, slug: str = "", author_id: int = 0):
|
|||
@mutation.field("remove_author")
|
||||
@login_required
|
||||
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:
|
||||
author = session.query(Author).filter(Author.id == user_id).first()
|
||||
author = session.query(Author).filter(Author.id == author_id).first()
|
||||
if author:
|
||||
shout = session.query(Shout).filter(Shout.slug == slug).first()
|
||||
# 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")
|
||||
@login_required
|
||||
async def remove_invite(_, info, invite_id: int):
|
||||
info.context["user_id"]
|
||||
|
||||
author_dict = info.context["author"]
|
||||
author_id = author_dict.get("id")
|
||||
if isinstance(author_id, int):
|
||||
|
|
|
@ -78,12 +78,11 @@ async def load_drafts(_, info):
|
|||
Returns:
|
||||
dict: Список черновиков или сообщение об ошибке
|
||||
"""
|
||||
user_id = info.context.get("user_id")
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
|
||||
if not user_id or not author_id:
|
||||
return {"error": "User ID and author ID are required"}
|
||||
if not author_id:
|
||||
return {"error": "Author ID is required"}
|
||||
|
||||
try:
|
||||
with local_session() as session:
|
||||
|
@ -152,11 +151,10 @@ async def create_draft(_, info, draft_input):
|
|||
... assert result['draft'].title == 'Test'
|
||||
... return result
|
||||
"""
|
||||
user_id = info.context.get("user_id")
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
|
||||
if not user_id or not author_id:
|
||||
if not author_id:
|
||||
return {"error": "Author ID is required"}
|
||||
|
||||
# Проверяем обязательные поля
|
||||
|
@ -227,11 +225,10 @@ async def update_draft(_, info, draft_id: int, draft_input):
|
|||
Returns:
|
||||
dict: Обновленный черновик или сообщение об ошибке
|
||||
"""
|
||||
user_id = info.context.get("user_id")
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
|
||||
if not user_id or not author_id:
|
||||
if not author_id:
|
||||
return {"error": "Author ID are required"}
|
||||
|
||||
try:
|
||||
|
@ -389,11 +386,10 @@ async def publish_draft(_, info, draft_id: int):
|
|||
Returns:
|
||||
dict: Результат публикации с shout или сообщением об ошибке
|
||||
"""
|
||||
user_id = info.context.get("user_id")
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
|
||||
if not user_id or not author_id:
|
||||
if not author_id:
|
||||
return {"error": "Author ID is required"}
|
||||
|
||||
try:
|
||||
|
@ -469,7 +465,7 @@ async def publish_draft(_, info, draft_id: int):
|
|||
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.debug(f"Shout data: {shout.dict()}")
|
||||
|
@ -479,3 +475,74 @@ async def publish_draft(_, info, draft_id: int):
|
|||
except Exception as e:
|
||||
logger.error(f"Failed to publish draft {draft_id}: {e}", exc_info=True)
|
||||
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
|
||||
... return result
|
||||
"""
|
||||
user_id = info.context.get("user_id", "")
|
||||
author_dict = info.context.get("author", {})
|
||||
author_id = author_dict.get("id")
|
||||
roles = info.context.get("roles", [])
|
||||
shout = None
|
||||
if not user_id or not author_id:
|
||||
if not author_id:
|
||||
return {"error": "unauthorized", "shout": None}
|
||||
with local_session() as session:
|
||||
shout = (
|
||||
|
@ -136,7 +135,6 @@ async def get_my_shout(_, info, shout_id: int):
|
|||
@query.field("get_shouts_drafts")
|
||||
@login_required
|
||||
async def get_shouts_drafts(_, info):
|
||||
# user_id = info.context.get("user_id")
|
||||
author_dict = info.context.get("author")
|
||||
if not author_dict:
|
||||
return {"error": "author profile was not found"}
|
||||
|
@ -160,16 +158,15 @@ async def get_shouts_drafts(_, info):
|
|||
# @login_required
|
||||
async def create_shout(_, info, inp):
|
||||
logger.info(f"Starting create_shout with input: {inp}")
|
||||
user_id = info.context.get("user_id")
|
||||
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:
|
||||
logger.error("Author profile not found in context")
|
||||
return {"error": "author profile was not found"}
|
||||
|
||||
author_id = author_dict.get("id")
|
||||
if user_id and author_id:
|
||||
if author_id:
|
||||
try:
|
||||
with local_session() as session:
|
||||
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)
|
||||
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}")
|
||||
return {"error": error_msg}
|
||||
|
||||
|
@ -394,26 +391,19 @@ def patch_topics(session, shout, topics_input):
|
|||
# @mutation.field("update_shout")
|
||||
# @login_required
|
||||
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}")
|
||||
logger.debug(f"Full shout_input: {shout_input}") # DraftInput
|
||||
|
||||
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:
|
||||
author_id = info.context.get("author").get("id")
|
||||
if not author_id:
|
||||
logger.error("Unauthorized update attempt")
|
||||
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:
|
||||
with local_session() as session:
|
||||
if author_id:
|
||||
|
@ -620,13 +610,12 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
|
|||
# @mutation.field("delete_shout")
|
||||
# @login_required
|
||||
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")
|
||||
if not author_dict:
|
||||
return {"error": "author profile was not found"}
|
||||
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)
|
||||
with local_session() as session:
|
||||
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:
|
||||
await cache_by_id(Author, author.id, cache_author)
|
||||
info.context["author"] = author.dict()
|
||||
info.context["user_id"] = author.id
|
||||
unfollow(None, info, "shout", shout.slug)
|
||||
|
||||
for topic in shout.topics:
|
||||
|
@ -746,7 +734,7 @@ async def unpublish_shout(_, info, shout_id: int):
|
|||
return {"error": "Shout not found"}
|
||||
|
||||
# Если у публикации есть связанный черновик, загружаем его с relationships
|
||||
if shout.draft:
|
||||
if shout.draft is not None:
|
||||
# Отдельно загружаем черновик с его связями
|
||||
draft = (
|
||||
session.query(Draft)
|
||||
|
|
|
@ -27,12 +27,14 @@ from utils.logger import root_logger as logger
|
|||
@login_required
|
||||
async def follow(_, info, what, slug="", entity_id=0):
|
||||
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")
|
||||
logger.debug(f"follower: {follower_dict}")
|
||||
|
||||
if not user_id or not follower_dict:
|
||||
return GraphQLError("unauthorized")
|
||||
if not viewer_id or not follower_dict:
|
||||
return GraphQLError("Access denied")
|
||||
|
||||
follower_id = follower_dict.get("id")
|
||||
logger.debug(f"follower_id: {follower_id}")
|
||||
|
@ -107,7 +109,6 @@ async def follow(_, info, what, slug="", entity_id=0):
|
|||
# Если это авторы, получаем безопасную версию
|
||||
if what == "AUTHOR":
|
||||
# Получаем ID текущего пользователя и фильтруем данные
|
||||
current_user_id = user_id
|
||||
follows_filtered = []
|
||||
|
||||
for author_data in existing_follows:
|
||||
|
@ -117,7 +118,7 @@ async def follow(_, info, what, slug="", entity_id=0):
|
|||
if hasattr(temp_author, key):
|
||||
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:
|
||||
# Создаем объект автора для entity_dict
|
||||
|
@ -126,7 +127,7 @@ async def follow(_, info, what, slug="", entity_id=0):
|
|||
if hasattr(temp_author, key):
|
||||
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:
|
||||
follows = follows_filtered
|
||||
else:
|
||||
|
@ -149,13 +150,15 @@ async def follow(_, info, what, slug="", entity_id=0):
|
|||
@login_required
|
||||
async def unfollow(_, info, what, slug="", entity_id=0):
|
||||
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")
|
||||
logger.debug(f"follower: {follower_dict}")
|
||||
|
||||
if not user_id or not follower_dict:
|
||||
if not viewer_id or not follower_dict:
|
||||
logger.warning("Неавторизованный доступ при попытке отписаться")
|
||||
return {"error": "unauthorized"}
|
||||
return GraphQLError("Unauthorized")
|
||||
|
||||
follower_id = follower_dict.get("id")
|
||||
logger.debug(f"follower_id: {follower_id}")
|
||||
|
@ -219,7 +222,6 @@ async def unfollow(_, info, what, slug="", entity_id=0):
|
|||
# Если это авторы, получаем безопасную версию
|
||||
if what == "AUTHOR":
|
||||
# Получаем ID текущего пользователя и фильтруем данные
|
||||
current_user_id = user_id
|
||||
follows_filtered = []
|
||||
|
||||
for author_data in existing_follows:
|
||||
|
@ -232,7 +234,7 @@ async def unfollow(_, info, what, slug="", entity_id=0):
|
|||
if hasattr(temp_author, key):
|
||||
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
|
||||
else:
|
||||
|
|
|
@ -96,7 +96,6 @@ async def get_my_rates_shouts(_, info, shouts):
|
|||
@mutation.field("rate_author")
|
||||
@login_required
|
||||
async def rate_author(_, info, rated_slug, value):
|
||||
info.context["user_id"]
|
||||
rater_id = info.context.get("author", {}).get("id")
|
||||
with local_session() as session:
|
||||
rater_id = int(rater_id)
|
||||
|
|
|
@ -383,11 +383,11 @@ async def update_reaction(_, info, reaction):
|
|||
:param reaction: Dictionary with reaction data.
|
||||
: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")
|
||||
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"}
|
||||
|
||||
del reaction["id"]
|
||||
|
@ -437,16 +437,15 @@ async def delete_reaction(_, info, reaction_id: int):
|
|||
:param reaction_id: Reaction ID to delete.
|
||||
:return: Dictionary with deleted reaction data or error.
|
||||
"""
|
||||
user_id = info.context.get("user_id")
|
||||
author_id = info.context.get("author", {}).get("id")
|
||||
roles = info.context.get("roles", [])
|
||||
|
||||
if not user_id:
|
||||
if not author_id:
|
||||
return {"error": "Unauthorized"}
|
||||
|
||||
with local_session() as session:
|
||||
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()
|
||||
|
||||
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()
|
||||
# TODO: add more reaction types here
|
||||
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.commit()
|
||||
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_dict = shout.dict()
|
||||
|
||||
# Обработка поля created_by
|
||||
if has_field(info, "created_by") and shout_dict.get("created_by"):
|
||||
main_author_id = shout_dict.get("created_by")
|
||||
a = session.query(Author).filter(Author.id == main_author_id).first()
|
||||
|
@ -227,6 +228,44 @@ def get_shouts_with_links(info, q, limit=20, offset=0):
|
|||
"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"):
|
||||
stat = {}
|
||||
if isinstance(row.stat, str):
|
||||
|
|
|
@ -315,12 +315,12 @@ async def update_topic(_, _info, topic_input):
|
|||
@mutation.field("delete_topic")
|
||||
@login_required
|
||||
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:
|
||||
t: Topic = session.query(Topic).filter(Topic.slug == slug).first()
|
||||
if not t:
|
||||
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 t.created_by != author.id:
|
||||
return {"error": "access denied"}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
type Query {
|
||||
# author
|
||||
get_author(slug: String, author_id: Int): Author
|
||||
get_author_id(user: String!): Author
|
||||
get_authors_all: [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
|
||||
|
|
|
@ -3,7 +3,7 @@ from typing import Tuple
|
|||
|
||||
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 utils.logger import root_logger as logger
|
||||
from auth.internal import verify_internal_auth
|
||||
|
@ -147,13 +147,12 @@ def login_required(f):
|
|||
raise GraphQLError("У вас нет необходимых прав для доступа")
|
||||
|
||||
logger.info(f"Авторизован пользователь {user_id} с ролями: {user_roles}")
|
||||
info.context["user_id"] = user_id.strip()
|
||||
info.context["roles"] = user_roles
|
||||
|
||||
# Проверяем права администратора
|
||||
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:
|
||||
logger.error(f"Профиль автора не найден для пользователя {user_id}")
|
||||
info.context["author"] = author
|
||||
|
@ -177,14 +176,13 @@ def login_accepted(f):
|
|||
|
||||
if user_id and 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["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:
|
||||
logger.debug(f"login_accepted: Найден профиль автора: {author}")
|
||||
# Используем флаг is_admin из контекста или передаем права владельца для собственных данных
|
||||
|
@ -196,7 +194,6 @@ def login_accepted(f):
|
|||
)
|
||||
else:
|
||||
logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.")
|
||||
info.context["user_id"] = None
|
||||
info.context["roles"] = None
|
||||
info.context["author"] = None
|
||||
info.context["is_admin"] = False
|
||||
|
|
|
@ -63,16 +63,16 @@ class EnvManager:
|
|||
},
|
||||
"APP": {
|
||||
"pattern": r"^(APP|PORT|HOST|DEBUG|DOMAIN|ENVIRONMENT|ENV|FRONTEND)_",
|
||||
"name": "Приложение",
|
||||
"description": "Основные настройки приложения"
|
||||
"name": "Общие настройки",
|
||||
"description": "Общие настройки приложения"
|
||||
},
|
||||
"LOGGING": {
|
||||
"pattern": r"^(LOG|LOGGING|SENTRY|GLITCH|GLITCHTIP)_",
|
||||
"name": "Логирование",
|
||||
"name": "Мониторинг",
|
||||
"description": "Настройки логирования и мониторинга"
|
||||
},
|
||||
"EMAIL": {
|
||||
"pattern": r"^(MAIL|EMAIL|SMTP)_",
|
||||
"pattern": r"^(MAIL|EMAIL|SMTP|IMAP|POP3|POST)_",
|
||||
"name": "Электронная почта",
|
||||
"description": "Настройки отправки электронной почты"
|
||||
},
|
||||
|
|
|
@ -5,13 +5,12 @@ import os
|
|||
import httpx
|
||||
import time
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from settings import TXTAI_SERVICE_URL
|
||||
|
||||
# Set up proper logging
|
||||
logger = logging.getLogger("search")
|
||||
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("httpcore").setLevel(logging.WARNING)
|
||||
|
||||
|
@ -19,7 +18,7 @@ logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|||
SEARCH_ENABLED = bool(
|
||||
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"))
|
||||
|
||||
# Search cache configuration
|
||||
|
@ -948,3 +947,48 @@ async def initialize_search_index(shouts_data):
|
|||
categories.add(getattr(matching_shouts[0], "category", "unknown"))
|
||||
except Exception as e:
|
||||
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_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