2024-10-14 11:11:13 +03:00
|
|
|
|
import asyncio
|
2023-01-17 22:07:44 +01:00
|
|
|
|
import os
|
2025-07-31 18:55:59 +03:00
|
|
|
|
import traceback
|
2025-07-07 22:53:01 +03:00
|
|
|
|
from contextlib import asynccontextmanager
|
2022-09-03 13:50:14 +03:00
|
|
|
|
from importlib import import_module
|
2025-06-03 01:24:49 +03:00
|
|
|
|
from pathlib import Path
|
2023-12-17 23:30:20 +03:00
|
|
|
|
|
2024-02-19 11:58:02 +03:00
|
|
|
|
from ariadne import load_schema_from_path, make_executable_schema
|
2022-09-03 13:50:14 +03:00
|
|
|
|
from ariadne.asgi import GraphQL
|
2025-07-25 10:13:26 +03:00
|
|
|
|
from graphql import GraphQLError
|
2022-09-03 13:50:14 +03:00
|
|
|
|
from starlette.applications import Starlette
|
2025-05-16 09:23:48 +03:00
|
|
|
|
from starlette.middleware import Middleware
|
2025-05-29 12:37:39 +03:00
|
|
|
|
from starlette.middleware.cors import CORSMiddleware
|
2024-10-14 12:31:55 +03:00
|
|
|
|
from starlette.requests import Request
|
2025-06-30 21:46:53 +03:00
|
|
|
|
from starlette.responses import FileResponse, JSONResponse, Response
|
2025-05-29 12:37:39 +03:00
|
|
|
|
from starlette.routing import Mount, Route
|
2025-05-16 09:23:48 +03:00
|
|
|
|
from starlette.staticfiles import StaticFiles
|
2023-11-28 22:07:53 +03:00
|
|
|
|
|
2025-05-29 12:37:39 +03:00
|
|
|
|
from auth.handler import EnhancedGraphQLHTTPHandler
|
|
|
|
|
|
from auth.middleware import AuthMiddleware, auth_middleware
|
2025-09-23 17:14:47 +03:00
|
|
|
|
from auth.oauth import oauth_callback_http, oauth_login_http
|
2024-08-09 09:37:06 +03:00
|
|
|
|
from cache.precache import precache_data
|
|
|
|
|
|
from cache.revalidator import revalidation_manager
|
2025-08-17 17:56:31 +03:00
|
|
|
|
from rbac import initialize_rbac
|
2025-10-09 01:15:19 +03:00
|
|
|
|
from services.search import check_search_service, initialize_search_index, search_service
|
2025-05-22 04:34:30 +03:00
|
|
|
|
from services.viewed import ViewedStorage
|
|
|
|
|
|
from settings import DEV_SERVER_PID_FILE_NAME
|
[0.9.7] - 2025-08-18
### 🔄 Изменения
- **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации
- **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)`
### 🧪 Тестирование
- **Исправление тестов** - адаптация к новой структуре моделей
- **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py`
- **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев
- **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями
- **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода
### 🔧 Рефакторинг
- **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру
- **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль
- **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры
- **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей
- **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки
### 🔧 Авторизация с cookies
- **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization
- **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно
- **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token`
- **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession`
- **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author`
- **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами
- **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession`
- **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации
### 📝 Документация
- **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа
- Обновлена документация RBAC
- Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00
|
|
|
|
from storage.redis import redis
|
|
|
|
|
|
from storage.schema import create_all_tables, resolvers
|
|
|
|
|
|
from utils.exception import ExceptionHandlerMiddleware
|
2025-08-28 20:19:30 +03:00
|
|
|
|
from utils.logger import custom_error_formatter
|
2025-05-29 12:37:39 +03:00
|
|
|
|
from utils.logger import root_logger as logger
|
2025-08-28 20:19:30 +03:00
|
|
|
|
from utils.sentry import start_sentry
|
2025-05-22 04:34:30 +03:00
|
|
|
|
|
|
|
|
|
|
DEVMODE = os.getenv("DOKKU_APP_TYPE", "false").lower() == "false"
|
2025-06-03 01:24:49 +03:00
|
|
|
|
DIST_DIR = Path(__file__).parent / "dist" # Директория для собранных файлов
|
|
|
|
|
|
INDEX_HTML = Path(__file__).parent / "index.html"
|
2024-01-25 22:41:27 +03:00
|
|
|
|
|
2024-04-17 18:32:23 +03:00
|
|
|
|
import_module("resolvers")
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
2025-07-02 22:30:21 +03:00
|
|
|
|
schema = make_executable_schema(load_schema_from_path("schema/"), resolvers)
|
2024-02-19 11:58:02 +03:00
|
|
|
|
|
2025-05-19 11:25:41 +03:00
|
|
|
|
# Создаем middleware с правильным порядком
|
|
|
|
|
|
middleware = [
|
2025-06-28 13:56:05 +03:00
|
|
|
|
# Начинаем с обработки ошибок
|
|
|
|
|
|
Middleware(ExceptionHandlerMiddleware),
|
2025-05-19 11:25:41 +03:00
|
|
|
|
# CORS должен быть перед другими middleware для корректной обработки preflight-запросов
|
|
|
|
|
|
Middleware(
|
|
|
|
|
|
CORSMiddleware,
|
2025-05-21 01:34:02 +03:00
|
|
|
|
allow_origins=[
|
2025-05-29 12:37:39 +03:00
|
|
|
|
"https://testing.discours.io",
|
2025-05-29 18:26:10 +03:00
|
|
|
|
"https://testing3.discours.io",
|
2025-09-28 12:22:37 +03:00
|
|
|
|
"https://v3.discours.io",
|
2025-06-28 13:56:05 +03:00
|
|
|
|
"https://session-daily.vercel.app",
|
2025-06-03 01:48:23 +03:00
|
|
|
|
"https://coretest.discours.io",
|
2025-05-21 01:34:02 +03:00
|
|
|
|
"https://new.discours.io",
|
2025-07-31 18:55:59 +03:00
|
|
|
|
"https://localhost:3000",
|
2025-05-29 12:37:39 +03:00
|
|
|
|
],
|
|
|
|
|
|
allow_methods=["GET", "POST", "OPTIONS"], # Явно указываем OPTIONS
|
2025-05-19 11:25:41 +03:00
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
|
),
|
2025-06-28 13:56:05 +03:00
|
|
|
|
# Аутентификация должна быть после CORS
|
2025-05-19 11:25:41 +03:00
|
|
|
|
Middleware(AuthMiddleware),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2025-08-28 20:19:30 +03:00
|
|
|
|
# Создаем экземпляр GraphQL с улучшенным обработчиком и кастомным форматтером ошибок
|
|
|
|
|
|
graphql_app = GraphQL(
|
|
|
|
|
|
schema,
|
|
|
|
|
|
debug=DEVMODE,
|
|
|
|
|
|
http_handler=EnhancedGraphQLHTTPHandler(),
|
|
|
|
|
|
error_formatter=custom_error_formatter,
|
|
|
|
|
|
)
|
2025-05-19 11:25:41 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
|
2025-08-30 21:18:48 +03:00
|
|
|
|
|
2025-08-30 21:20:01 +03:00
|
|
|
|
|
2025-06-03 01:24:49 +03:00
|
|
|
|
async def graphql_handler(request: Request) -> Response:
|
2025-05-22 04:34:30 +03:00
|
|
|
|
"""
|
|
|
|
|
|
Обработчик GraphQL запросов с поддержкой middleware и обработкой ошибок.
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
Выполняет:
|
|
|
|
|
|
1. Проверку метода запроса (GET, POST, OPTIONS)
|
|
|
|
|
|
2. Обработку GraphQL запроса через ariadne
|
|
|
|
|
|
3. Применение middleware для корректной обработки cookie и авторизации
|
|
|
|
|
|
4. Обработку исключений и формирование ответа
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
Args:
|
|
|
|
|
|
request: Starlette Request объект
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
Returns:
|
|
|
|
|
|
Response: объект ответа (обычно JSONResponse)
|
|
|
|
|
|
"""
|
2025-05-19 11:25:41 +03:00
|
|
|
|
if request.method not in ["GET", "POST", "OPTIONS"]:
|
|
|
|
|
|
return JSONResponse({"error": "Method Not Allowed by main.py"}, status_code=405)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
# Проверяем, что все необходимые middleware корректно отработали
|
|
|
|
|
|
if not hasattr(request, "scope") or "auth" not in request.scope:
|
|
|
|
|
|
logger.warning("[graphql] AuthMiddleware не обработал запрос перед GraphQL обработчиком")
|
2025-05-19 11:25:41 +03:00
|
|
|
|
|
|
|
|
|
|
try:
|
2025-05-22 04:34:30 +03:00
|
|
|
|
# Обрабатываем запрос через GraphQL приложение
|
2025-05-19 11:25:41 +03:00
|
|
|
|
result = await graphql_app.handle_request(request)
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
# Применяем middleware для установки cookie
|
|
|
|
|
|
# Используем метод process_result из auth_middleware для корректной обработки
|
|
|
|
|
|
# cookie на основе результатов операций login/logout
|
2025-06-02 02:56:11 +03:00
|
|
|
|
return await auth_middleware.process_result(request, result)
|
2025-05-19 11:25:41 +03:00
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
|
return JSONResponse({"error": "Request cancelled"}, status_code=499)
|
2025-07-25 10:10:36 +03:00
|
|
|
|
except GraphQLError as e:
|
|
|
|
|
|
# Для GraphQL ошибок (например, неавторизованный доступ) не логируем полный трейс
|
|
|
|
|
|
logger.warning(f"GraphQL error: {e}")
|
|
|
|
|
|
return JSONResponse({"error": str(e)}, status_code=403)
|
2025-05-19 11:25:41 +03:00
|
|
|
|
except Exception as e:
|
2025-07-25 10:10:36 +03:00
|
|
|
|
logger.error(f"Unexpected GraphQL error: {e!s}")
|
|
|
|
|
|
logger.debug(f"Unexpected GraphQL error traceback: {traceback.format_exc()}")
|
|
|
|
|
|
return JSONResponse({"error": "Internal server error"}, status_code=500)
|
2025-05-22 04:34:30 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-06-30 21:46:53 +03:00
|
|
|
|
async def spa_handler(request: Request) -> Response:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Обработчик для SPA (Single Page Application) fallback.
|
|
|
|
|
|
|
|
|
|
|
|
Возвращает index.html для всех маршрутов, которые не найдены,
|
e2e-fixing
fix: убран health endpoint, E2E тест использует корневой маршрут
- Убран health endpoint из main.py (не нужен)
- E2E тест теперь проверяет корневой маршрут / вместо /health
- Корневой маршрут доступен без логина, что подходит для проверки состояния сервера
- E2E тест с браузером работает корректно
docs: обновлен отчет о прогрессе E2E теста
- Убраны упоминания health endpoint
- Указано что используется корневой маршрут для проверки серверов
- Обновлен список измененных файлов
fix: исправлены GraphQL проблемы и E2E тест с браузером
- Добавлено поле success в тип CommonResult для совместимости с фронтендом
- Обновлены резолверы community, collection, topic для возврата поля success
- Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint
- E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице
- Все GraphQL проблемы с полем success решены
- E2E тест работает правильно с браузером как требовалось
fix: исправлен поиск UI элементов в E2E тесте
- Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300
- Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×)
- Добавлен правильный поиск модального окна с множественными селекторами
- Добавлен правильный поиск кнопки подтверждения в модальном окне
- E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Обновлен отчет о прогрессе с полными результатами тестирования
fix: исправлен импорт require_any_permission в resolvers/collection.py
- Заменен импорт require_any_permission с auth.decorators на services.rbac
- Бэкенд сервер теперь запускается корректно
- E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Оба сервера (бэкенд и фронтенд) работают стабильно
fix: исправлен порядок импортов в resolvers/collection.py
- Перемещен импорт require_any_permission в правильное место
- E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности
feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
|
|
|
|
чтобы клиентский роутер (SolidJS) мог обработать маршрутизацию.
|
2025-06-30 21:46:53 +03:00
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
request: Starlette Request объект
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
FileResponse: ответ с содержимым index.html
|
|
|
|
|
|
"""
|
e2e-fixing
fix: убран health endpoint, E2E тест использует корневой маршрут
- Убран health endpoint из main.py (не нужен)
- E2E тест теперь проверяет корневой маршрут / вместо /health
- Корневой маршрут доступен без логина, что подходит для проверки состояния сервера
- E2E тест с браузером работает корректно
docs: обновлен отчет о прогрессе E2E теста
- Убраны упоминания health endpoint
- Указано что используется корневой маршрут для проверки серверов
- Обновлен список измененных файлов
fix: исправлены GraphQL проблемы и E2E тест с браузером
- Добавлено поле success в тип CommonResult для совместимости с фронтендом
- Обновлены резолверы community, collection, topic для возврата поля success
- Исправлен E2E тест для работы с корневым маршрутом вместо health endpoint
- E2E тест теперь запускает браузер, авторизуется, находит сообщество в таблице
- Все GraphQL проблемы с полем success решены
- E2E тест работает правильно с браузером как требовалось
fix: исправлен поиск UI элементов в E2E тесте
- Добавлен правильный поиск кнопки удаления по CSS классу _delete-button_1qlfg_300
- Добавлены альтернативные способы поиска кнопки удаления (title, aria-label, символ ×)
- Добавлен правильный поиск модального окна с множественными селекторами
- Добавлен правильный поиск кнопки подтверждения в модальном окне
- E2E тест теперь полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Обновлен отчет о прогрессе с полными результатами тестирования
fix: исправлен импорт require_any_permission в resolvers/collection.py
- Заменен импорт require_any_permission с auth.decorators на services.rbac
- Бэкенд сервер теперь запускается корректно
- E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Оба сервера (бэкенд и фронтенд) работают стабильно
fix: исправлен порядок импортов в resolvers/collection.py
- Перемещен импорт require_any_permission в правильное место
- E2E тест полностью работает: находит кнопку удаления, модальное окно и кнопку подтверждения
- Сообщество не удаляется из-за прав доступа - это нормальное поведение системы безопасности
feat: настроен HTTPS для локальной разработки с mkcert
2025-08-01 00:30:44 +03:00
|
|
|
|
# Исключаем API маршруты из SPA fallback
|
|
|
|
|
|
path = request.url.path
|
|
|
|
|
|
if path.startswith(("/graphql", "/oauth", "/assets")):
|
|
|
|
|
|
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
|
|
|
|
|
2025-06-30 21:46:53 +03:00
|
|
|
|
index_path = DIST_DIR / "index.html"
|
|
|
|
|
|
if index_path.exists():
|
|
|
|
|
|
return FileResponse(index_path, media_type="text/html")
|
|
|
|
|
|
return JSONResponse({"error": "Admin panel not built"}, status_code=404)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-21 14:23:53 +03:00
|
|
|
|
async def health_handler(request: Request) -> Response:
|
|
|
|
|
|
"""Health check endpoint with Redis monitoring"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
redis_info = await redis.get_info()
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
{"status": "healthy", "redis": {"connected": redis.is_connected, "ping": await redis.ping(), **redis_info}}
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Health check failed: {e}")
|
|
|
|
|
|
return JSONResponse({"status": "unhealthy", "error": str(e)}, status_code=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
async def shutdown() -> None:
|
2025-05-22 04:34:30 +03:00
|
|
|
|
"""Остановка сервера и освобождение ресурсов"""
|
|
|
|
|
|
logger.info("Остановка сервера")
|
|
|
|
|
|
|
|
|
|
|
|
# Закрываем соединение с Redis
|
|
|
|
|
|
await redis.disconnect()
|
|
|
|
|
|
|
|
|
|
|
|
# Останавливаем поисковый сервис
|
2025-06-16 20:20:23 +03:00
|
|
|
|
await search_service.close()
|
2025-05-22 04:34:30 +03:00
|
|
|
|
|
2025-06-03 01:24:49 +03:00
|
|
|
|
pid_file = Path(DEV_SERVER_PID_FILE_NAME)
|
|
|
|
|
|
if pid_file.exists():
|
|
|
|
|
|
pid_file.unlink()
|
2025-05-22 04:34:30 +03:00
|
|
|
|
|
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
|
async def dev_start() -> None:
|
2025-05-22 04:34:30 +03:00
|
|
|
|
"""
|
|
|
|
|
|
Инициализация сервера в DEV режиме.
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
Функция:
|
|
|
|
|
|
1. Проверяет наличие DEV режима
|
|
|
|
|
|
2. Создает PID-файл для отслеживания процесса
|
|
|
|
|
|
3. Логирует информацию о старте сервера
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
Используется только при запуске сервера с флагом "dev".
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-06-03 01:24:49 +03:00
|
|
|
|
pid_path = Path(DEV_SERVER_PID_FILE_NAME)
|
2025-05-22 04:34:30 +03:00
|
|
|
|
# Если PID-файл уже существует, проверяем, не запущен ли уже сервер с этим PID
|
2025-06-03 01:24:49 +03:00
|
|
|
|
if pid_path.exists():
|
2025-05-22 04:34:30 +03:00
|
|
|
|
try:
|
2025-06-03 01:24:49 +03:00
|
|
|
|
with pid_path.open(encoding="utf-8") as f:
|
2025-05-22 04:34:30 +03:00
|
|
|
|
old_pid = int(f.read().strip())
|
|
|
|
|
|
# Проверяем, существует ли процесс с таким PID
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
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):
|
2025-06-02 02:56:11 +03:00
|
|
|
|
print("[warning] Invalid PID file found, recreating")
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
# Создаем или перезаписываем PID-файл
|
2025-06-03 01:24:49 +03:00
|
|
|
|
with pid_path.open("w", encoding="utf-8") as f:
|
2025-05-22 04:34:30 +03:00
|
|
|
|
f.write(str(os.getpid()))
|
|
|
|
|
|
print(f"[main] process started in DEV mode with PID {os.getpid()}")
|
|
|
|
|
|
except Exception as e:
|
2025-06-02 02:56:11 +03:00
|
|
|
|
logger.error(f"[main] Error during server startup: {e!s}")
|
2025-05-22 04:34:30 +03:00
|
|
|
|
# Не прерываем запуск сервера из-за ошибки в этой функции
|
2025-06-02 02:56:11 +03:00
|
|
|
|
print(f"[warning] Error during DEV mode initialization: {e!s}")
|
2025-05-22 04:34:30 +03:00
|
|
|
|
|
2025-05-16 09:23:48 +03:00
|
|
|
|
|
2025-08-30 18:53:38 +03:00
|
|
|
|
async def initialize_search_index_with_data() -> None:
|
|
|
|
|
|
"""Инициализация поискового индекса данными из БД"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from orm.shout import Shout
|
|
|
|
|
|
from storage.db import local_session
|
|
|
|
|
|
|
|
|
|
|
|
# Получаем все опубликованные шауты из БД
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
|
shouts = session.query(Shout).filter(Shout.published_at.is_not(None)).all()
|
|
|
|
|
|
|
|
|
|
|
|
if shouts:
|
|
|
|
|
|
await initialize_search_index(shouts)
|
|
|
|
|
|
print(f"[search] Loaded {len(shouts)} published shouts into search index")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("[search] No published shouts found to index")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Failed to initialize search index with data: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-06-03 01:24:49 +03:00
|
|
|
|
# Глобальная переменная для background tasks
|
2025-08-23 10:47:52 +03:00
|
|
|
|
background_tasks: list[asyncio.Task] = []
|
2022-09-03 13:50:14 +03:00
|
|
|
|
|
2025-06-03 01:24:49 +03:00
|
|
|
|
|
2025-07-07 22:53:01 +03:00
|
|
|
|
@asynccontextmanager
|
2025-06-16 20:20:23 +03:00
|
|
|
|
async def lifespan(app: Starlette):
|
2025-05-22 04:34:30 +03:00
|
|
|
|
"""
|
|
|
|
|
|
Функция жизненного цикла приложения.
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
Обеспечивает:
|
|
|
|
|
|
1. Инициализацию всех необходимых сервисов и компонентов
|
|
|
|
|
|
2. Предзагрузку кеша данных
|
|
|
|
|
|
3. Подключение к Redis и поисковому сервису
|
|
|
|
|
|
4. Корректное завершение работы при остановке сервера
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
Args:
|
2025-06-16 20:20:23 +03:00
|
|
|
|
app: экземпляр Starlette приложения
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
Yields:
|
|
|
|
|
|
None: генератор для управления жизненным циклом
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
print("[lifespan] Starting application initialization")
|
|
|
|
|
|
create_all_tables()
|
2025-08-17 17:56:31 +03:00
|
|
|
|
|
2025-08-17 16:33:54 +03:00
|
|
|
|
# Инициализируем RBAC систему с dependency injection
|
|
|
|
|
|
initialize_rbac()
|
2025-08-17 17:56:31 +03:00
|
|
|
|
|
2025-08-28 20:19:30 +03:00
|
|
|
|
# Инициализируем Sentry для мониторинга ошибок
|
|
|
|
|
|
start_sentry()
|
|
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
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")
|
|
|
|
|
|
|
2025-08-30 18:53:38 +03:00
|
|
|
|
# Инициализируем поисковый индекс данными из БД
|
|
|
|
|
|
print("[lifespan] Initializing search index with existing data...")
|
|
|
|
|
|
await initialize_search_index_with_data()
|
2025-08-23 10:47:52 +03:00
|
|
|
|
print("[lifespan] Search service initialized with Muvera")
|
2025-05-22 04:34:30 +03:00
|
|
|
|
|
2025-10-09 01:15:19 +03:00
|
|
|
|
# NOTE: Предзагрузка моделей убрана - ColBERT загружается lazy при первом поиске
|
|
|
|
|
|
# BiEncoder модели больше не используются (default=colbert)
|
2025-09-01 16:38:23 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
yield
|
|
|
|
|
|
finally:
|
|
|
|
|
|
print("[lifespan] Shutting down application services")
|
2025-03-24 21:42:51 -03:00
|
|
|
|
|
2025-06-03 01:24:49 +03:00
|
|
|
|
# Отменяем все background tasks
|
|
|
|
|
|
for task in background_tasks:
|
|
|
|
|
|
if not task.done():
|
|
|
|
|
|
task.cancel()
|
2025-03-24 21:42:51 -03:00
|
|
|
|
|
2025-06-03 01:24:49 +03:00
|
|
|
|
# Ждем завершения отмены tasks
|
|
|
|
|
|
if background_tasks:
|
|
|
|
|
|
await asyncio.gather(*background_tasks, return_exceptions=True)
|
2024-10-14 12:13:18 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
tasks = [redis.disconnect(), ViewedStorage.stop(), revalidation_manager.stop()]
|
|
|
|
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
|
|
print("[lifespan] Shutdown complete")
|
|
|
|
|
|
|
2025-05-29 12:37:39 +03:00
|
|
|
|
|
2025-05-22 04:34:30 +03:00
|
|
|
|
# Обновляем маршрут в Starlette
|
2024-02-16 12:40:41 +03:00
|
|
|
|
app = Starlette(
|
2025-05-22 04:34:30 +03:00
|
|
|
|
routes=[
|
|
|
|
|
|
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
|
2025-09-24 13:35:49 +03:00
|
|
|
|
# OAuth маршруты - порядок важен! Более специфичные маршруты должны быть первыми
|
2025-09-23 17:14:47 +03:00
|
|
|
|
Route("/oauth/{provider}/callback", oauth_callback_http, methods=["GET"]),
|
2025-09-24 13:35:49 +03:00
|
|
|
|
Route(
|
|
|
|
|
|
"/oauth/{provider}/{redirect_uri:path}", oauth_login_http, methods=["GET"]
|
|
|
|
|
|
), # Поддержка старого формата фронтенда
|
|
|
|
|
|
Route("/oauth/{provider}", oauth_login_http, methods=["GET"]),
|
2025-09-21 14:23:53 +03:00
|
|
|
|
# Health check endpoint
|
|
|
|
|
|
Route("/health", health_handler, methods=["GET"]),
|
2025-06-30 21:46:53 +03:00
|
|
|
|
# Статические файлы (CSS, JS, изображения)
|
|
|
|
|
|
Mount("/assets", app=StaticFiles(directory=str(DIST_DIR / "assets"))),
|
|
|
|
|
|
# Корневой маршрут для админ-панели
|
|
|
|
|
|
Route("/", spa_handler, methods=["GET"]),
|
|
|
|
|
|
# SPA fallback для всех остальных маршрутов
|
|
|
|
|
|
Route("/{path:path}", spa_handler, methods=["GET"]),
|
2025-05-22 04:34:30 +03:00
|
|
|
|
],
|
2025-06-28 13:56:05 +03:00
|
|
|
|
middleware=middleware, # Используем единый список middleware
|
2025-05-22 04:34:30 +03:00
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
|
debug=True,
|
2024-04-08 09:17:05 +03:00
|
|
|
|
)
|
2025-05-22 04:34:30 +03:00
|
|
|
|
|
|
|
|
|
|
if DEVMODE:
|
|
|
|
|
|
# Для DEV режима регистрируем дополнительный CORS middleware только для localhost
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
|
CORSMiddleware,
|
2025-07-07 22:53:01 +03:00
|
|
|
|
allow_origins=[
|
|
|
|
|
|
"https://localhost:3000",
|
|
|
|
|
|
"https://localhost:3001",
|
|
|
|
|
|
"https://localhost:3002",
|
|
|
|
|
|
"http://localhost:3000",
|
|
|
|
|
|
"http://localhost:3001",
|
|
|
|
|
|
"http://localhost:3002",
|
|
|
|
|
|
],
|
2025-05-22 04:34:30 +03:00
|
|
|
|
allow_credentials=True,
|
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
|
)
|