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-16 06:23:48 +00:00
|
|
|
|
from starlette.responses import FileResponse, JSONResponse, HTMLResponse, RedirectResponse
|
|
|
|
|
from starlette.routing import Route, Mount
|
|
|
|
|
from starlette.staticfiles import StaticFiles
|
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 settings import DEV_SERVER_PID_FILE_NAME, MODE, ADMIN_EMAILS
|
|
|
|
|
from utils.logger import root_logger as logger
|
|
|
|
|
from auth.internal import InternalAuthentication
|
|
|
|
|
from auth import routes as auth_routes # Импортируем маршруты авторизации
|
|
|
|
|
from auth.middleware import (
|
|
|
|
|
AuthorizationMiddleware,
|
|
|
|
|
GraphQLExtensionsMiddleware,
|
|
|
|
|
) # Импортируем middleware для авторизации
|
2024-01-25 19:41:27 +00:00
|
|
|
|
|
2024-04-17 15:32:23 +00:00
|
|
|
|
import_module("resolvers")
|
2025-05-16 06:23:48 +00:00
|
|
|
|
import_module("auth.resolvers")
|
|
|
|
|
|
|
|
|
|
# Создаем схему 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
|
|
|
|
# Пути к клиентским файлам
|
|
|
|
|
CLIENT_DIR = join(os.path.dirname(__file__), "client")
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# GraphQL API
|
|
|
|
|
class CustomGraphQLHTTPHandler(GraphQLHTTPHandler):
|
|
|
|
|
"""
|
|
|
|
|
Кастомный GraphQL HTTP обработчик, который добавляет объект response в контекст
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
async def get_context_for_request(self, request: Request, data: dict) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
Переопределяем метод для добавления объекта response и extensions в контекст
|
|
|
|
|
"""
|
|
|
|
|
context = await super().get_context_for_request(request, data)
|
|
|
|
|
# Создаем объект ответа, который будем использовать для установки cookie
|
|
|
|
|
response = JSONResponse({})
|
|
|
|
|
context["response"] = response
|
|
|
|
|
|
|
|
|
|
# Добавляем extensions в контекст
|
|
|
|
|
if "extensions" not in context:
|
|
|
|
|
context["extensions"] = GraphQLExtensionsMiddleware()
|
|
|
|
|
|
|
|
|
|
return context
|
2024-10-14 09:13:18 +00:00
|
|
|
|
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
graphql_app = GraphQL(schema, debug=MODE == "development", http_handler=CustomGraphQLHTTPHandler())
|
2024-10-14 09:19:30 +00:00
|
|
|
|
|
2024-10-14 09:31:55 +00:00
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
async def graphql_handler(request):
|
|
|
|
|
"""Обработчик GraphQL запросов"""
|
|
|
|
|
# Проверяем заголовок Content-Type
|
|
|
|
|
content_type = request.headers.get("content-type", "")
|
|
|
|
|
if not content_type.startswith("application/json") and "application/json" in request.headers.get(
|
|
|
|
|
"accept", ""
|
|
|
|
|
):
|
|
|
|
|
# Если не application/json, но клиент принимает JSON
|
|
|
|
|
request._headers["content-type"] = "application/json"
|
|
|
|
|
|
|
|
|
|
# Обрабатываем GraphQL запрос
|
|
|
|
|
result = await graphql_app.handle_request(request)
|
|
|
|
|
|
|
|
|
|
# Если result - это ответ от сервера, возвращаем его как есть
|
|
|
|
|
if hasattr(result, "body"):
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
# Если результат - это словарь, значит нужно его сконвертировать в JSONResponse
|
|
|
|
|
if isinstance(result, dict):
|
2024-10-14 09:31:55 +00:00
|
|
|
|
return JSONResponse(result)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
return result
|
2024-10-14 08:11:13 +00:00
|
|
|
|
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
async def admin_handler(request: Request):
|
|
|
|
|
"""
|
|
|
|
|
Обработчик для маршрута /admin с серверной проверкой прав доступа
|
|
|
|
|
"""
|
|
|
|
|
# Проверяем авторизован ли пользователь
|
|
|
|
|
if not request.user.is_authenticated:
|
2025-05-16 07:30:02 +00:00
|
|
|
|
# Если пользователь не авторизован, перенаправляем на главную страницу
|
|
|
|
|
return RedirectResponse(url="/", status_code=303)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
# Проверяем является ли пользователь администратором
|
|
|
|
|
auth = getattr(request, "auth", None)
|
|
|
|
|
is_admin = False
|
|
|
|
|
|
|
|
|
|
# Проверяем наличие объекта auth и метода is_admin
|
|
|
|
|
if auth:
|
|
|
|
|
try:
|
|
|
|
|
# Проверяем имеет ли пользователь права администратора
|
|
|
|
|
is_admin = auth.is_admin
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка при проверке прав администратора: {e}")
|
|
|
|
|
|
|
|
|
|
# Дополнительная проверка email (для случаев, когда нет метода is_admin)
|
|
|
|
|
admin_emails = ADMIN_EMAILS.split(",")
|
|
|
|
|
if not is_admin and hasattr(auth, "email") and auth.email in admin_emails:
|
|
|
|
|
is_admin = True
|
|
|
|
|
|
|
|
|
|
if is_admin:
|
|
|
|
|
# Если пользователь - администратор, возвращаем HTML-файл
|
|
|
|
|
return FileResponse(INDEX_HTML)
|
|
|
|
|
else:
|
|
|
|
|
# Для авторизованных пользователей без прав администратора показываем страницу с ошибкой доступа
|
|
|
|
|
return HTMLResponse(
|
|
|
|
|
"""
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Доступ запрещен</title>
|
|
|
|
|
<style>
|
|
|
|
|
body { font-family: Arial, sans-serif; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f5f5f5; }
|
|
|
|
|
.error-container { max-width: 500px; padding: 30px; background-color: #fff; border-radius: 5px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); text-align: center; }
|
|
|
|
|
h1 { color: #e74c3c; margin-bottom: 20px; }
|
|
|
|
|
p { color: #333; margin-bottom: 20px; line-height: 1.5; }
|
|
|
|
|
.back-button { background-color: #3498db; color: #fff; border: none; padding: 10px 20px; border-radius: 3px; cursor: pointer; text-decoration: none; display: inline-block; }
|
|
|
|
|
.back-button:hover { background-color: #2980b9; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="error-container">
|
|
|
|
|
<h1>Доступ запрещен</h1>
|
|
|
|
|
<p>У вас нет прав для доступа к административной панели. Обратитесь к администратору системы для получения необходимых разрешений.</p>
|
|
|
|
|
<a href="/" class="back-button">Вернуться на главную</a>
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
""",
|
|
|
|
|
status_code=403
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Функция запуска сервера
|
|
|
|
|
async def start():
|
|
|
|
|
"""Запуск сервера и инициализация данных"""
|
|
|
|
|
logger.info(f"Запуск сервера в режиме: {MODE}")
|
|
|
|
|
|
|
|
|
|
# Создаем все таблицы в БД
|
|
|
|
|
create_all_tables()
|
|
|
|
|
|
|
|
|
|
# Запускаем предварительное кеширование данных
|
|
|
|
|
asyncio.create_task(precache_data())
|
|
|
|
|
|
|
|
|
|
# Запускаем задачу ревалидации кеша
|
|
|
|
|
asyncio.create_task(revalidation_manager.start())
|
|
|
|
|
|
|
|
|
|
# Выводим сообщение о запуске сервера и доступности API
|
|
|
|
|
logger.info("Сервер запущен и готов принимать запросы")
|
|
|
|
|
logger.info("GraphQL API доступно по адресу: /graphql")
|
|
|
|
|
logger.info("Админ-панель доступна по адресу: /admin")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Функция остановки сервера
|
|
|
|
|
async def shutdown():
|
|
|
|
|
"""Остановка сервера и освобождение ресурсов"""
|
|
|
|
|
logger.info("Остановка сервера")
|
|
|
|
|
|
|
|
|
|
# Закрываем соединение с Redis
|
|
|
|
|
await redis.disconnect()
|
|
|
|
|
|
|
|
|
|
# Останавливаем поисковый сервис
|
|
|
|
|
search_service.close()
|
|
|
|
|
|
|
|
|
|
# Удаляем PID-файл, если он существует
|
|
|
|
|
if exists(DEV_SERVER_PID_FILE_NAME):
|
|
|
|
|
os.unlink(DEV_SERVER_PID_FILE_NAME)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем маршруты статических файлов, если директория существует
|
|
|
|
|
routes = []
|
|
|
|
|
if exists(DIST_DIR):
|
2025-05-16 07:30:02 +00:00
|
|
|
|
routes.append(Mount("/", app=StaticFiles(directory=DIST_DIR, html=True)))
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
# Маршруты для API и веб-приложения
|
|
|
|
|
routes.extend(
|
|
|
|
|
[
|
|
|
|
|
Route("/graphql", graphql_handler, methods=["GET", "POST"]),
|
|
|
|
|
# Добавляем специальный маршрут для админ-панели с проверкой прав доступа
|
|
|
|
|
Route("/admin", admin_handler, methods=["GET"]),
|
|
|
|
|
# Маршрут для обработки всех остальных запросов - SPA
|
|
|
|
|
Route("/{path:path}", index_handler, methods=["GET"]),
|
|
|
|
|
Route("/", index_handler, methods=["GET"]),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Добавляем маршруты авторизации
|
|
|
|
|
routes.extend(auth_routes)
|
|
|
|
|
|
2024-02-16 09:40:41 +00:00
|
|
|
|
app = Starlette(
|
2025-05-16 06:23:48 +00:00
|
|
|
|
debug=MODE == "development",
|
|
|
|
|
routes=routes,
|
|
|
|
|
middleware=[
|
|
|
|
|
Middleware(ExceptionHandlerMiddleware),
|
|
|
|
|
Middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=["*"],
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
),
|
|
|
|
|
# Добавляем middleware для обработки Authorization заголовка с Bearer токеном
|
|
|
|
|
Middleware(AuthorizationMiddleware),
|
|
|
|
|
# Добавляем middleware для аутентификации после обработки токенов
|
|
|
|
|
Middleware(AuthenticationMiddleware, backend=InternalAuthentication()),
|
2024-02-16 09:40:41 +00:00
|
|
|
|
],
|
2025-05-16 06:23:48 +00:00
|
|
|
|
on_startup=[start],
|
|
|
|
|
on_shutdown=[shutdown],
|
2024-04-08 06:17:05 +00:00
|
|
|
|
)
|