2024-10-14 08:11:13 +00:00
|
|
|
|
import asyncio
|
2023-01-17 21:07:44 +00:00
|
|
|
|
import os
|
2022-09-03 10:50:14 +00:00
|
|
|
|
from importlib import import_module
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from os.path import exists, join
|
2023-12-17 20:30:20 +00:00
|
|
|
|
|
2024-02-19 08:58:02 +00:00
|
|
|
|
from ariadne import load_schema_from_path, make_executable_schema
|
2022-09-03 10:50:14 +00:00
|
|
|
|
from ariadne.asgi import GraphQL
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from ariadne.asgi.handlers import GraphQLHTTPHandler
|
2022-09-03 10:50:14 +00:00
|
|
|
|
from starlette.applications import Starlette
|
2024-12-11 22:04:11 +00:00
|
|
|
|
from starlette.middleware.cors import CORSMiddleware
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
|
|
|
|
from starlette.middleware import Middleware
|
2024-10-14 09:31:55 +00:00
|
|
|
|
from starlette.requests import Request
|
2025-05-19 08:25:41 +00:00
|
|
|
|
from starlette.responses import FileResponse, JSONResponse, Response
|
2025-05-16 06:23:48 +00:00
|
|
|
|
from starlette.routing import Route, Mount
|
|
|
|
|
from starlette.staticfiles import StaticFiles
|
2025-05-19 08:25:41 +00:00
|
|
|
|
from starlette.types import ASGIApp
|
2023-11-28 19:07:53 +00:00
|
|
|
|
|
2024-08-09 06:37:06 +00:00
|
|
|
|
from cache.precache import precache_data
|
|
|
|
|
from cache.revalidator import revalidation_manager
|
2024-06-04 06:07:46 +00:00
|
|
|
|
from services.exception import ExceptionHandlerMiddleware
|
2024-08-09 06:37:06 +00:00
|
|
|
|
from services.redis import redis
|
2025-02-10 15:04:08 +00:00
|
|
|
|
from services.schema import create_all_tables, resolvers
|
2024-05-18 08:28:38 +00:00
|
|
|
|
from services.search import search_service
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
from utils.logger import root_logger as logger
|
|
|
|
|
from auth.internal import InternalAuthentication
|
2025-05-19 08:25:41 +00:00
|
|
|
|
from auth.middleware import AuthMiddleware
|
2025-05-19 21:00:24 +00:00
|
|
|
|
from settings import (
|
|
|
|
|
SESSION_COOKIE_NAME,
|
|
|
|
|
SESSION_COOKIE_HTTPONLY,
|
|
|
|
|
SESSION_COOKIE_SECURE,
|
|
|
|
|
SESSION_COOKIE_SAMESITE,
|
|
|
|
|
SESSION_COOKIE_MAX_AGE,
|
|
|
|
|
SESSION_TOKEN_HEADER,
|
|
|
|
|
)
|
2024-01-25 19:41:27 +00:00
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
# Импортируем резолверы
|
2024-04-17 15:32:23 +00:00
|
|
|
|
import_module("resolvers")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
# Создаем схему GraphQL
|
2024-04-17 15:32:23 +00:00
|
|
|
|
schema = make_executable_schema(load_schema_from_path("schema/"), resolvers)
|
2024-02-19 08:58:02 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
# Пути к клиентским файлам
|
|
|
|
|
DIST_DIR = join(os.path.dirname(__file__), "dist") # Директория для собранных файлов
|
|
|
|
|
INDEX_HTML = join(os.path.dirname(__file__), "index.html")
|
2022-09-03 10:50:14 +00:00
|
|
|
|
|
2024-10-14 09:13:18 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
async def index_handler(request: Request):
|
|
|
|
|
"""
|
|
|
|
|
Раздача основного HTML файла
|
|
|
|
|
"""
|
|
|
|
|
return FileResponse(INDEX_HTML)
|
|
|
|
|
|
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
# Создаем единый экземпляр AuthMiddleware для использования с GraphQL
|
|
|
|
|
auth_middleware = AuthMiddleware(lambda scope, receive, send: None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EnhancedGraphQLHTTPHandler(GraphQLHTTPHandler):
|
2025-05-16 06:23:48 +00:00
|
|
|
|
"""
|
2025-05-19 08:25:41 +00:00
|
|
|
|
Улучшенный GraphQL HTTP обработчик с поддержкой cookie и авторизации
|
2025-05-16 06:23:48 +00:00
|
|
|
|
"""
|
2025-05-19 08:25:41 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
async def get_context_for_request(self, request: Request, data: dict) -> dict:
|
|
|
|
|
"""
|
2025-05-19 08:25:41 +00:00
|
|
|
|
Расширяем контекст для GraphQL запросов
|
2025-05-16 06:23:48 +00:00
|
|
|
|
"""
|
2025-05-19 08:25:41 +00:00
|
|
|
|
# Получаем стандартный контекст от базового класса
|
2025-05-16 06:23:48 +00:00
|
|
|
|
context = await super().get_context_for_request(request, data)
|
2025-05-19 08:25:41 +00:00
|
|
|
|
|
2025-05-19 21:00:24 +00:00
|
|
|
|
# Создаем объект ответа для установки cookie
|
2025-05-16 06:23:48 +00:00
|
|
|
|
response = JSONResponse({})
|
|
|
|
|
context["response"] = response
|
2025-05-19 08:25:41 +00:00
|
|
|
|
|
|
|
|
|
# Интегрируем с AuthMiddleware
|
2025-05-19 21:00:24 +00:00
|
|
|
|
auth_middleware.set_context(context)
|
2025-05-19 08:25:41 +00:00
|
|
|
|
context["extensions"] = auth_middleware
|
|
|
|
|
|
2025-05-19 21:00:24 +00:00
|
|
|
|
logger.debug(f"[graphql] Подготовлен расширенный контекст для запроса")
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
return context
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
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
|
2024-10-14 09:13:18 +00:00
|
|
|
|
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
# Функция запуска сервера
|
|
|
|
|
async def start():
|
|
|
|
|
"""Запуск сервера и инициализация данных"""
|
2025-05-19 21:00:24 +00:00
|
|
|
|
# Инициализируем соединение с Redis
|
|
|
|
|
await redis.connect()
|
|
|
|
|
logger.info("Установлено соединение с Redis")
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
# Создаем все таблицы в БД
|
|
|
|
|
create_all_tables()
|
|
|
|
|
|
|
|
|
|
# Запускаем предварительное кеширование данных
|
|
|
|
|
asyncio.create_task(precache_data())
|
|
|
|
|
|
|
|
|
|
# Запускаем задачу ревалидации кеша
|
|
|
|
|
asyncio.create_task(revalidation_manager.start())
|
|
|
|
|
|
|
|
|
|
# Выводим сообщение о запуске сервера и доступности API
|
|
|
|
|
logger.info("Сервер запущен и готов принимать запросы")
|
|
|
|
|
logger.info("GraphQL API доступно по адресу: /graphql")
|
2025-05-19 21:00:24 +00:00
|
|
|
|
logger.info("Админ-панель доступна по адресу: http://127.0.0.1:8000/")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Функция остановки сервера
|
|
|
|
|
async def shutdown():
|
|
|
|
|
"""Остановка сервера и освобождение ресурсов"""
|
|
|
|
|
logger.info("Остановка сервера")
|
|
|
|
|
|
|
|
|
|
# Закрываем соединение с Redis
|
|
|
|
|
await redis.disconnect()
|
|
|
|
|
|
|
|
|
|
# Останавливаем поисковый сервис
|
|
|
|
|
search_service.close()
|
|
|
|
|
|
|
|
|
|
# Удаляем PID-файл, если он существует
|
2025-05-19 08:25:41 +00:00
|
|
|
|
from settings import DEV_SERVER_PID_FILE_NAME
|
2025-05-16 06:23:48 +00:00
|
|
|
|
if exists(DEV_SERVER_PID_FILE_NAME):
|
|
|
|
|
os.unlink(DEV_SERVER_PID_FILE_NAME)
|
|
|
|
|
|
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
# Создаем middleware с правильным порядком
|
|
|
|
|
middleware = [
|
|
|
|
|
# Начинаем с обработки ошибок
|
|
|
|
|
Middleware(ExceptionHandlerMiddleware),
|
|
|
|
|
# CORS должен быть перед другими middleware для корректной обработки preflight-запросов
|
|
|
|
|
Middleware(
|
|
|
|
|
CORSMiddleware,
|
2025-05-20 22:34:02 +00:00
|
|
|
|
allow_origins=[
|
|
|
|
|
"https://localhost:3000",
|
|
|
|
|
"https://testing.discours.io",
|
|
|
|
|
"https://discours.io",
|
|
|
|
|
"https://new.discours.io",
|
|
|
|
|
"https://discours.ru",
|
|
|
|
|
"https://new.discours.ru"
|
|
|
|
|
],
|
2025-05-19 08:25:41 +00:00
|
|
|
|
allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
),
|
|
|
|
|
# После CORS идёт обработка авторизации
|
|
|
|
|
Middleware(AuthMiddleware),
|
|
|
|
|
# И затем аутентификация
|
|
|
|
|
Middleware(AuthenticationMiddleware, backend=InternalAuthentication()),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2025-05-19 21:00:24 +00:00
|
|
|
|
# Создаем экземпляр GraphQL с улучшенным обработчиком
|
|
|
|
|
graphql_app = GraphQL(
|
|
|
|
|
schema,
|
|
|
|
|
debug=True,
|
|
|
|
|
http_handler=EnhancedGraphQLHTTPHandler()
|
|
|
|
|
)
|
2025-05-19 08:25:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
|
|
|
|
|
async def graphql_handler(request: Request):
|
|
|
|
|
if request.method not in ["GET", "POST", "OPTIONS"]:
|
|
|
|
|
return JSONResponse({"error": "Method Not Allowed by main.py"}, status_code=405)
|
|
|
|
|
|
|
|
|
|
try:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
# Обрабатываем 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"] = "*"
|
2025-05-20 22:34:02 +00:00
|
|
|
|
response.headers["Access-Control-Allow-Credentials"] = "true"
|
2025-05-19 21:00:24 +00:00
|
|
|
|
response.headers["Access-Control-Max-Age"] = "86400" # 24 hours
|
|
|
|
|
return response
|
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
result = await graphql_app.handle_request(request)
|
2025-05-19 21:00:24 +00:00
|
|
|
|
|
|
|
|
|
# Если результат не является 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
|
2025-05-19 08:25:41 +00:00
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
return JSONResponse({"error": "Request cancelled"}, status_code=499)
|
|
|
|
|
except Exception as e:
|
2025-05-19 21:00:24 +00:00
|
|
|
|
logger.error(f"GraphQL error: {str(e)}")
|
2025-05-19 08:25:41 +00:00
|
|
|
|
return JSONResponse({"error": str(e)}, status_code=500)
|
2025-05-16 07:30:02 +00:00
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
# Добавляем маршруты, порядок имеет значение
|
|
|
|
|
routes = [
|
|
|
|
|
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
|
|
|
|
|
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True)),
|
|
|
|
|
]
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
# Создаем приложение Starlette с маршрутами и middleware
|
2024-02-16 09:40:41 +00:00
|
|
|
|
app = Starlette(
|
2025-05-16 06:23:48 +00:00
|
|
|
|
routes=routes,
|
2025-05-19 08:25:41 +00:00
|
|
|
|
middleware=middleware,
|
2025-05-16 06:23:48 +00:00
|
|
|
|
on_startup=[start],
|
|
|
|
|
on_shutdown=[shutdown],
|
2024-04-08 06:17:05 +00:00
|
|
|
|
)
|