tested-auth-refactoring
Some checks failed
Deploy on push / deploy (push) Failing after 5s

This commit is contained in:
2025-07-25 01:04:15 +03:00
parent 867232e48f
commit b60a314ddd
28 changed files with 975 additions and 523 deletions

View File

@@ -62,7 +62,6 @@ repos:
# additional_dependencies: [
# "types-redis",
# "types-requests",
# "types-passlib",
# "types-Authlib",
# "sqlalchemy[mypy]"
# ]

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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"]
# Создаем CommunityAuthor с дефолтными ролями
community_author = CommunityAuthor(

100
orm/base.py Normal file
View File

@@ -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)

View File

@@ -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):

View File

@@ -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]:
"""

View File

@@ -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="")

View File

@@ -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)

View File

@@ -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):
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):
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)

View File

@@ -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):

View File

@@ -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="")

View File

@@ -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()))

482
package-lock.json generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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,
},
}
)

View File

@@ -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"]
# Создаем CommunityAuthor с дефолтными ролями
community_author = CommunityAuthor(

View File

@@ -72,19 +72,30 @@ 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:
# Проверяем, что roles не None и не пустая строка
if community_author.roles is not None and community_author.roles.strip():
user_roles = community_author.role_list
# Добавляем синтетическую роль для системных админов
@@ -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),
}
# === ПУБЛИКАЦИИ ===

View File

@@ -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

View File

@@ -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"]

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -14,6 +14,7 @@ from auth.tokens.storage import TokenStorage
async def test_token_storage(redis_client):
"""Тест базовой функциональности TokenStorage с правильными fixtures"""
try:
print("✅ Тестирование TokenStorage...")
# Тест создания сессии
@@ -49,3 +50,9 @@ async def test_token_storage(redis_client):
print("Все тесты пройдены успешно!")
return True
finally:
# Безопасное закрытие клиента с использованием aclose()
if hasattr(redis_client, 'aclose'):
await redis_client.aclose()
elif hasattr(redis_client, 'close'):
await redis_client.close()

View File

@@ -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

View File

@@ -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,11 +307,14 @@ 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)
# Найдем существующих пользователей с таким 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 = {
@@ -247,37 +330,102 @@ class TestSimpleAuthService:
assert user.name == "Test Create User"
# Очистка
db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).delete()
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, simple_community):
"""Тест создания пользователя с привязкой к сообществу"""
test_email = "test_community_unique@example.com"
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
# Удаляем пользователя если существует
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()
# Создаем тестового пользователя
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"""