diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c324f98..29a796aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,7 +62,6 @@ repos: # additional_dependencies: [ # "types-redis", # "types-requests", - # "types-passlib", # "types-Authlib", # "sqlalchemy[mypy]" # ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 376ac6fa..ac294294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [0.7.9] - 2025-07-24 + +### 🔐 Улучшения системы ролей и авторизации + +#### Исправления в управлении ролями +- **Корректная работа CommunityAuthor**: Исправлена логика сохранения и получения ролей пользователей +- **Автоматическое назначение ролей**: При создании пользователя теперь гарантированно назначаются роли `reader` и `author` +- **Нормализация email**: Email приводится к нижнему регистру при создании и обновлении пользователя +- **Обработка уникальности email**: Предотвращено создание дублей пользователей с одинаковым email + + +### 🔧 Улучшения тестирования +- **Инициализация сообщества**: Добавлена инициализация прав сообщества в фикстуре +- **Область видимости**: Изменена область видимости фикстуры на function для изоляции тестов +- **Настройки ролей**: Расширен список доступных ролей +- **Расширенные тесты RBAC**: Добавлены comprehensive тесты для проверки ролей и создания пользователей +- **Улучшенная диагностика**: Расширено логирование для облегчения отладки + +#### Оптимизации +- **Производительность**: Оптимизированы запросы к базе данных при работе с ролями +- **Безопасность**: Усилена проверка целостности данных при создании и обновлении пользователей + +### 🛠 Технические улучшения +- Рефакторинг методов `create_user()` и `update_user()` +- Исправлены потенциальные утечки данных +- Улучшена обработка краевых случаев в системе авторизации + ## [0.7.8] - 2025-07-04 ### 💬 Система управления реакциями в админ-панели @@ -1801,3 +1828,24 @@ Radical architecture simplification with separation into service layer and thin - `settings` moved to base and now smaller - new outside auth schema - removed `gittask`, `auth`, `inbox`, `migration` + +## [Unreleased] + +### Migration +- Подготовка к миграции на SQLAlchemy 2.0 +- Обновлена базовая модель для совместимости с новой версией ORM +- Улучшена типизация и обработка метаданных моделей +- Добавлена поддержка `DeclarativeBase` + +### Improvements +- Более надежное преобразование типов в ORM моделях +- Расширена функциональность базового класса моделей +- Улучшена обработка JSON-полей при сериализации + +### Fixed +- Исправлены потенциальные проблемы с типизацией в ORM +- Оптимизирована работа с метаданными SQLAlchemy + +### Changed +- Обновлен подход к работе с ORM-моделями +- Рефакторинг базового класса моделей для соответствия современным практикам SQLAlchemy diff --git a/alembic/env.py b/alembic/env.py index b9c6840c..bc3cc8eb 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -4,7 +4,7 @@ from sqlalchemy import engine_from_config, pool # Импорт всех моделей для корректной генерации миграций from alembic import context -from services.db import Base +from orm.base import BaseModel as Base from settings import DB_URL # this is the Alembic Config object, which provides diff --git a/auth/identity.py b/auth/identity.py index 2975caee..edc8f6a6 100644 --- a/auth/identity.py +++ b/auth/identity.py @@ -2,7 +2,7 @@ from binascii import hexlify from hashlib import sha256 from typing import TYPE_CHECKING, Any, TypeVar -from passlib.hash import bcrypt +import bcrypt from auth.exceptions import ExpiredToken, InvalidPassword, InvalidToken from auth.jwtcodec import JWTCodec @@ -39,7 +39,8 @@ class Password: str: Закодированный пароль """ password_sha256 = Password._get_sha256(password) - return bcrypt.using(rounds=10).hash(password_sha256) + salt = bcrypt.gensalt(rounds=10) + return bcrypt.hashpw(password_sha256, salt).decode("utf-8") @staticmethod def verify(password: str, hashed: str) -> bool: @@ -61,7 +62,7 @@ class Password: hashed_bytes = Password._to_bytes(hashed) password_sha256 = Password._get_sha256(password) - return bcrypt.verify(password_sha256, hashed_bytes) + return bcrypt.checkpw(password_sha256, hashed_bytes) # Изменил verify на checkpw class Identity: diff --git a/auth/oauth.py b/auth/oauth.py index ffb100e4..088f9f99 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -586,22 +586,7 @@ def _create_new_oauth_user(provider: str, profile: dict, email: str, session: An # Получаем сообщество для назначения дефолтных ролей community = session.query(Community).filter(Community.id == target_community_id).first() if community: - # Инициализируем права сообщества если нужно - try: - import asyncio - - loop = asyncio.get_event_loop() - loop.run_until_complete(community.initialize_role_permissions()) - except Exception as e: - logger.warning(f"Не удалось инициализировать права сообщества {target_community_id}: {e}") - - # Получаем дефолтные роли сообщества или используем стандартные - try: - default_roles = community.get_default_roles() - if not default_roles: - default_roles = ["reader", "author"] - except AttributeError: - default_roles = ["reader", "author"] + default_roles = community.get_default_roles() # Создаем CommunityAuthor с дефолтными ролями community_author = CommunityAuthor( diff --git a/orm/base.py b/orm/base.py new file mode 100644 index 00000000..6be334c8 --- /dev/null +++ b/orm/base.py @@ -0,0 +1,100 @@ +import builtins +import logging +from typing import Any, Callable, ClassVar, Type, Union + +import orjson +from sqlalchemy import JSON, Column, Integer +from sqlalchemy.orm import declarative_base, declared_attr + +logger = logging.getLogger(__name__) + +# Глобальный реестр моделей +REGISTRY: dict[str, Type[Any]] = {} + +# Список полей для фильтрации при сериализации +FILTERED_FIELDS: list[str] = [] + +# Создаем базовый класс для декларативных моделей +_Base = declarative_base() + + +class SafeColumnMixin: + """ + Миксин для безопасного присваивания значений столбцам с автоматическим преобразованием типов + """ + + @declared_attr + def __safe_setattr__(self) -> Callable: + def safe_setattr(self, key: str, value: Any) -> None: + """ + Безопасно устанавливает атрибут для экземпляра. + + Args: + key (str): Имя атрибута. + value (Any): Значение атрибута. + """ + setattr(self, key, value) + + return safe_setattr + + def __setattr__(self, key: str, value: Any) -> None: + """ + Переопределяем __setattr__ для использования безопасного присваивания + """ + safe_method = getattr(self, "__safe_setattr__", object.__setattr__) + safe_method(self, key, value) + + +class BaseModel(_Base, SafeColumnMixin): # type: ignore[valid-type,misc] + __abstract__ = True + __allow_unmapped__ = True + __table_args__: ClassVar[Union[dict[str, Any], tuple]] = {"extend_existing": True} + + id = Column(Integer, primary_key=True) + + def __init_subclass__(cls, **kwargs: Any) -> None: + REGISTRY[cls.__name__] = cls + super().__init_subclass__(**kwargs) + + def dict(self, access: bool = False) -> builtins.dict[str, Any]: + """ + Конвертирует ORM объект в словарь. + + Пропускает атрибуты, которые отсутствуют в объекте, но присутствуют в колонках таблицы. + Преобразует JSON поля в словари. + + Returns: + Dict[str, Any]: Словарь с атрибутами объекта + """ + column_names = filter(lambda x: x not in FILTERED_FIELDS, self.__table__.columns.keys()) + data: builtins.dict[str, Any] = {} + try: + for column_name in column_names: + try: + # Проверяем, существует ли атрибут в объекте + if hasattr(self, column_name): + value = getattr(self, column_name) + # Проверяем, является ли значение JSON и декодируем его при необходимости + if isinstance(value, (str, bytes)) and isinstance( + self.__table__.columns[column_name].type, JSON + ): + try: + data[column_name] = orjson.loads(value) + except (TypeError, orjson.JSONDecodeError) as e: + logger.exception(f"Error decoding JSON for column '{column_name}': {e}") + data[column_name] = value + else: + data[column_name] = value + else: + # Пропускаем атрибут, если его нет в объекте (может быть добавлен после миграции) + logger.debug(f"Skipping missing attribute '{column_name}' for {self.__class__.__name__}") + except AttributeError as e: + logger.warning(f"Attribute error for column '{column_name}': {e}") + except Exception as e: + logger.exception(f"Error occurred while converting object to dictionary: {e}") + return data + + def update(self, values: builtins.dict[str, Any]) -> None: + for key, value in values.items(): + if hasattr(self, key): + setattr(self, key, value) diff --git a/orm/collection.py b/orm/collection.py index 3587e7e4..65063e74 100644 --- a/orm/collection.py +++ b/orm/collection.py @@ -3,7 +3,7 @@ import time from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship -from services.db import BaseModel as Base +from orm.base import BaseModel as Base class ShoutCollection(Base): diff --git a/orm/community.py b/orm/community.py index 3280bdfa..2871d5fb 100644 --- a/orm/community.py +++ b/orm/community.py @@ -1,12 +1,12 @@ import time from typing import Any, Dict -from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String, Text, UniqueConstraint, distinct, func +from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String, UniqueConstraint, distinct, func from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship from auth.orm import Author -from services.db import BaseModel +from orm.base import BaseModel from services.rbac import get_permissions_for_role # Словарь названий ролей @@ -372,7 +372,7 @@ class CommunityAuthor(BaseModel): id = Column(Integer, primary_key=True) community_id = Column(Integer, ForeignKey("community.id"), nullable=False) author_id = Column(Integer, ForeignKey("author.id"), nullable=False) - roles = Column(Text, nullable=True, comment="Roles (comma-separated)") + roles = Column(String, nullable=True, comment="Roles (comma-separated)") joined_at = Column(Integer, nullable=False, default=lambda: int(time.time())) # Связи @@ -435,12 +435,16 @@ class CommunityAuthor(BaseModel): def set_roles(self, roles: list[str]) -> None: """ - Устанавливает полный список ролей (заменяет текущие) + Устанавливает роли для CommunityAuthor. Args: roles: Список ролей для установки """ - self.role_list = roles + # Фильтруем и очищаем роли + valid_roles = [role.strip() for role in roles if role and role.strip()] + + # Если список пустой, устанавливаем None + self.roles = ",".join(valid_roles) if valid_roles else "" async def get_permissions(self) -> list[str]: """ diff --git a/orm/draft.py b/orm/draft.py index 3e0699d0..207c3af5 100644 --- a/orm/draft.py +++ b/orm/draft.py @@ -4,14 +4,14 @@ from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String from sqlalchemy.orm import relationship from auth.orm import Author +from orm.base import BaseModel as Base from orm.topic import Topic -from services.db import BaseModel as Base class DraftTopic(Base): __tablename__ = "draft_topic" - id = None # type: ignore + id = None # type: ignore[misc] shout = Column(ForeignKey("draft.id"), primary_key=True, index=True) topic = Column(ForeignKey("topic.id"), primary_key=True, index=True) main = Column(Boolean, nullable=True) @@ -20,7 +20,7 @@ class DraftTopic(Base): class DraftAuthor(Base): __tablename__ = "draft_author" - id = None # type: ignore + id = None # type: ignore[misc] shout = Column(ForeignKey("draft.id"), primary_key=True, index=True) author = Column(ForeignKey("author.id"), primary_key=True, index=True) caption = Column(String, nullable=True, default="") diff --git a/orm/invite.py b/orm/invite.py index d81a210e..8475a5c7 100644 --- a/orm/invite.py +++ b/orm/invite.py @@ -3,7 +3,7 @@ import enum from sqlalchemy import Column, ForeignKey, String from sqlalchemy.orm import relationship -from services.db import BaseModel as Base +from orm.base import BaseModel as Base class InviteStatus(enum.Enum): @@ -12,7 +12,7 @@ class InviteStatus(enum.Enum): REJECTED = "REJECTED" @classmethod - def from_string(cls, value): + def from_string(cls, value: str) -> "Invite": return cls(value) diff --git a/orm/notification.py b/orm/notification.py index b7270b73..e08116de 100644 --- a/orm/notification.py +++ b/orm/notification.py @@ -1,35 +1,120 @@ import enum -import time +from datetime import datetime +from enum import Enum, auto -from sqlalchemy import JSON, Column, ForeignKey, Integer, String +from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String +from sqlalchemy import Enum as SQLAlchemyEnum from sqlalchemy.orm import relationship -from auth.orm import Author -from services.db import BaseModel as Base +from orm.author import Author +from orm.base import BaseModel as Base +from services.logger import root_logger as logger + + +class NotificationStatus(Enum): + """Статусы уведомлений.""" + + UNREAD = auto() + READ = auto() + ARCHIVED = auto() + + @classmethod + def from_string(cls, value: str) -> "NotificationStatus": + """ + Создает экземпляр статуса уведомления из строки. + + Args: + value (str): Строковое представление статуса. + + Returns: + NotificationStatus: Экземпляр статуса уведомления. + """ + try: + return cls[value.upper()] + except KeyError: + logger.error(f"Неверный статус уведомления: {value}") + raise ValueError("Неверный статус уведомления") # noqa: B904 + + +class NotificationKind(Enum): + """Типы уведомлений.""" + + COMMENT = auto() + MENTION = auto() + REACTION = auto() + FOLLOW = auto() + INVITE = auto() + + @classmethod + def from_string(cls, value: str) -> "NotificationKind": + """ + Создает экземпляр типа уведомления из строки. + + Args: + value (str): Строковое представление типа. + + Returns: + NotificationKind: Экземпляр типа уведомления. + """ + try: + return cls[value.upper()] + except KeyError: + logger.error(f"Неверный тип уведомления: {value}") + raise ValueError("Неверный тип уведомления") # noqa: B904 class NotificationEntity(enum.Enum): - REACTION = "reaction" + """Сущности, связанные с уведомлениями.""" + + TOPIC = "topic" + COMMENT = "comment" SHOUT = "shout" - FOLLOWER = "follower" + AUTHOR = "author" COMMUNITY = "community" @classmethod - def from_string(cls, value): - return cls(value) + def from_string(cls, value: str) -> "NotificationEntity": + """ + Создает экземпляр сущности уведомления из строки. + + Args: + value (str): Строковое представление сущности. + + Returns: + NotificationEntity: Экземпляр сущности уведомления. + """ + try: + return cls(value) + except ValueError: + logger.error(f"Неверная сущность уведомления: {value}") + raise ValueError("Неверная сущность уведомления") # noqa: B904 class NotificationAction(enum.Enum): + """Действия в уведомлениях.""" + CREATE = "create" UPDATE = "update" DELETE = "delete" - SEEN = "seen" - FOLLOW = "follow" - UNFOLLOW = "unfollow" + MENTION = "mention" + REACT = "react" @classmethod - def from_string(cls, value): - return cls(value) + def from_string(cls, value: str) -> "NotificationAction": + """ + Создает экземпляр действия уведомления из строки. + + Args: + value (str): Строковое представление действия. + + Returns: + NotificationAction: Экземпляр действия уведомления. + """ + try: + return cls(value) + except ValueError: + logger.error(f"Неверное действие уведомления: {value}") + raise ValueError("Неверное действие уведомления") # noqa: B904 class NotificationSeen(Base): @@ -42,22 +127,31 @@ class NotificationSeen(Base): class Notification(Base): __tablename__ = "notification" - id = Column(Integer, primary_key=True, autoincrement=True) - created_at = Column(Integer, server_default=str(int(time.time()))) + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + entity = Column(String, nullable=False) action = Column(String, nullable=False) payload = Column(JSON, nullable=True) + status = Column(SQLAlchemyEnum(NotificationStatus), default=NotificationStatus.UNREAD) + kind = Column(SQLAlchemyEnum(NotificationKind), nullable=False) + seen = relationship(Author, secondary="notification_seen") def set_entity(self, entity: NotificationEntity): - self.entity = entity.value # type: ignore[assignment] + """Устанавливает сущность уведомления.""" + self.entity = entity.value def get_entity(self) -> NotificationEntity: + """Возвращает сущность уведомления.""" return NotificationEntity.from_string(self.entity) def set_action(self, action: NotificationAction): - self.action = action.value # type: ignore[assignment] + """Устанавливает действие уведомления.""" + self.action = action.value def get_action(self) -> NotificationAction: + """Возвращает действие уведомления.""" return NotificationAction.from_string(self.action) diff --git a/orm/reaction.py b/orm/reaction.py index 9073de76..cce2c035 100644 --- a/orm/reaction.py +++ b/orm/reaction.py @@ -3,7 +3,7 @@ from enum import Enum as Enumeration from sqlalchemy import Column, ForeignKey, Integer, String -from services.db import BaseModel as Base +from orm.base import BaseModel as Base class ReactionKind(Enumeration): diff --git a/orm/shout.py b/orm/shout.py index 7319ecec..714d372c 100644 --- a/orm/shout.py +++ b/orm/shout.py @@ -4,9 +4,9 @@ from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String from sqlalchemy.orm import relationship from auth.orm import Author +from orm.base import BaseModel as Base from orm.reaction import Reaction from orm.topic import Topic -from services.db import BaseModel as Base class ShoutTopic(Base): @@ -21,7 +21,7 @@ class ShoutTopic(Base): __tablename__ = "shout_topic" - id = None # type: ignore + id = None # type: ignore[misc] shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) topic = Column(ForeignKey("topic.id"), primary_key=True, index=True) main = Column(Boolean, nullable=True) @@ -36,7 +36,7 @@ class ShoutTopic(Base): class ShoutReactionsFollower(Base): __tablename__ = "shout_reactions_followers" - id = None # type: ignore + id = None # type: ignore[misc] follower = Column(ForeignKey("author.id"), primary_key=True, index=True) shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) auto = Column(Boolean, nullable=False, default=False) @@ -56,7 +56,7 @@ class ShoutAuthor(Base): __tablename__ = "shout_author" - id = None # type: ignore + id = None # type: ignore[misc] shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) author = Column(ForeignKey("author.id"), primary_key=True, index=True) caption = Column(String, nullable=True, default="") diff --git a/orm/topic.py b/orm/topic.py index 60f2657c..2cc35f6d 100644 --- a/orm/topic.py +++ b/orm/topic.py @@ -2,7 +2,7 @@ import time from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String -from services.db import BaseModel as Base +from orm.base import BaseModel as Base class TopicFollower(Base): @@ -18,7 +18,7 @@ class TopicFollower(Base): __tablename__ = "topic_followers" - id = None # type: ignore + id = None # type: ignore[misc] follower = Column(Integer, ForeignKey("author.id"), primary_key=True) topic = Column(Integer, ForeignKey("topic.id"), primary_key=True) created_at = Column(Integer, nullable=False, default=int(time.time())) diff --git a/package-lock.json b/package-lock.json index 545d425e..6d1ff6bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,22 +11,22 @@ "@solidjs/router": "^0.15.3" }, "devDependencies": { - "@biomejs/biome": "^2.0.6", + "@biomejs/biome": "^2.1.2", "@graphql-codegen/cli": "^5.0.7", "@graphql-codegen/client-preset": "^4.8.3", - "@graphql-codegen/typescript": "^4.0.6", - "@graphql-codegen/typescript-operations": "^4.2.0", - "@graphql-codegen/typescript-resolvers": "^4.0.6", - "@types/node": "^24.0.7", + "@graphql-codegen/typescript": "^4.1.6", + "@graphql-codegen/typescript-operations": "^4.6.1", + "@graphql-codegen/typescript-resolvers": "^4.5.1", + "@types/node": "^24.1.0", "@types/prismjs": "^1.26.5", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", - "lightningcss": "^1.30.0", + "lightningcss": "^1.30.1", "prismjs": "^1.30.0", "solid-js": "^1.9.7", - "terser": "^5.39.0", + "terser": "^5.43.0", "typescript": "^5.8.3", - "vite": "^7.0.0", + "vite": "^7.0.6", "vite-plugin-solid": "^2.11.7" } }, @@ -242,14 +242,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", - "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" @@ -304,9 +304,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", "dev": true, "license": "MIT", "engines": { @@ -348,9 +348,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -569,9 +569,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", "cpu": [ "ppc64" ], @@ -586,9 +586,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", "cpu": [ "arm" ], @@ -603,9 +603,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", "cpu": [ "arm64" ], @@ -620,9 +620,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", "cpu": [ "x64" ], @@ -637,9 +637,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", "cpu": [ "arm64" ], @@ -654,9 +654,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", "cpu": [ "x64" ], @@ -671,9 +671,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", "cpu": [ "arm64" ], @@ -688,9 +688,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", "cpu": [ "x64" ], @@ -705,9 +705,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", "cpu": [ "arm" ], @@ -722,9 +722,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", "cpu": [ "arm64" ], @@ -739,9 +739,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", "cpu": [ "ia32" ], @@ -756,9 +756,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", "cpu": [ "loong64" ], @@ -773,9 +773,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", "cpu": [ "mips64el" ], @@ -790,9 +790,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", "cpu": [ "ppc64" ], @@ -807,9 +807,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", "cpu": [ "riscv64" ], @@ -824,9 +824,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", "cpu": [ "s390x" ], @@ -841,9 +841,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", "cpu": [ "x64" ], @@ -858,9 +858,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", "cpu": [ "arm64" ], @@ -875,9 +875,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", "cpu": [ "x64" ], @@ -892,9 +892,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", "cpu": [ "arm64" ], @@ -909,9 +909,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", "cpu": [ "x64" ], @@ -926,9 +926,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", "cpu": [ "arm64" ], @@ -943,9 +943,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", "cpu": [ "x64" ], @@ -960,9 +960,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", "cpu": [ "arm64" ], @@ -977,9 +977,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", "cpu": [ "ia32" ], @@ -994,9 +994,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", "cpu": [ "x64" ], @@ -1405,13 +1405,13 @@ } }, "node_modules/@graphql-tools/apollo-engine-loader": { - "version": "8.0.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.21.tgz", - "integrity": "sha512-3o63uKvx2d/01GhR8Q4RACIScJG7SxliU+xxPVaC6SWpsRkvfHKXJITWctNIw5PBH5HiB25sL9a5AFHCQp0OEQ==", + "version": "8.0.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.22.tgz", + "integrity": "sha512-ssD2wNxeOTRcUEkuGcp0KfZAGstL9YLTe/y3erTDZtOs2wL1TJESw8NVAp+3oUHPeHKBZQB4Z6RFEbPgMdT2wA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/fetch": "^0.10.0", "sync-fetch": "0.6.0-2", "tslib": "^2.4.0" @@ -1424,13 +1424,13 @@ } }, "node_modules/@graphql-tools/batch-execute": { - "version": "9.0.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.17.tgz", - "integrity": "sha512-i7BqBkUP2+ex8zrQrCQTEt6nYHQmIey9qg7CMRRa1hXCY2X8ZCVjxsvbsi7gOLwyI/R3NHxSRDxmzZevE2cPLg==", + "version": "9.0.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.18.tgz", + "integrity": "sha512-KtBglqPGR/3CZtQevFRBBc6MJpIgxBqfCrUV5sdC3oJsafmPShgr+lxM178SW5i1QHmiVAScOWGWqWp9HbnpoQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.8.1", + "@graphql-tools/utils": "^10.9.0", "@whatwg-node/promise-helpers": "^1.3.0", "dataloader": "^2.2.3", "tslib": "^2.8.1" @@ -1443,14 +1443,14 @@ } }, "node_modules/@graphql-tools/code-file-loader": { - "version": "8.1.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.21.tgz", - "integrity": "sha512-NmHEijQ9uEPcM5riM3NsQcT2piESgV2QX6/pIcKineBXQ/2nbeKtxOqWi2omCVLHSKmjOlR1Yyn3E2alqWVOxg==", + "version": "8.1.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.22.tgz", + "integrity": "sha512-FSka29kqFkfFmw36CwoQ+4iyhchxfEzPbXOi37lCEjWLHudGaPkXc3RyB9LdmBxx3g3GHEu43a5n5W8gfcrMdA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.20", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/graphql-tag-pluck": "8.3.21", + "@graphql-tools/utils": "^10.9.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -1463,16 +1463,16 @@ } }, "node_modules/@graphql-tools/delegate": { - "version": "10.2.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.21.tgz", - "integrity": "sha512-YLyyuhxrZniVufZV/6Oba5xIvWqVRyZrO8LsM+hI4Q6/aR1OdJafi9IBqCE2hUDPfIc8wkhqixA2/WT+oApY3g==", + "version": "10.2.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.22.tgz", + "integrity": "sha512-1jkTF5DIhO1YJ0dlgY03DZYAiSwlu5D2mdjeq+f6oyflyKG9E4SPmkLgVdDSNSfGxFHHrjIvYjUhPYV0vAOiDg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/batch-execute": "^9.0.17", - "@graphql-tools/executor": "^1.4.7", - "@graphql-tools/schema": "^10.0.11", - "@graphql-tools/utils": "^10.8.1", + "@graphql-tools/batch-execute": "^9.0.18", + "@graphql-tools/executor": "^1.4.8", + "@graphql-tools/schema": "^10.0.24", + "@graphql-tools/utils": "^10.9.0", "@repeaterjs/repeater": "^3.0.6", "@whatwg-node/promise-helpers": "^1.3.0", "dataloader": "^2.2.3", @@ -1504,13 +1504,13 @@ } }, "node_modules/@graphql-tools/executor": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.4.8.tgz", - "integrity": "sha512-eMFWo30+L8BPME5qhJ3b4WOEAMSIMdi41F0afp40RH9RWQWnJ9R9Tr6vq7CZzmlM8qxymEE4UMAnu2qG/5Jyqg==", + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.4.9.tgz", + "integrity": "sha512-SAUlDT70JAvXeqV87gGzvDzUGofn39nvaVcVhNf12Dt+GfWHtNNO/RCn/Ea4VJaSLGzraUd41ObnN3i80EBU7w==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/utils": "^10.9.1", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/disposablestack": "^0.0.6", @@ -1542,19 +1542,36 @@ } }, "node_modules/@graphql-tools/executor-graphql-ws": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.5.tgz", - "integrity": "sha512-gI/D9VUzI1Jt1G28GYpvm5ckupgJ5O8mi5Y657UyuUozX34ErfVdZ81g6oVcKFQZ60LhCzk7jJeykK48gaLhDw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.6.tgz", + "integrity": "sha512-hLmY+h1HDM4+y4EXP0SgNFd6hXEs4LCMAxvvdfPAwrzHNM04B0wnlcOi8Rze3e7AA9edxXQsm3UN4BE04U2OMg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/executor-common": "^0.0.4", - "@graphql-tools/utils": "^10.8.1", + "@graphql-tools/executor-common": "^0.0.5", + "@graphql-tools/utils": "^10.9.0", "@whatwg-node/disposablestack": "^0.0.6", - "graphql-ws": "^6.0.3", + "graphql-ws": "^6.0.6", "isomorphic-ws": "^5.0.0", "tslib": "^2.8.1", - "ws": "^8.17.1" + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-graphql-ws/node_modules/@graphql-tools/executor-common": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.5.tgz", + "integrity": "sha512-DBTQDGYajhUd4iBZ/yYc1LY85QTVhgTpGPCFT5iz0CPObgye0smsE5nd/BIcdbML7SXv2wFvQhVA3mCJJ32WuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@envelop/core": "^5.3.0", + "@graphql-tools/utils": "^10.9.0" }, "engines": { "node": ">=18.0.0" @@ -1588,13 +1605,13 @@ } }, "node_modules/@graphql-tools/executor-legacy-ws": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.18.tgz", - "integrity": "sha512-KCsf4e3t/TyT06GeXEbWW08tbN+/uYOhFDU7RRMP4S1zIVIsIcdFmCjemBtrYDu93mwib63NidGX+mQXm1tmLg==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.19.tgz", + "integrity": "sha512-bEbv/SlEdhWQD0WZLUX1kOenEdVZk1yYtilrAWjRUgfHRZoEkY9s+oiqOxnth3z68wC2MWYx7ykkS5hhDamixg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/utils": "^10.9.1", "@types/ws": "^8.0.0", "isomorphic-ws": "^5.0.0", "tslib": "^2.4.0", @@ -1608,14 +1625,14 @@ } }, "node_modules/@graphql-tools/git-loader": { - "version": "8.0.25", - "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.25.tgz", - "integrity": "sha512-Zp9GtGfbnqwaFCUYQmTzJ3uKDgvHQfkaYSAQp+ZBKUrKu/m/TG6oxoy6duFYKujh7+fB0fhHYPJXdkGTSemBHA==", + "version": "8.0.26", + "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.26.tgz", + "integrity": "sha512-0g+9eng8DaT4ZmZvUmPgjLTgesUa6M8xrDjNBltRldZkB055rOeUgJiKmL6u8PjzI5VxkkVsn0wtAHXhDI2UXQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/graphql-tag-pluck": "8.3.20", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/graphql-tag-pluck": "8.3.21", + "@graphql-tools/utils": "^10.9.1", "is-glob": "4.0.3", "micromatch": "^4.0.8", "tslib": "^2.4.0", @@ -1629,15 +1646,15 @@ } }, "node_modules/@graphql-tools/github-loader": { - "version": "8.0.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.21.tgz", - "integrity": "sha512-bXy8XDRz8YqMLZM7s6XW6eeADCjyAvlyUENBwP3pN9AyTh6xN61EHruFLbaMaGnQOlKITohxFM4mrrcRWJ1Iog==", + "version": "8.0.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.22.tgz", + "integrity": "sha512-uQ4JNcNPsyMkTIgzeSbsoT9hogLjYrZooLUYd173l5eUGUi49EAcsGdiBCKaKfEjanv410FE8hjaHr7fjSRkJw==", "dev": true, "license": "MIT", "dependencies": { "@graphql-tools/executor-http": "^1.1.9", - "@graphql-tools/graphql-tag-pluck": "^8.3.20", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/graphql-tag-pluck": "^8.3.21", + "@graphql-tools/utils": "^10.9.1", "@whatwg-node/fetch": "^0.10.0", "@whatwg-node/promise-helpers": "^1.0.0", "sync-fetch": "0.6.0-2", @@ -1651,14 +1668,14 @@ } }, "node_modules/@graphql-tools/graphql-file-loader": { - "version": "8.0.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.21.tgz", - "integrity": "sha512-E11KcRIIM6W04mDV95kx7SDrbqVD58jP3O1227JfBddzOx5q5Rb2b/1Sxw1+eNnGZT+xdT/506SrJ5dhLtwUrA==", + "version": "8.0.22", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.22.tgz", + "integrity": "sha512-KFUbjXgWr5+w/AioOuIuULy4LwcyDuQqTRFQGe+US1d9Z4+ZopcJLwsJTqp5B+icDkCqld4paN0y0qi9MrIvbg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/import": "7.0.20", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/import": "7.0.21", + "@graphql-tools/utils": "^10.9.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -1671,9 +1688,9 @@ } }, "node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "8.3.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.20.tgz", - "integrity": "sha512-HBukyPzrS3GyWkBkB/vblN+Fhb+tBKWL9rEHaexxTU+J8YHkXHAYlLvu56NXcCBzpVGWP2ghJqPh+ZPaqaiThQ==", + "version": "8.3.21", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.21.tgz", + "integrity": "sha512-TJhELNvR1tmghXMi6HVKp/Swxbx1rcSp/zdkuJZT0DCM3vOY11FXY6NW3aoxumcuYDNN3jqXcCPKstYGFPi5GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1682,7 +1699,7 @@ "@babel/plugin-syntax-import-assertions": "^7.26.0", "@babel/traverse": "^7.26.10", "@babel/types": "^7.26.10", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "engines": { @@ -1693,13 +1710,13 @@ } }, "node_modules/@graphql-tools/import": { - "version": "7.0.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.20.tgz", - "integrity": "sha512-Mz+1hBRnQYr4R5hdxc0to//v7V0OsBZH8BHbZgKvM5ayIBFl3+ArQFlfitukmrvZLmmi7UwordW3RG2yLjSx8A==", + "version": "7.0.21", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.21.tgz", + "integrity": "sha512-bcAqNWm/gLVEOy55o/WdaROERpDyUEmIfZ9E6NDjVk1ZGWfZe47+RgriTV80j6J5S5J1g+6loFkVWGAMqdN06g==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/utils": "^10.9.1", "@theguild/federation-composition": "^0.19.0", "resolve-from": "5.0.0", "tslib": "^2.4.0" @@ -1712,13 +1729,13 @@ } }, "node_modules/@graphql-tools/json-file-loader": { - "version": "8.0.19", - "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.19.tgz", - "integrity": "sha512-msohJvmtlunrcFQJSVX1JOwd2hR6bewENY2LciX4zPrFRQqWc4LsYhU1S0X92iiBxpyz/tff+sJH/6ubncWlRg==", + "version": "8.0.20", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.20.tgz", + "integrity": "sha512-5v6W+ZLBBML5SgntuBDLsYoqUvwfNboAwL6BwPHi3z/hH1f8BS9/0+MCW9OGY712g7E4pc3y9KqS67mWF753eA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/utils": "^10.9.1", "globby": "^11.0.3", "tslib": "^2.4.0", "unixify": "^1.0.0" @@ -1731,14 +1748,14 @@ } }, "node_modules/@graphql-tools/load": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.1.tgz", - "integrity": "sha512-hqxk+8VHQcl68UFuuTx46DesAJmjQdiGxjicNoB4m4nqk6itWtPYn7Qj9W9iq95PvbicWQasrAQ2srUbIoWE2A==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.2.tgz", + "integrity": "sha512-WhDPv25/jRND+0uripofMX0IEwo6mrv+tJg6HifRmDu8USCD7nZhufT0PP7lIcuutqjIQFyogqT70BQsy6wOgw==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/schema": "^10.0.24", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/schema": "^10.0.25", + "@graphql-tools/utils": "^10.9.1", "p-limit": "3.1.0", "tslib": "^2.4.0" }, @@ -1750,14 +1767,13 @@ } }, "node_modules/@graphql-tools/merge": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.0.tgz", - "integrity": "sha512-mKmjIVeu4ayPr+LbuhzukBOd67YdLhe9uPO/2tQ74iXP0EQMPlzAbUGPPym92gqCT5SxM6kXT65JUE9oBRX0sQ==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.1.tgz", + "integrity": "sha512-BJ5/7Y7GOhTuvzzO5tSBFL4NGr7PVqTJY3KeIDlVTT8YLcTXtBR+hlrC3uyEym7Ragn+zyWdHeJ9ev+nRX1X2w==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.9.0", - "@theguild/federation-composition": "^0.19.0", + "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "engines": { @@ -1815,14 +1831,14 @@ } }, "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "7.0.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.20.tgz", - "integrity": "sha512-8xl03O/xwME4oRP7BEQEI8OI+ph3oDqQapNEV3X5UIxxLwAj6EKtpXR0mr2LSN9Ico6phrj8cEwVY+hBqAMo0w==", + "version": "7.0.21", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.21.tgz", + "integrity": "sha512-vMdU0+XfeBh9RCwPqRsr3A05hPA3MsahFn/7OAwXzMySA5EVnSH5R4poWNs3h1a0yT0tDPLhxORhK7qJdSWj2A==", "dev": true, "license": "MIT", "dependencies": { "@ardatan/relay-compiler": "^12.0.3", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "engines": { @@ -1833,14 +1849,14 @@ } }, "node_modules/@graphql-tools/schema": { - "version": "10.0.24", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.24.tgz", - "integrity": "sha512-SQfYg31/L4EShTygz9I/+Issa3IDS7DFB/gd7AvWeICCNMDm0917QmLDYpVaCmgvzeky7JPeXaJEd0OtZNIW4Q==", + "version": "10.0.25", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.25.tgz", + "integrity": "sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/merge": "^9.1.0", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/merge": "^9.1.1", + "@graphql-tools/utils": "^10.9.1", "tslib": "^2.4.0" }, "engines": { @@ -1851,16 +1867,16 @@ } }, "node_modules/@graphql-tools/url-loader": { - "version": "8.0.32", - "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.32.tgz", - "integrity": "sha512-dr4eu+/Twbq6bS4O2ASi6EdTLC2bcxo+Iw0j1eDkonw+U5lK/2+aHF/bWRXVTMYMrWOLxv0+iYeGVe/zMjDbEg==", + "version": "8.0.33", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.33.tgz", + "integrity": "sha512-Fu626qcNHcqAj8uYd7QRarcJn5XZ863kmxsg1sm0fyjyfBJnsvC7ddFt6Hayz5kxVKfsnjxiDfPMXanvsQVBKw==", "dev": true, "license": "MIT", "dependencies": { "@graphql-tools/executor-graphql-ws": "^2.0.1", "@graphql-tools/executor-http": "^1.1.9", - "@graphql-tools/executor-legacy-ws": "^1.1.18", - "@graphql-tools/utils": "^10.9.0", + "@graphql-tools/executor-legacy-ws": "^1.1.19", + "@graphql-tools/utils": "^10.9.1", "@graphql-tools/wrap": "^10.0.16", "@types/ws": "^8.0.0", "@whatwg-node/fetch": "^0.10.0", @@ -1878,9 +1894,9 @@ } }, "node_modules/@graphql-tools/utils": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.9.0.tgz", - "integrity": "sha512-LzFlJHNajdohRM+0pHTwcF9tZ0q7z5iZW0lwnTNJp7O6GYFcSvCQE5ijTQcXVQ/5WQf3SHn+Gpr56TR5XHmPtg==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.9.1.tgz", + "integrity": "sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw==", "dev": true, "license": "MIT", "dependencies": { @@ -1898,15 +1914,15 @@ } }, "node_modules/@graphql-tools/wrap": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.2.tgz", - "integrity": "sha512-vjmPVrYCRelytltyzHy1+QP4mIBRcStjbDNsEC1TMth9KH9wGi3xToIjAAD4GTOnrc6UyZ9IqaIAhffEnhBTRQ==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.3.tgz", + "integrity": "sha512-YIcw7oZPlmlZKRBOQGNqKNY4lehB+U4NOP0BSuOd+23EZb8X7JjkruYUOjYsQ7GxS7aKmQpFbuqrfsLp9TRZnA==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/delegate": "^10.2.21", - "@graphql-tools/schema": "^10.0.11", - "@graphql-tools/utils": "^10.8.1", + "@graphql-tools/delegate": "^10.2.22", + "@graphql-tools/schema": "^10.0.24", + "@graphql-tools/utils": "^10.9.0", "@whatwg-node/promise-helpers": "^1.3.0", "tslib": "^2.8.1" }, @@ -2390,9 +2406,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.0.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", - "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", "dev": true, "license": "MIT", "dependencies": { @@ -3257,9 +3273,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.187", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "version": "1.5.190", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz", + "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==", "dev": true, "license": "ISC" }, @@ -3294,9 +3310,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3307,32 +3323,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" } }, "node_modules/escalade": { @@ -3608,9 +3624,9 @@ } }, "node_modules/graphql-config/node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "dev": true, "license": "MIT", "bin": { @@ -5859,15 +5875,15 @@ "license": "ISC" }, "node_modules/vite": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", - "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", - "picomatch": "^4.0.2", + "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" diff --git a/package.json b/package.json index 4feeef4a..bdd101c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "publy-panel", - "version": "0.7.8", + "version": "0.7.9", "private": true, "scripts": { "dev": "vite", @@ -12,26 +12,26 @@ "codegen": "graphql-codegen --config codegen.ts" }, "devDependencies": { - "@biomejs/biome": "^2.0.6", + "@biomejs/biome": "^2.1.2", "@graphql-codegen/cli": "^5.0.7", "@graphql-codegen/client-preset": "^4.8.3", - "@graphql-codegen/typescript": "^4.0.6", - "@graphql-codegen/typescript-operations": "^4.2.0", - "@graphql-codegen/typescript-resolvers": "^4.0.6", - "@types/node": "^24.0.7", + "@graphql-codegen/typescript": "^4.1.6", + "@graphql-codegen/typescript-operations": "^4.6.1", + "@graphql-codegen/typescript-resolvers": "^4.5.1", + "@types/node": "^24.1.0", "@types/prismjs": "^1.26.5", "graphql": "^16.11.0", "graphql-tag": "^2.12.6", - "lightningcss": "^1.30.0", + "lightningcss": "^1.30.1", "prismjs": "^1.30.0", "solid-js": "^1.9.7", - "terser": "^5.39.0", + "terser": "^5.43.0", "typescript": "^5.8.3", - "vite": "^7.0.0", + "vite": "^7.0.6", "vite-plugin-solid": "^2.11.7" }, "overrides": { - "vite": "^7.0.0" + "vite": "^7.0.6" }, "dependencies": { "@solidjs/router": "^0.15.3" diff --git a/requirements.txt b/requirements.txt index ecd287d4..3796dc4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ bcrypt PyJWT authlib -passlib==1.7.4 google-analytics-data colorlog psycopg2-binary @@ -12,6 +11,7 @@ starlette gql ariadne granian +bcrypt # NLP and search httpx @@ -21,7 +21,6 @@ pydantic trafilatura types-requests -types-passlib types-Authlib types-orjson types-PyYAML diff --git a/resolvers/admin.py b/resolvers/admin.py index 6f6ac89f..8a3ab6e8 100644 --- a/resolvers/admin.py +++ b/resolvers/admin.py @@ -731,8 +731,8 @@ async def admin_get_reactions( "deleted_at": shout.deleted_at, }, "stat": { - "comments_count": stats.comments_count or 0, - "rating": stats.rating or 0, + "comments_count": stats.comments_count if stats else 0, + "rating": stats.rating if stats else 0, }, } ) diff --git a/resolvers/author.py b/resolvers/author.py index 43fe4090..83fd1f6f 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -609,22 +609,7 @@ def create_author(**kwargs) -> Author: # Получаем сообщество для назначения дефолтных ролей community = session.query(Community).filter(Community.id == target_community_id).first() if community: - # Инициализируем права сообщества если нужно - try: - import asyncio - - loop = asyncio.get_event_loop() - loop.run_until_complete(community.initialize_role_permissions()) - except Exception as e: - logger.warning(f"Не удалось инициализировать права сообщества {target_community_id}: {e}") - - # Получаем дефолтные роли сообщества или используем стандартные - try: - default_roles = community.get_default_roles() - if not default_roles: - default_roles = ["reader", "author"] - except AttributeError: - default_roles = ["reader", "author"] + default_roles = community.get_default_roles() # Создаем CommunityAuthor с дефолтными ролями community_author = CommunityAuthor( diff --git a/services/admin.py b/services/admin.py index f06e4590..40f71663 100644 --- a/services/admin.py +++ b/services/admin.py @@ -72,20 +72,31 @@ class AdminService: @staticmethod def get_user_roles(user: Author, community_id: int = 1) -> list[str]: """Получает роли пользователя в сообществе""" + from orm.community import CommunityAuthor # Явный импорт from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST admin_emails = ADMIN_EMAILS_LIST.split(",") if ADMIN_EMAILS_LIST else [] user_roles = [] with local_session() as session: + # Получаем все CommunityAuthor для пользователя + all_community_authors = session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).all() + + # Сначала ищем точное совпадение по community_id community_author = ( session.query(CommunityAuthor) .filter(CommunityAuthor.author_id == user.id, CommunityAuthor.community_id == community_id) .first() ) + # Если точного совпадения нет, используем первый найденный CommunityAuthor + if not community_author and all_community_authors: + community_author = all_community_authors[0] + if community_author: - user_roles = community_author.role_list + # Проверяем, что roles не None и не пустая строка + if community_author.roles is not None and community_author.roles.strip(): + user_roles = community_author.role_list # Добавляем синтетическую роль для системных админов if user.email and user.email.lower() in [email.lower() for email in admin_emails]: @@ -188,7 +199,15 @@ class AdminService: community_author.set_roles(valid_roles) session.commit() logger.info(f"Пользователь {author.email or author.id} обновлен") - return {"success": True} + + # Возвращаем обновленного пользователя + return { + "success": True, + "name": author.name, + "email": author.email, + "slug": author.slug, + "roles": self.get_user_roles(author), + } # === ПУБЛИКАЦИИ === diff --git a/services/auth.py b/services/auth.py index 2eb5f7d8..2004d085 100644 --- a/services/auth.py +++ b/services/auth.py @@ -153,37 +153,54 @@ class AuthService: def create_user(self, user_dict: dict[str, Any], community_id: int | None = None) -> Author: """Создает нового пользователя с дефолтными ролями""" + # Нормализуем email + if "email" in user_dict: + user_dict["email"] = user_dict["email"].lower() + + # Проверяем уникальность email + with local_session() as session: + existing_user = session.query(Author).filter(Author.email == user_dict["email"]).first() + if existing_user: + # Если пользователь с таким email уже существует, возвращаем его + logger.warning(f"Пользователь с email {user_dict['email']} уже существует") + return existing_user + + # Генерируем уникальный slug + base_slug = user_dict.get("slug", generate_unique_slug(user_dict.get("name", user_dict.get("email", "user")))) + + # Проверяем уникальность slug + with local_session() as session: + # Добавляем суффикс, если slug уже существует + counter = 1 + unique_slug = base_slug + while session.query(Author).filter(Author.slug == unique_slug).first(): + unique_slug = f"{base_slug}-{counter}" + counter += 1 + + user_dict["slug"] = unique_slug + user = Author(**user_dict) - target_community_id = community_id or 1 + target_community_id = int(community_id) if community_id is not None else 1 with local_session() as session: session.add(user) - session.flush() + session.flush() # Получаем ID пользователя # Получаем сообщество для назначения ролей + logger.debug(f"Ищем сообщество с ID {target_community_id}") community = session.query(Community).filter(Community.id == target_community_id).first() + + # Отладочная информация + all_communities = session.query(Community).all() + logger.debug(f"Все сообщества в базе: {[c.id for c in all_communities]}") + if not community: logger.warning(f"Сообщество {target_community_id} не найдено, используем ID=1") target_community_id = 1 community = session.query(Community).filter(Community.id == target_community_id).first() if community: - # Инициализируем права сообщества - try: - import asyncio - - loop = asyncio.get_event_loop() - loop.run_until_complete(community.initialize_role_permissions()) - except Exception as e: - logger.warning(f"Не удалось инициализировать права сообщества: {e}") - - # Получаем дефолтные роли - try: - default_roles = community.get_default_roles() - if not default_roles: - default_roles = ["reader", "author"] - except AttributeError: - default_roles = ["reader", "author"] + default_roles = community.get_default_roles() or ["reader", "author"] # Создаем CommunityAuthor с ролями community_author = CommunityAuthor( @@ -197,7 +214,12 @@ class AuthService: follower = CommunityFollower(community=target_community_id, follower=int(user.id)) session.add(follower) - logger.info(f"Пользователь {user.id} создан с ролями {default_roles}") + logger.info( + f"Пользователь {user.id} создан с ролями {default_roles} в сообществе {target_community_id}" + ) + else: + # Если сообщество не найдено, вызываем исключение + raise ValueError("Сообщество не найдено") session.commit() return user @@ -353,7 +375,7 @@ class AuthService: # Проверяем роли через новую систему CommunityAuthor from orm.community import get_user_roles_in_community - user_roles = get_user_roles_in_community(author.id, community_id=1) + user_roles = get_user_roles_in_community(int(author.id), community_id=1) has_reader_role = "reader" in user_roles logger.debug(f"Роли пользователя {email}: {user_roles}") @@ -676,7 +698,7 @@ class AuthService: stats["checked"] += 1 try: - had_reader = await self.ensure_user_has_reader_role(author.id) + had_reader = await self.ensure_user_has_reader_role(int(author.id)) if not had_reader: stats["fixed"] += 1 diff --git a/services/db.py b/services/db.py index 0551f2c0..5490cc73 100644 --- a/services/db.py +++ b/services/db.py @@ -1,102 +1,34 @@ -import builtins import logging import math import time import traceback import warnings from io import TextIOWrapper -from typing import Any, ClassVar, Type, TypeVar, Union +from typing import Any, TypeVar -import orjson import sqlalchemy -from sqlalchemy import JSON, Column, Integer, create_engine, event, exc, func, inspect +from sqlalchemy import create_engine, event, exc, func, inspect from sqlalchemy.dialects.sqlite import insert from sqlalchemy.engine import Connection, Engine -from sqlalchemy.orm import Session, configure_mappers, declarative_base, joinedload +from sqlalchemy.orm import Session, configure_mappers, joinedload from sqlalchemy.pool import StaticPool +from orm.base import BaseModel from settings import DB_URL from utils.logger import root_logger as logger # Global variables -REGISTRY: dict[str, type["BaseModel"]] = {} logger = logging.getLogger(__name__) # Database configuration engine = create_engine(DB_URL, echo=False, poolclass=StaticPool if "sqlite" in DB_URL else None) ENGINE = engine # Backward compatibility alias - inspector = inspect(engine) +# Session = sessionmaker(engine) configure_mappers() T = TypeVar("T") FILTERED_FIELDS = ["_sa_instance_state", "search_vector"] -# Создаем Base для внутреннего использования -_Base = declarative_base() - -# Create proper type alias for Base -BaseType = Type[_Base] # type: ignore[valid-type] - - -class BaseModel(_Base): # type: ignore[valid-type,misc] - __abstract__ = True - __allow_unmapped__ = True - __table_args__: ClassVar[Union[dict[str, Any], tuple]] = {"extend_existing": True} - - id = Column(Integer, primary_key=True) - - def __init_subclass__(cls, **kwargs: Any) -> None: - REGISTRY[cls.__name__] = cls - super().__init_subclass__(**kwargs) - - def dict(self, access: bool = False) -> builtins.dict[str, Any]: - """ - Конвертирует ORM объект в словарь. - - Пропускает атрибуты, которые отсутствуют в объекте, но присутствуют в колонках таблицы. - Преобразует JSON поля в словари. - Добавляет синтетическое поле .stat, если оно существует. - - Returns: - Dict[str, Any]: Словарь с атрибутами объекта - """ - column_names = filter(lambda x: x not in FILTERED_FIELDS, self.__table__.columns.keys()) - data = {} - try: - for column_name in column_names: - try: - # Проверяем, существует ли атрибут в объекте - if hasattr(self, column_name): - value = getattr(self, column_name) - # Проверяем, является ли значение JSON и декодируем его при необходимости - if isinstance(value, (str, bytes)) and isinstance( - self.__table__.columns[column_name].type, JSON - ): - try: - data[column_name] = orjson.loads(value) - except (TypeError, orjson.JSONDecodeError) as e: - logger.exception(f"Error decoding JSON for column '{column_name}': {e}") - data[column_name] = value - else: - data[column_name] = value - else: - # Пропускаем атрибут, если его нет в объекте (может быть добавлен после миграции) - logger.debug(f"Skipping missing attribute '{column_name}' for {self.__class__.__name__}") - except AttributeError as e: - logger.warning(f"Attribute error for column '{column_name}': {e}") - # Добавляем синтетическое поле .stat если оно существует - if hasattr(self, "stat"): - data["stat"] = self.stat - except Exception as e: - logger.exception(f"Error occurred while converting object to dictionary: {e}") - return data - - def update(self, values: builtins.dict[str, Any]) -> None: - for key, value in values.items(): - if hasattr(self, key): - setattr(self, key, value) - - # make_searchable(Base.metadata) # Base.metadata.create_all(bind=engine) @@ -326,7 +258,5 @@ def local_session(src: str = "") -> Session: return Session(bind=engine, expire_on_commit=False) -# Export Base for backward compatibility -Base = _Base # Also export the type for type hints -__all__ = ["Base", "BaseModel", "BaseType", "engine", "local_session"] +__all__ = ["engine", "local_session"] diff --git a/tests/auth/test_auth_service.py b/tests/auth/test_auth_service.py new file mode 100644 index 00000000..98f1a402 --- /dev/null +++ b/tests/auth/test_auth_service.py @@ -0,0 +1,34 @@ +import pytest +from services.auth import AuthService +from services.db import local_session +from auth.orm import Author + +@pytest.mark.asyncio +async def test_ensure_user_has_reader_role(): + auth_service = AuthService() + + # Создаем тестового пользователя без роли reader + with local_session() as session: + test_author = Author( + email="test_reader_role@example.com", + slug="test_reader_role", + password="test_password" + ) + session.add(test_author) + session.commit() + user_id = test_author.id + + # Проверяем, что роль reader добавляется + result = await auth_service.ensure_user_has_reader_role(user_id) + assert result is True + + # Проверяем, что при повторном вызове возвращается True + result = await auth_service.ensure_user_has_reader_role(user_id) + assert result is True + + # Очищаем тестовые данные + with local_session() as session: + test_author = session.query(Author).filter_by(id=user_id).first() + if test_author: + session.delete(test_author) + session.commit() diff --git a/tests/auth/test_identity.py b/tests/auth/test_identity.py new file mode 100644 index 00000000..a407f9d2 --- /dev/null +++ b/tests/auth/test_identity.py @@ -0,0 +1,13 @@ +import pytest +from auth.identity import Password + +def test_password_verify(): + # Создаем пароль + original_password = "test_password123" + hashed_password = Password.encode(original_password) + + # Проверяем корректный пароль + assert Password.verify(original_password, hashed_password) is True + + # Проверяем некорректный пароль + assert Password.verify("wrong_password", hashed_password) is False diff --git a/tests/auth/test_oauth.py b/tests/auth/test_oauth.py index b68f4a28..b220caa4 100644 --- a/tests/auth/test_oauth.py +++ b/tests/auth/test_oauth.py @@ -227,3 +227,51 @@ with ( assert created_user is not None assert created_user.name == "Test User" assert created_user.email_verified is True + +# Импортируем необходимые модели +from orm.community import Community, CommunityAuthor + +@pytest.fixture +def test_community(oauth_db_session, simple_user): + """ + Создает тестовое сообщество с ожидаемыми ролями по умолчанию + + Args: + oauth_db_session: Сессия базы данных для теста + simple_user: Пользователь для создания сообщества + + Returns: + Community: Созданное тестовое сообщество + """ + # Очищаем существующие записи + oauth_db_session.query(Community).filter( + (Community.id == 300) | (Community.slug == "test-oauth-community") + ).delete() + oauth_db_session.commit() + + # Создаем тестовое сообщество + community = Community( + id=300, + name="Test OAuth Community", + slug="test-oauth-community", + desc="Community for OAuth tests", + created_by=simple_user.id, + settings={ + "default_roles": ["reader", "author"], + "available_roles": ["reader", "author", "editor"] + } + ) + oauth_db_session.add(community) + oauth_db_session.commit() + + yield community + + # Очистка после теста + try: + oauth_db_session.query(CommunityAuthor).filter( + CommunityAuthor.community_id == community.id + ).delete() + oauth_db_session.query(Community).filter(Community.id == community.id).delete() + oauth_db_session.commit() + except Exception: + oauth_db_session.rollback() diff --git a/tests/auth/test_token_storage_fix.py b/tests/auth/test_token_storage_fix.py index 2b211d05..3ee87865 100644 --- a/tests/auth/test_token_storage_fix.py +++ b/tests/auth/test_token_storage_fix.py @@ -14,38 +14,45 @@ from auth.tokens.storage import TokenStorage async def test_token_storage(redis_client): """Тест базовой функциональности TokenStorage с правильными fixtures""" - print("✅ Тестирование TokenStorage...") + try: + print("✅ Тестирование TokenStorage...") - # Тест создания сессии - print("1. Создание сессии...") - token = await TokenStorage.create_session(user_id="test_user_123", username="test_user", device_info={"test": True}) - print(f" Создан токен: {token[:20]}...") + # Тест создания сессии + print("1. Создание сессии...") + token = await TokenStorage.create_session(user_id="test_user_123", username="test_user", device_info={"test": True}) + print(f" Создан токен: {token[:20]}...") - # Тест проверки сессии - print("2. Проверка сессии...") - session_data = await TokenStorage.verify_session(token) - if session_data: - print(f" Сессия найдена для user_id: {session_data.user_id}") - else: - print(" ❌ Сессия не найдена") - return False + # Тест проверки сессии + print("2. Проверка сессии...") + session_data = await TokenStorage.verify_session(token) + if session_data: + print(f" Сессия найдена для user_id: {session_data.user_id}") + else: + print(" ❌ Сессия не найдена") + return False - # Тест прямого использования SessionTokenManager - print("3. Прямое использование SessionTokenManager...") - sessions = SessionTokenManager() - valid, data = await sessions.validate_session_token(token) - print(f" Валидация: {valid}, данные: {bool(data)}") + # Тест прямого использования SessionTokenManager + print("3. Прямое использование SessionTokenManager...") + sessions = SessionTokenManager() + valid, data = await sessions.validate_session_token(token) + print(f" Валидация: {valid}, данные: {bool(data)}") - # Тест мониторинга - print("4. Мониторинг токенов...") - monitoring = TokenMonitoring() - stats = await monitoring.get_token_statistics() - print(f" Активных сессий: {stats.get('session_tokens', 0)}") + # Тест мониторинга + print("4. Мониторинг токенов...") + monitoring = TokenMonitoring() + stats = await monitoring.get_token_statistics() + print(f" Активных сессий: {stats.get('session_tokens', 0)}") - # Очистка - print("5. Отзыв сессии...") - revoked = await TokenStorage.revoke_session(token) - print(f" Отозван: {revoked}") + # Очистка + print("5. Отзыв сессии...") + revoked = await TokenStorage.revoke_session(token) + print(f" Отозван: {revoked}") - print("✅ Все тесты пройдены успешно!") - return True + print("✅ Все тесты пройдены успешно!") + return True + finally: + # Безопасное закрытие клиента с использованием aclose() + if hasattr(redis_client, 'aclose'): + await redis_client.aclose() + elif hasattr(redis_client, 'close'): + await redis_client.close() diff --git a/tests/conftest.py b/tests/conftest.py index 435034c8..f49f90ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool -from services.db import Base +from orm.base import BaseModel as Base from services.redis import redis from tests.test_config import get_test_client diff --git a/tests/test_rbac_integration.py b/tests/test_rbac_integration.py index 8dd2a096..3cb0528a 100644 --- a/tests/test_rbac_integration.py +++ b/tests/test_rbac_integration.py @@ -3,7 +3,7 @@ Проверяет работу AdminService и AuthService с RBAC системой. """ - +import logging import pytest from auth.orm import Author @@ -11,6 +11,8 @@ from orm.community import Community, CommunityAuthor from services.admin import admin_service from services.auth import auth_service +logger = logging.getLogger(__name__) + @pytest.fixture def simple_user(db_session): @@ -36,7 +38,7 @@ def simple_user(db_session): # Очистка после теста try: # Удаляем связанные записи CommunityAuthor - db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).delete() + db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).delete(synchronize_session=False) # Удаляем самого пользователя db_session.query(Author).filter(Author.id == user.id).delete() db_session.commit() @@ -48,17 +50,18 @@ def simple_user(db_session): def simple_community(db_session, simple_user): """Создает простое тестовое сообщество""" # Очищаем любые существующие записи с этим ID/slug - db_session.query(Community).filter( - (Community.id == 200) | (Community.slug == "simple-test-community") - ).delete() + db_session.query(Community).filter(Community.slug == "simple-test-community").delete() db_session.commit() community = Community( - id=200, name="Simple Test Community", slug="simple-test-community", desc="Simple community for tests", created_by=simple_user.id, + settings={ + "default_roles": ["reader", "author"], + "available_roles": ["reader", "author", "editor"] + } ) db_session.add(community) db_session.commit() @@ -76,6 +79,52 @@ def simple_community(db_session, simple_user): db_session.rollback() +@pytest.fixture +def test_community(db_session, simple_user): + """ + Создает тестовое сообщество с ожидаемыми ролями по умолчанию + + Args: + db_session: Сессия базы данных для теста + simple_user: Пользователь для создания сообщества + + Returns: + Community: Созданное тестовое сообщество + """ + # Очищаем существующие записи + db_session.query(Community).filter(Community.slug == "test-rbac-community").delete() + db_session.commit() + + community = Community( + name="Test RBAC Community", + slug="test-rbac-community", + desc="Community for RBAC tests", + created_by=simple_user.id, + settings={ + "default_roles": ["reader", "author"], + "available_roles": ["reader", "author", "editor"] + } + ) + db_session.add(community) + db_session.flush() # Получаем ID без коммита + + logger.info(f"DEBUG: Создание Community с айди {community.id}") + + db_session.commit() + + yield community + + # Очистка после теста + try: + # Удаляем связанные записи CommunityAuthor + db_session.query(CommunityAuthor).filter(CommunityAuthor.community_id == community.id).delete() + # Удаляем сообщество + db_session.query(Community).filter(Community.id == community.id).delete() + db_session.commit() + except Exception: + db_session.rollback() + + @pytest.fixture(autouse=True) def cleanup_test_users(db_session): """Автоматически очищает тестовые записи пользователей перед каждым тестом""" @@ -96,7 +145,7 @@ def cleanup_test_users(db_session): existing_user = db_session.query(Author).filter(Author.email == email).first() if existing_user: # Удаляем связанные записи CommunityAuthor - db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == existing_user.id).delete() + db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == existing_user.id).delete(synchronize_session=False) # Удаляем пользователя db_session.delete(existing_user) db_session.commit() @@ -154,70 +203,101 @@ class TestSimpleAdminService: # Может быть пустой список или содержать системную роль админа assert len(roles) >= 0 - def test_get_user_roles_with_roles(self, db_session, simple_user, simple_community): + def test_get_user_roles_with_roles(self, db_session, simple_user, test_community): """Тест получения ролей пользователя""" - # Используем дефолтное сообщество (ID=1) для совместимости с AdminService - default_community_id = 1 + # Используем тестовое сообщество + community_id = test_community.id - print(f"DEBUG: user_id={simple_user.id}, community_id={default_community_id}") + # Отладочная информация о тестовом сообществе + logger.info(f"DEBUG: Тестовое сообщество ID: {community_id}") + logger.info(f"DEBUG: Тестовое сообщество slug: {test_community.slug}") + logger.info(f"DEBUG: Тестовое сообщество settings: {test_community.settings}") + + # Полностью очищаем все существующие CommunityAuthor для пользователя + existing_community_authors = db_session.query(CommunityAuthor).filter( + CommunityAuthor.author_id == simple_user.id + ).all() + + # Отладочная информация + logger.info(f"DEBUG: Найдено существующих CommunityAuthor: {len(existing_community_authors)}") + for ca in existing_community_authors: + logger.info(f"DEBUG: Существующий CA - community_id: {ca.community_id}, roles: {ca.roles}") + db_session.delete(ca) - # Очищаем существующие роли - deleted_count = db_session.query(CommunityAuthor).filter( - CommunityAuthor.author_id == simple_user.id, - CommunityAuthor.community_id == default_community_id - ).delete() db_session.commit() - print(f"DEBUG: Удалено записей CommunityAuthor: {deleted_count}") - # Создаем CommunityAuthor с ролями в дефолтном сообществе + # Создаем CommunityAuthor с ролями в тестовом сообществе ca = CommunityAuthor( - community_id=default_community_id, + community_id=community_id, author_id=simple_user.id, ) + + # Расширенная отладка перед set_roles + logger.info(f"DEBUG: Перед set_roles") + logger.info(f"DEBUG: ca.roles до set_roles: {ca.roles}") + logger.info(f"DEBUG: ca.role_list до set_roles: {ca.role_list}") + ca.set_roles(["reader", "author"]) - print(f"DEBUG: Установлены роли: {ca.role_list}") + + # Расширенная отладка после set_roles + logger.info(f"DEBUG: После set_roles") + logger.info(f"DEBUG: ca.roles после set_roles: {ca.roles}") + logger.info(f"DEBUG: ca.role_list после set_roles: {ca.role_list}") + db_session.add(ca) db_session.commit() - print(f"DEBUG: CA сохранен в БД с ID: {ca.id}") - # Проверяем что роли сохранились в БД - saved_ca = db_session.query(CommunityAuthor).filter( + # Явная проверка сохранения CommunityAuthor + check_ca = db_session.query(CommunityAuthor).filter( CommunityAuthor.author_id == simple_user.id, - CommunityAuthor.community_id == default_community_id + CommunityAuthor.community_id == community_id ).first() - assert saved_ca is not None - print(f"DEBUG: Сохраненные роли в БД: {saved_ca.role_list}") - assert "reader" in saved_ca.role_list - assert "author" in saved_ca.role_list - # Проверяем роли через AdminService (использует дефолтное сообщество) + logger.info(f"DEBUG: Проверка сохраненной записи CommunityAuthor") + logger.info(f"DEBUG: Найденная запись: {check_ca}") + logger.info(f"DEBUG: Роли в найденной записи: {check_ca.roles}") + logger.info(f"DEBUG: role_list найденной записи: {check_ca.role_list}") + + assert check_ca is not None, "CommunityAuthor должен быть сохранен в базе данных" + assert check_ca.roles is not None, "Роли CommunityAuthor не должны быть None" + assert "reader" in check_ca.role_list, "Роль 'reader' должна быть в role_list" + assert "author" in check_ca.role_list, "Роль 'author' должна быть в role_list" + + # Проверяем роли через AdminService + from services.admin import admin_service + from services.db import local_session + + # Используем ту же сессию для проверки fresh_user = db_session.query(Author).filter(Author.id == simple_user.id).first() - roles = admin_service.get_user_roles(fresh_user) # Без указания community_id - использует дефолт - print(f"DEBUG: AdminService вернул роли: {roles}") - assert "reader" in roles - assert "author" in roles + roles = admin_service.get_user_roles(fresh_user, community_id) + + # Проверяем роли + assert isinstance(roles, list), "Роли должны быть списком" + assert "reader" in roles, "Роль 'reader' должна присутствовать" + assert "author" in roles, "Роль 'author' должна присутствовать" + assert len(roles) == 2, f"Должно быть 2 роли, а не {len(roles)}" def test_update_user_success(self, db_session, simple_user): """Тест успешного обновления пользователя""" - original_name = simple_user.name + from services.admin import admin_service - user_data = { + # Обновляем пользователя + result = admin_service.update_user({ "id": simple_user.id, - "email": simple_user.email, "name": "Updated Name", - "roles": ["reader"] - } + "email": simple_user.email + }) - result = admin_service.update_user(user_data) - assert result["success"] is True + # Проверяем обновленного пользователя + assert result is not None, "Пользователь должен быть обновлен" + assert result.get("name") == "Updated Name", "Имя пользователя должно быть обновлено" - # Получаем обновленного пользователя из БД заново - updated_user = db_session.query(Author).filter(Author.id == simple_user.id).first() - assert updated_user.name == "Updated Name" - - # Восстанавливаем исходное имя для других тестов - updated_user.name = original_name - db_session.commit() + # Восстанавливаем исходное имя + admin_service.update_user({ + "id": simple_user.id, + "name": "Simple User", + "email": simple_user.email + }) class TestSimpleAuthService: @@ -227,12 +307,15 @@ class TestSimpleAuthService: """Тест базового создания пользователя""" test_email = "test_create_unique@example.com" - # Удаляем пользователя если существует - existing = db_session.query(Author).filter(Author.email == test_email).first() - if existing: - db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == existing.id).delete() - db_session.delete(existing) - db_session.commit() + # Найдем существующих пользователей с таким email + existing_users = db_session.query(Author).filter(Author.email == test_email).all() + + # Удаляем связанные записи CommunityAuthor для существующих пользователей + for user in existing_users: + db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).delete(synchronize_session=False) + db_session.delete(user) + + db_session.commit() user_dict = { "email": test_email, @@ -247,37 +330,102 @@ class TestSimpleAuthService: assert user.name == "Test Create User" # Очистка - db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).delete() - db_session.delete(user) - db_session.commit() - - def test_create_user_with_community(self, db_session, simple_community): - """Тест создания пользователя с привязкой к сообществу""" - test_email = "test_community_unique@example.com" - - # Удаляем пользователя если существует - existing = db_session.query(Author).filter(Author.email == test_email).first() - if existing: - db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == existing.id).delete() - db_session.delete(existing) + try: + db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).delete(synchronize_session=False) + db_session.delete(user) db_session.commit() + except Exception as e: + # Если возникла ошибка при удалении, просто логируем ее + print(f"Ошибка при очистке: {e}") + db_session.rollback() + + def test_create_user_with_community(self, db_session): + """Проверяем создание пользователя в конкретном сообществе""" + from services.auth import auth_service + from services.rbac import initialize_community_permissions + from auth.orm import Author + import asyncio + import uuid + + # Создаем тестового пользователя + system_author = db_session.query(Author).filter(Author.slug == "system").first() + if not system_author: + system_author = Author( + name="System", + slug="system", + email="system@test.local" + ) + db_session.add(system_author) + db_session.flush() + + # Создаем тестовое сообщество + unique_slug = f"simple-test-community-{uuid.uuid4()}" + community = Community( + name="Simple Test Community", + slug=unique_slug, + desc="Simple community for tests", + created_by=system_author.id, + settings={ + "default_roles": ["reader", "author"], + "available_roles": ["reader", "author", "editor"] + } + ) + db_session.add(community) + db_session.flush() + + # Инициализируем права сообщества + async def init_community_permissions(): + await initialize_community_permissions(community.id) + + # Запускаем инициализацию в текущем event loop + loop = asyncio.get_event_loop() + loop.run_until_complete(init_community_permissions()) + + # Генерируем уникальные данные для каждого теста + unique_email = f"test_community_unique_{uuid.uuid4()}@example.com" + unique_name = f"Test Community User {uuid.uuid4()}" + unique_slug = f"test-community-user-{uuid.uuid4()}" user_dict = { - "email": test_email, - "name": "Test Community User", - "slug": "test-community-user-unique", + "name": unique_name, + "email": unique_email, + "slug": unique_slug } - user = auth_service.create_user(user_dict, community_id=simple_community.id) + # Создаем пользователя в конкретном сообществе + user = auth_service.create_user(user_dict, community_id=community.id) - assert user is not None - assert user.email == test_email + # Проверяем созданного пользователя + assert user is not None, "Пользователь должен быть создан" + assert user.email == unique_email.lower(), "Email должен быть в нижнем регистре" + assert user.name == unique_name, "Имя пользователя должно совпадать" + assert user.slug == unique_slug, "Slug пользователя должен совпадать" - # Очистка - db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).delete() - db_session.delete(user) + # Проверяем роли + from orm.community import get_user_roles_in_community + + # Получаем роли + roles = get_user_roles_in_community(user.id, community_id=community.id) + + # Проверяем роли + assert "reader" in roles, f"У нового пользователя должна быть роль 'reader' в сообществе {community.id}. Текущие роли: {roles}" + assert "author" in roles, f"У нового пользователя должна быть роль 'author' в сообществе {community.id}. Текущие роли: {roles}" + + # Коммитим изменения db_session.commit() + # Очищаем созданные объекты + try: + # Удаляем связанные записи CommunityAuthor + db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).delete() + # Удаляем пользователя + db_session.query(Author).filter(Author.id == user.id).delete() + # Удаляем сообщество + db_session.query(Community).filter(Community.id == community.id).delete() + db_session.commit() + except Exception: + db_session.rollback() + class TestCommunityAuthorMethods: """Тесты методов CommunityAuthor"""