upgrade schema, resolvers, panel added
This commit is contained in:
271
main.py
271
main.py
@@ -1,16 +1,19 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from os.path import exists
|
||||
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 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 JSONResponse, Response
|
||||
from starlette.routing import Route
|
||||
from starlette.responses import FileResponse, JSONResponse, HTMLResponse, RedirectResponse
|
||||
from starlette.routing import Route, Mount
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
from cache.precache import precache_data
|
||||
from cache.revalidator import revalidation_manager
|
||||
@@ -18,78 +21,220 @@ from services.exception import ExceptionHandlerMiddleware
|
||||
from services.redis import redis
|
||||
from services.schema import create_all_tables, resolvers
|
||||
from services.search import search_service
|
||||
from services.viewed import ViewedStorage
|
||||
from services.webhook import WebhookEndpoint, create_webhook_endpoint
|
||||
from settings import DEV_SERVER_PID_FILE_NAME, MODE
|
||||
|
||||
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 для авторизации
|
||||
|
||||
import_module("resolvers")
|
||||
import_module("auth.resolvers")
|
||||
|
||||
# Создаем схему GraphQL
|
||||
schema = make_executable_schema(load_schema_from_path("schema/"), resolvers)
|
||||
|
||||
|
||||
async def start():
|
||||
if MODE == "development":
|
||||
if not exists(DEV_SERVER_PID_FILE_NAME):
|
||||
# pid file management
|
||||
with open(DEV_SERVER_PID_FILE_NAME, "w", encoding="utf-8") as f:
|
||||
f.write(str(os.getpid()))
|
||||
print(f"[main] process started in {MODE} mode")
|
||||
# Пути к клиентским файлам
|
||||
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")
|
||||
|
||||
|
||||
async def lifespan(_app):
|
||||
try:
|
||||
create_all_tables()
|
||||
await asyncio.gather(
|
||||
redis.connect(),
|
||||
precache_data(),
|
||||
ViewedStorage.init(),
|
||||
create_webhook_endpoint(),
|
||||
search_service.info(),
|
||||
start(),
|
||||
revalidation_manager.start(),
|
||||
)
|
||||
yield
|
||||
finally:
|
||||
tasks = [redis.disconnect(), ViewedStorage.stop(), revalidation_manager.stop()]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
async def index_handler(request: Request):
|
||||
"""
|
||||
Раздача основного HTML файла
|
||||
"""
|
||||
return FileResponse(INDEX_HTML)
|
||||
|
||||
|
||||
# Создаем экземпляр GraphQL
|
||||
graphql_app = GraphQL(schema, debug=True)
|
||||
# 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
|
||||
|
||||
|
||||
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
|
||||
async def graphql_handler(request: Request):
|
||||
if request.method not in ["GET", "POST"]:
|
||||
return JSONResponse({"error": "Method Not Allowed"}, status_code=405)
|
||||
graphql_app = GraphQL(schema, debug=MODE == "development", http_handler=CustomGraphQLHTTPHandler())
|
||||
|
||||
try:
|
||||
result = await graphql_app.handle_request(request)
|
||||
if isinstance(result, Response):
|
||||
return result
|
||||
|
||||
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):
|
||||
return JSONResponse(result)
|
||||
except asyncio.CancelledError:
|
||||
return JSONResponse({"error": "Request cancelled"}, status_code=499)
|
||||
except Exception as e:
|
||||
print(f"GraphQL error: {str(e)}")
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Обновляем маршрут в Starlette
|
||||
app = Starlette(
|
||||
routes=[
|
||||
Route("/", graphql_handler, methods=["GET", "POST"]),
|
||||
Route("/new-author", WebhookEndpoint),
|
||||
],
|
||||
lifespan=lifespan,
|
||||
debug=True,
|
||||
async def admin_handler(request: Request):
|
||||
"""
|
||||
Обработчик для маршрута /admin с серверной проверкой прав доступа
|
||||
"""
|
||||
# Проверяем авторизован ли пользователь
|
||||
if not request.user.is_authenticated:
|
||||
# Если пользователь не авторизован, перенаправляем на страницу входа
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
|
||||
# Проверяем является ли пользователь администратором
|
||||
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):
|
||||
# Добавляем маршруты для статических ресурсов, если директория dist существует
|
||||
routes.append(Mount("/assets", app=StaticFiles(directory=join(DIST_DIR, "assets"))))
|
||||
routes.append(Mount("/chunks", app=StaticFiles(directory=join(DIST_DIR, "chunks"))))
|
||||
|
||||
# Маршруты для 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"]),
|
||||
]
|
||||
)
|
||||
|
||||
app.add_middleware(ExceptionHandlerMiddleware)
|
||||
if "dev" in sys.argv:
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["https://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
# Добавляем маршруты авторизации
|
||||
routes.extend(auth_routes)
|
||||
|
||||
app = Starlette(
|
||||
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()),
|
||||
],
|
||||
on_startup=[start],
|
||||
on_shutdown=[shutdown],
|
||||
)
|
||||
|
Reference in New Issue
Block a user