This commit is contained in:
2025-05-19 11:25:41 +03:00
parent 11e46f7352
commit dc5ad46df9
20 changed files with 952 additions and 509 deletions

210
main.py
View File

@@ -11,9 +11,10 @@ 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, HTMLResponse, RedirectResponse
from starlette.responses import FileResponse, 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
@@ -22,23 +23,17 @@ from services.redis import redis
from services.schema import create_all_tables, resolvers
from services.search import search_service
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 для авторизации
from auth.middleware import AuthMiddleware
# Импортируем резолверы
import_module("resolvers")
import_module("auth.resolvers")
# Создаем схему GraphQL
schema = make_executable_schema(load_schema_from_path("schema/"), resolvers)
# Пути к клиентским файлам
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")
@@ -50,121 +45,35 @@ async def index_handler(request: Request):
return FileResponse(INDEX_HTML)
# GraphQL API
class CustomGraphQLHTTPHandler(GraphQLHTTPHandler):
"""
Кастомный GraphQL HTTP обработчик, который добавляет объект response в контекст
"""
# Создаем единый экземпляр 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:
"""
Переопределяем метод для добавления объекта response и extensions в контекст
Расширяем контекст для GraphQL запросов
"""
# Получаем стандартный контекст от базового класса
context = await super().get_context_for_request(request, data)
# Создаем объект ответа, который будем использовать для установки cookie
# Добавляем объект ответа для установки cookie
response = JSONResponse({})
context["response"] = response
# Добавляем extensions в контекст
if "extensions" not in context:
context["extensions"] = GraphQLExtensionsMiddleware()
# Интегрируем с AuthMiddleware
context["extensions"] = auth_middleware
return context
graphql_app = GraphQL(schema, debug=MODE == "development", http_handler=CustomGraphQLHTTPHandler())
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)
return result
async def admin_handler(request: Request):
"""
Обработчик для маршрута /admin с серверной проверкой прав доступа
"""
# Проверяем авторизован ли пользователь
if not request.user.is_authenticated:
# Если пользователь не авторизован, перенаправляем на главную страницу
return RedirectResponse(url="/", 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()
@@ -192,47 +101,60 @@ async def shutdown():
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)
# Добавляем маршруты статических файлов, если директория существует
routes = []
if exists(DIST_DIR):
routes.append(Mount("/", app=StaticFiles(directory=DIST_DIR, html=True)))
# Создаем middleware с правильным порядком
middleware = [
# Начинаем с обработки ошибок
Middleware(ExceptionHandlerMiddleware),
# CORS должен быть перед другими middleware для корректной обработки preflight-запросов
Middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS
allow_headers=["*"],
allow_credentials=True,
),
# После CORS идёт обработка авторизации
Middleware(AuthMiddleware),
# И затем аутентификация
Middleware(AuthenticationMiddleware, backend=InternalAuthentication()),
]
# Создаем экземпляр GraphQL
graphql_app = GraphQL(schema, debug=True)
# Оборачиваем 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:
result = await graphql_app.handle_request(request)
if isinstance(result, Response):
return result
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)
# Маршруты для 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)
# Добавляем маршруты, порядок имеет значение
routes = [
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True)),
]
# Создаем приложение Starlette с маршрутами и middleware
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()),
],
middleware=middleware,
on_startup=[start],
on_shutdown=[shutdown],
)