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: [ # additional_dependencies: [
# "types-redis", # "types-redis",
# "types-requests", # "types-requests",
# "types-passlib",
# "types-Authlib", # "types-Authlib",
# "sqlalchemy[mypy]" # "sqlalchemy[mypy]"
# ] # ]

View File

@@ -1,5 +1,32 @@
# Changelog # 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 ## [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 - `settings` moved to base and now smaller
- new outside auth schema - new outside auth schema
- removed `gittask`, `auth`, `inbox`, `migration` - 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 alembic import context
from services.db import Base from orm.base import BaseModel as Base
from settings import DB_URL from settings import DB_URL
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides

View File

@@ -2,7 +2,7 @@ from binascii import hexlify
from hashlib import sha256 from hashlib import sha256
from typing import TYPE_CHECKING, Any, TypeVar from typing import TYPE_CHECKING, Any, TypeVar
from passlib.hash import bcrypt import bcrypt
from auth.exceptions import ExpiredToken, InvalidPassword, InvalidToken from auth.exceptions import ExpiredToken, InvalidPassword, InvalidToken
from auth.jwtcodec import JWTCodec from auth.jwtcodec import JWTCodec
@@ -39,7 +39,8 @@ class Password:
str: Закодированный пароль str: Закодированный пароль
""" """
password_sha256 = Password._get_sha256(password) 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 @staticmethod
def verify(password: str, hashed: str) -> bool: def verify(password: str, hashed: str) -> bool:
@@ -61,7 +62,7 @@ class Password:
hashed_bytes = Password._to_bytes(hashed) hashed_bytes = Password._to_bytes(hashed)
password_sha256 = Password._get_sha256(password) password_sha256 = Password._get_sha256(password)
return bcrypt.verify(password_sha256, hashed_bytes) return bcrypt.checkpw(password_sha256, hashed_bytes) # Изменил verify на checkpw
class Identity: 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() community = session.query(Community).filter(Community.id == target_community_id).first()
if community: 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() default_roles = community.get_default_roles()
if not default_roles:
default_roles = ["reader", "author"]
except AttributeError:
default_roles = ["reader", "author"]
# Создаем CommunityAuthor с дефолтными ролями # Создаем CommunityAuthor с дефолтными ролями
community_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 import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from services.db import BaseModel as Base from orm.base import BaseModel as Base
class ShoutCollection(Base): class ShoutCollection(Base):

View File

@@ -1,12 +1,12 @@
import time import time
from typing import Any, Dict 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.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from auth.orm import Author from auth.orm import Author
from services.db import BaseModel from orm.base import BaseModel
from services.rbac import get_permissions_for_role from services.rbac import get_permissions_for_role
# Словарь названий ролей # Словарь названий ролей
@@ -372,7 +372,7 @@ class CommunityAuthor(BaseModel):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
community_id = Column(Integer, ForeignKey("community.id"), nullable=False) community_id = Column(Integer, ForeignKey("community.id"), nullable=False)
author_id = Column(Integer, ForeignKey("author.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())) 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: def set_roles(self, roles: list[str]) -> None:
""" """
Устанавливает полный список ролей (заменяет текущие) Устанавливает роли для CommunityAuthor.
Args: Args:
roles: Список ролей для установки 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]: 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 sqlalchemy.orm import relationship
from auth.orm import Author from auth.orm import Author
from orm.base import BaseModel as Base
from orm.topic import Topic from orm.topic import Topic
from services.db import BaseModel as Base
class DraftTopic(Base): class DraftTopic(Base):
__tablename__ = "draft_topic" __tablename__ = "draft_topic"
id = None # type: ignore id = None # type: ignore[misc]
shout = Column(ForeignKey("draft.id"), primary_key=True, index=True) shout = Column(ForeignKey("draft.id"), primary_key=True, index=True)
topic = Column(ForeignKey("topic.id"), primary_key=True, index=True) topic = Column(ForeignKey("topic.id"), primary_key=True, index=True)
main = Column(Boolean, nullable=True) main = Column(Boolean, nullable=True)
@@ -20,7 +20,7 @@ class DraftTopic(Base):
class DraftAuthor(Base): class DraftAuthor(Base):
__tablename__ = "draft_author" __tablename__ = "draft_author"
id = None # type: ignore id = None # type: ignore[misc]
shout = Column(ForeignKey("draft.id"), primary_key=True, index=True) shout = Column(ForeignKey("draft.id"), primary_key=True, index=True)
author = Column(ForeignKey("author.id"), primary_key=True, index=True) author = Column(ForeignKey("author.id"), primary_key=True, index=True)
caption = Column(String, nullable=True, default="") caption = Column(String, nullable=True, default="")

View File

@@ -3,7 +3,7 @@ import enum
from sqlalchemy import Column, ForeignKey, String from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from services.db import BaseModel as Base from orm.base import BaseModel as Base
class InviteStatus(enum.Enum): class InviteStatus(enum.Enum):
@@ -12,7 +12,7 @@ class InviteStatus(enum.Enum):
REJECTED = "REJECTED" REJECTED = "REJECTED"
@classmethod @classmethod
def from_string(cls, value): def from_string(cls, value: str) -> "Invite":
return cls(value) return cls(value)

View File

@@ -1,35 +1,120 @@
import enum 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 sqlalchemy.orm import relationship
from auth.orm import Author from orm.author import Author
from services.db import BaseModel as Base 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): class NotificationEntity(enum.Enum):
REACTION = "reaction" """Сущности, связанные с уведомлениями."""
TOPIC = "topic"
COMMENT = "comment"
SHOUT = "shout" SHOUT = "shout"
FOLLOWER = "follower" AUTHOR = "author"
COMMUNITY = "community" COMMUNITY = "community"
@classmethod @classmethod
def from_string(cls, value): def from_string(cls, value: str) -> "NotificationEntity":
"""
Создает экземпляр сущности уведомления из строки.
Args:
value (str): Строковое представление сущности.
Returns:
NotificationEntity: Экземпляр сущности уведомления.
"""
try:
return cls(value) return cls(value)
except ValueError:
logger.error(f"Неверная сущность уведомления: {value}")
raise ValueError("Неверная сущность уведомления") # noqa: B904
class NotificationAction(enum.Enum): class NotificationAction(enum.Enum):
"""Действия в уведомлениях."""
CREATE = "create" CREATE = "create"
UPDATE = "update" UPDATE = "update"
DELETE = "delete" DELETE = "delete"
SEEN = "seen" MENTION = "mention"
FOLLOW = "follow" REACT = "react"
UNFOLLOW = "unfollow"
@classmethod @classmethod
def from_string(cls, value): def from_string(cls, value: str) -> "NotificationAction":
"""
Создает экземпляр действия уведомления из строки.
Args:
value (str): Строковое представление действия.
Returns:
NotificationAction: Экземпляр действия уведомления.
"""
try:
return cls(value) return cls(value)
except ValueError:
logger.error(f"Неверное действие уведомления: {value}")
raise ValueError("Неверное действие уведомления") # noqa: B904
class NotificationSeen(Base): class NotificationSeen(Base):
@@ -42,22 +127,31 @@ class NotificationSeen(Base):
class Notification(Base): class Notification(Base):
__tablename__ = "notification" __tablename__ = "notification"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, index=True)
created_at = Column(Integer, server_default=str(int(time.time()))) created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
entity = Column(String, nullable=False) entity = Column(String, nullable=False)
action = Column(String, nullable=False) action = Column(String, nullable=False)
payload = Column(JSON, nullable=True) payload = Column(JSON, nullable=True)
status = Column(SQLAlchemyEnum(NotificationStatus), default=NotificationStatus.UNREAD)
kind = Column(SQLAlchemyEnum(NotificationKind), nullable=False)
seen = relationship(Author, secondary="notification_seen") seen = relationship(Author, secondary="notification_seen")
def set_entity(self, entity: NotificationEntity): def set_entity(self, entity: NotificationEntity):
self.entity = entity.value # type: ignore[assignment] """Устанавливает сущность уведомления."""
self.entity = entity.value
def get_entity(self) -> NotificationEntity: def get_entity(self) -> NotificationEntity:
"""Возвращает сущность уведомления."""
return NotificationEntity.from_string(self.entity) return NotificationEntity.from_string(self.entity)
def set_action(self, action: NotificationAction): def set_action(self, action: NotificationAction):
self.action = action.value # type: ignore[assignment] """Устанавливает действие уведомления."""
self.action = action.value
def get_action(self) -> NotificationAction: def get_action(self) -> NotificationAction:
"""Возвращает действие уведомления."""
return NotificationAction.from_string(self.action) 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 sqlalchemy import Column, ForeignKey, Integer, String
from services.db import BaseModel as Base from orm.base import BaseModel as Base
class ReactionKind(Enumeration): 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 sqlalchemy.orm import relationship
from auth.orm import Author from auth.orm import Author
from orm.base import BaseModel as Base
from orm.reaction import Reaction from orm.reaction import Reaction
from orm.topic import Topic from orm.topic import Topic
from services.db import BaseModel as Base
class ShoutTopic(Base): class ShoutTopic(Base):
@@ -21,7 +21,7 @@ class ShoutTopic(Base):
__tablename__ = "shout_topic" __tablename__ = "shout_topic"
id = None # type: ignore id = None # type: ignore[misc]
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
topic = Column(ForeignKey("topic.id"), primary_key=True, index=True) topic = Column(ForeignKey("topic.id"), primary_key=True, index=True)
main = Column(Boolean, nullable=True) main = Column(Boolean, nullable=True)
@@ -36,7 +36,7 @@ class ShoutTopic(Base):
class ShoutReactionsFollower(Base): class ShoutReactionsFollower(Base):
__tablename__ = "shout_reactions_followers" __tablename__ = "shout_reactions_followers"
id = None # type: ignore id = None # type: ignore[misc]
follower = Column(ForeignKey("author.id"), primary_key=True, index=True) follower = Column(ForeignKey("author.id"), primary_key=True, index=True)
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
auto = Column(Boolean, nullable=False, default=False) auto = Column(Boolean, nullable=False, default=False)
@@ -56,7 +56,7 @@ class ShoutAuthor(Base):
__tablename__ = "shout_author" __tablename__ = "shout_author"
id = None # type: ignore id = None # type: ignore[misc]
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True) shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
author = Column(ForeignKey("author.id"), primary_key=True, index=True) author = Column(ForeignKey("author.id"), primary_key=True, index=True)
caption = Column(String, nullable=True, default="") 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 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): class TopicFollower(Base):
@@ -18,7 +18,7 @@ class TopicFollower(Base):
__tablename__ = "topic_followers" __tablename__ = "topic_followers"
id = None # type: ignore id = None # type: ignore[misc]
follower = Column(Integer, ForeignKey("author.id"), primary_key=True) follower = Column(Integer, ForeignKey("author.id"), primary_key=True)
topic = Column(Integer, ForeignKey("topic.id"), primary_key=True) topic = Column(Integer, ForeignKey("topic.id"), primary_key=True)
created_at = Column(Integer, nullable=False, default=int(time.time())) 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" "@solidjs/router": "^0.15.3"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.0.6", "@biomejs/biome": "^2.1.2",
"@graphql-codegen/cli": "^5.0.7", "@graphql-codegen/cli": "^5.0.7",
"@graphql-codegen/client-preset": "^4.8.3", "@graphql-codegen/client-preset": "^4.8.3",
"@graphql-codegen/typescript": "^4.0.6", "@graphql-codegen/typescript": "^4.1.6",
"@graphql-codegen/typescript-operations": "^4.2.0", "@graphql-codegen/typescript-operations": "^4.6.1",
"@graphql-codegen/typescript-resolvers": "^4.0.6", "@graphql-codegen/typescript-resolvers": "^4.5.1",
"@types/node": "^24.0.7", "@types/node": "^24.1.0",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"lightningcss": "^1.30.0", "lightningcss": "^1.30.1",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"solid-js": "^1.9.7", "solid-js": "^1.9.7",
"terser": "^5.39.0", "terser": "^5.43.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.0.0", "vite": "^7.0.6",
"vite-plugin-solid": "^2.11.7" "vite-plugin-solid": "^2.11.7"
} }
}, },
@@ -242,14 +242,14 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.27.6", "version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
"integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.27.2", "@babel/template": "^7.27.2",
"@babel/types": "^7.27.6" "@babel/types": "^7.28.2"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -304,9 +304,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.27.6", "version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -348,9 +348,9 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.28.1", "version": "7.28.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -569,9 +569,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
"integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -586,9 +586,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
"integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -603,9 +603,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
"integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -620,9 +620,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
"integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -637,9 +637,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
"integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -654,9 +654,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
"integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -671,9 +671,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
"integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -688,9 +688,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
"integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -705,9 +705,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
"integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -722,9 +722,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
"integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -739,9 +739,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
"integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -756,9 +756,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
"integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -773,9 +773,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
"integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -790,9 +790,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
"integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -807,9 +807,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
"integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -824,9 +824,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
"integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -841,9 +841,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
"integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -858,9 +858,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
"integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -875,9 +875,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
"integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -892,9 +892,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
"integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -909,9 +909,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
"integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -926,9 +926,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
"integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -943,9 +943,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
"integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -960,9 +960,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
"integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -977,9 +977,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
"integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -994,9 +994,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
"integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1405,13 +1405,13 @@
} }
}, },
"node_modules/@graphql-tools/apollo-engine-loader": { "node_modules/@graphql-tools/apollo-engine-loader": {
"version": "8.0.21", "version": "8.0.22",
"resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.21.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-8.0.22.tgz",
"integrity": "sha512-3o63uKvx2d/01GhR8Q4RACIScJG7SxliU+xxPVaC6SWpsRkvfHKXJITWctNIw5PBH5HiB25sL9a5AFHCQp0OEQ==", "integrity": "sha512-ssD2wNxeOTRcUEkuGcp0KfZAGstL9YLTe/y3erTDZtOs2wL1TJESw8NVAp+3oUHPeHKBZQB4Z6RFEbPgMdT2wA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"@whatwg-node/fetch": "^0.10.0", "@whatwg-node/fetch": "^0.10.0",
"sync-fetch": "0.6.0-2", "sync-fetch": "0.6.0-2",
"tslib": "^2.4.0" "tslib": "^2.4.0"
@@ -1424,13 +1424,13 @@
} }
}, },
"node_modules/@graphql-tools/batch-execute": { "node_modules/@graphql-tools/batch-execute": {
"version": "9.0.17", "version": "9.0.18",
"resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.17.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.18.tgz",
"integrity": "sha512-i7BqBkUP2+ex8zrQrCQTEt6nYHQmIey9qg7CMRRa1hXCY2X8ZCVjxsvbsi7gOLwyI/R3NHxSRDxmzZevE2cPLg==", "integrity": "sha512-KtBglqPGR/3CZtQevFRBBc6MJpIgxBqfCrUV5sdC3oJsafmPShgr+lxM178SW5i1QHmiVAScOWGWqWp9HbnpoQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/utils": "^10.8.1", "@graphql-tools/utils": "^10.9.0",
"@whatwg-node/promise-helpers": "^1.3.0", "@whatwg-node/promise-helpers": "^1.3.0",
"dataloader": "^2.2.3", "dataloader": "^2.2.3",
"tslib": "^2.8.1" "tslib": "^2.8.1"
@@ -1443,14 +1443,14 @@
} }
}, },
"node_modules/@graphql-tools/code-file-loader": { "node_modules/@graphql-tools/code-file-loader": {
"version": "8.1.21", "version": "8.1.22",
"resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.21.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-8.1.22.tgz",
"integrity": "sha512-NmHEijQ9uEPcM5riM3NsQcT2piESgV2QX6/pIcKineBXQ/2nbeKtxOqWi2omCVLHSKmjOlR1Yyn3E2alqWVOxg==", "integrity": "sha512-FSka29kqFkfFmw36CwoQ+4iyhchxfEzPbXOi37lCEjWLHudGaPkXc3RyB9LdmBxx3g3GHEu43a5n5W8gfcrMdA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/graphql-tag-pluck": "8.3.20", "@graphql-tools/graphql-tag-pluck": "8.3.21",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"globby": "^11.0.3", "globby": "^11.0.3",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"unixify": "^1.0.0" "unixify": "^1.0.0"
@@ -1463,16 +1463,16 @@
} }
}, },
"node_modules/@graphql-tools/delegate": { "node_modules/@graphql-tools/delegate": {
"version": "10.2.21", "version": "10.2.22",
"resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.21.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.22.tgz",
"integrity": "sha512-YLyyuhxrZniVufZV/6Oba5xIvWqVRyZrO8LsM+hI4Q6/aR1OdJafi9IBqCE2hUDPfIc8wkhqixA2/WT+oApY3g==", "integrity": "sha512-1jkTF5DIhO1YJ0dlgY03DZYAiSwlu5D2mdjeq+f6oyflyKG9E4SPmkLgVdDSNSfGxFHHrjIvYjUhPYV0vAOiDg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/batch-execute": "^9.0.17", "@graphql-tools/batch-execute": "^9.0.18",
"@graphql-tools/executor": "^1.4.7", "@graphql-tools/executor": "^1.4.8",
"@graphql-tools/schema": "^10.0.11", "@graphql-tools/schema": "^10.0.24",
"@graphql-tools/utils": "^10.8.1", "@graphql-tools/utils": "^10.9.0",
"@repeaterjs/repeater": "^3.0.6", "@repeaterjs/repeater": "^3.0.6",
"@whatwg-node/promise-helpers": "^1.3.0", "@whatwg-node/promise-helpers": "^1.3.0",
"dataloader": "^2.2.3", "dataloader": "^2.2.3",
@@ -1504,13 +1504,13 @@
} }
}, },
"node_modules/@graphql-tools/executor": { "node_modules/@graphql-tools/executor": {
"version": "1.4.8", "version": "1.4.9",
"resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.4.8.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.4.9.tgz",
"integrity": "sha512-eMFWo30+L8BPME5qhJ3b4WOEAMSIMdi41F0afp40RH9RWQWnJ9R9Tr6vq7CZzmlM8qxymEE4UMAnu2qG/5Jyqg==", "integrity": "sha512-SAUlDT70JAvXeqV87gGzvDzUGofn39nvaVcVhNf12Dt+GfWHtNNO/RCn/Ea4VJaSLGzraUd41ObnN3i80EBU7w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"@graphql-typed-document-node/core": "^3.2.0", "@graphql-typed-document-node/core": "^3.2.0",
"@repeaterjs/repeater": "^3.0.4", "@repeaterjs/repeater": "^3.0.4",
"@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/disposablestack": "^0.0.6",
@@ -1542,19 +1542,36 @@
} }
}, },
"node_modules/@graphql-tools/executor-graphql-ws": { "node_modules/@graphql-tools/executor-graphql-ws": {
"version": "2.0.5", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.5.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.6.tgz",
"integrity": "sha512-gI/D9VUzI1Jt1G28GYpvm5ckupgJ5O8mi5Y657UyuUozX34ErfVdZ81g6oVcKFQZ60LhCzk7jJeykK48gaLhDw==", "integrity": "sha512-hLmY+h1HDM4+y4EXP0SgNFd6hXEs4LCMAxvvdfPAwrzHNM04B0wnlcOi8Rze3e7AA9edxXQsm3UN4BE04U2OMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/executor-common": "^0.0.4", "@graphql-tools/executor-common": "^0.0.5",
"@graphql-tools/utils": "^10.8.1", "@graphql-tools/utils": "^10.9.0",
"@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/disposablestack": "^0.0.6",
"graphql-ws": "^6.0.3", "graphql-ws": "^6.0.6",
"isomorphic-ws": "^5.0.0", "isomorphic-ws": "^5.0.0",
"tslib": "^2.8.1", "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": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@@ -1588,13 +1605,13 @@
} }
}, },
"node_modules/@graphql-tools/executor-legacy-ws": { "node_modules/@graphql-tools/executor-legacy-ws": {
"version": "1.1.18", "version": "1.1.19",
"resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.18.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.19.tgz",
"integrity": "sha512-KCsf4e3t/TyT06GeXEbWW08tbN+/uYOhFDU7RRMP4S1zIVIsIcdFmCjemBtrYDu93mwib63NidGX+mQXm1tmLg==", "integrity": "sha512-bEbv/SlEdhWQD0WZLUX1kOenEdVZk1yYtilrAWjRUgfHRZoEkY9s+oiqOxnth3z68wC2MWYx7ykkS5hhDamixg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"@types/ws": "^8.0.0", "@types/ws": "^8.0.0",
"isomorphic-ws": "^5.0.0", "isomorphic-ws": "^5.0.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
@@ -1608,14 +1625,14 @@
} }
}, },
"node_modules/@graphql-tools/git-loader": { "node_modules/@graphql-tools/git-loader": {
"version": "8.0.25", "version": "8.0.26",
"resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.25.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-8.0.26.tgz",
"integrity": "sha512-Zp9GtGfbnqwaFCUYQmTzJ3uKDgvHQfkaYSAQp+ZBKUrKu/m/TG6oxoy6duFYKujh7+fB0fhHYPJXdkGTSemBHA==", "integrity": "sha512-0g+9eng8DaT4ZmZvUmPgjLTgesUa6M8xrDjNBltRldZkB055rOeUgJiKmL6u8PjzI5VxkkVsn0wtAHXhDI2UXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/graphql-tag-pluck": "8.3.20", "@graphql-tools/graphql-tag-pluck": "8.3.21",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"is-glob": "4.0.3", "is-glob": "4.0.3",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"tslib": "^2.4.0", "tslib": "^2.4.0",
@@ -1629,15 +1646,15 @@
} }
}, },
"node_modules/@graphql-tools/github-loader": { "node_modules/@graphql-tools/github-loader": {
"version": "8.0.21", "version": "8.0.22",
"resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.21.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-8.0.22.tgz",
"integrity": "sha512-bXy8XDRz8YqMLZM7s6XW6eeADCjyAvlyUENBwP3pN9AyTh6xN61EHruFLbaMaGnQOlKITohxFM4mrrcRWJ1Iog==", "integrity": "sha512-uQ4JNcNPsyMkTIgzeSbsoT9hogLjYrZooLUYd173l5eUGUi49EAcsGdiBCKaKfEjanv410FE8hjaHr7fjSRkJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/executor-http": "^1.1.9", "@graphql-tools/executor-http": "^1.1.9",
"@graphql-tools/graphql-tag-pluck": "^8.3.20", "@graphql-tools/graphql-tag-pluck": "^8.3.21",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"@whatwg-node/fetch": "^0.10.0", "@whatwg-node/fetch": "^0.10.0",
"@whatwg-node/promise-helpers": "^1.0.0", "@whatwg-node/promise-helpers": "^1.0.0",
"sync-fetch": "0.6.0-2", "sync-fetch": "0.6.0-2",
@@ -1651,14 +1668,14 @@
} }
}, },
"node_modules/@graphql-tools/graphql-file-loader": { "node_modules/@graphql-tools/graphql-file-loader": {
"version": "8.0.21", "version": "8.0.22",
"resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.21.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.22.tgz",
"integrity": "sha512-E11KcRIIM6W04mDV95kx7SDrbqVD58jP3O1227JfBddzOx5q5Rb2b/1Sxw1+eNnGZT+xdT/506SrJ5dhLtwUrA==", "integrity": "sha512-KFUbjXgWr5+w/AioOuIuULy4LwcyDuQqTRFQGe+US1d9Z4+ZopcJLwsJTqp5B+icDkCqld4paN0y0qi9MrIvbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/import": "7.0.20", "@graphql-tools/import": "7.0.21",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"globby": "^11.0.3", "globby": "^11.0.3",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"unixify": "^1.0.0" "unixify": "^1.0.0"
@@ -1671,9 +1688,9 @@
} }
}, },
"node_modules/@graphql-tools/graphql-tag-pluck": { "node_modules/@graphql-tools/graphql-tag-pluck": {
"version": "8.3.20", "version": "8.3.21",
"resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.20.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-8.3.21.tgz",
"integrity": "sha512-HBukyPzrS3GyWkBkB/vblN+Fhb+tBKWL9rEHaexxTU+J8YHkXHAYlLvu56NXcCBzpVGWP2ghJqPh+ZPaqaiThQ==", "integrity": "sha512-TJhELNvR1tmghXMi6HVKp/Swxbx1rcSp/zdkuJZT0DCM3vOY11FXY6NW3aoxumcuYDNN3jqXcCPKstYGFPi5GQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1682,7 +1699,7 @@
"@babel/plugin-syntax-import-assertions": "^7.26.0", "@babel/plugin-syntax-import-assertions": "^7.26.0",
"@babel/traverse": "^7.26.10", "@babel/traverse": "^7.26.10",
"@babel/types": "^7.26.10", "@babel/types": "^7.26.10",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"engines": { "engines": {
@@ -1693,13 +1710,13 @@
} }
}, },
"node_modules/@graphql-tools/import": { "node_modules/@graphql-tools/import": {
"version": "7.0.20", "version": "7.0.21",
"resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.20.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.21.tgz",
"integrity": "sha512-Mz+1hBRnQYr4R5hdxc0to//v7V0OsBZH8BHbZgKvM5ayIBFl3+ArQFlfitukmrvZLmmi7UwordW3RG2yLjSx8A==", "integrity": "sha512-bcAqNWm/gLVEOy55o/WdaROERpDyUEmIfZ9E6NDjVk1ZGWfZe47+RgriTV80j6J5S5J1g+6loFkVWGAMqdN06g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"@theguild/federation-composition": "^0.19.0", "@theguild/federation-composition": "^0.19.0",
"resolve-from": "5.0.0", "resolve-from": "5.0.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"
@@ -1712,13 +1729,13 @@
} }
}, },
"node_modules/@graphql-tools/json-file-loader": { "node_modules/@graphql-tools/json-file-loader": {
"version": "8.0.19", "version": "8.0.20",
"resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.19.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.20.tgz",
"integrity": "sha512-msohJvmtlunrcFQJSVX1JOwd2hR6bewENY2LciX4zPrFRQqWc4LsYhU1S0X92iiBxpyz/tff+sJH/6ubncWlRg==", "integrity": "sha512-5v6W+ZLBBML5SgntuBDLsYoqUvwfNboAwL6BwPHi3z/hH1f8BS9/0+MCW9OGY712g7E4pc3y9KqS67mWF753eA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"globby": "^11.0.3", "globby": "^11.0.3",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"unixify": "^1.0.0" "unixify": "^1.0.0"
@@ -1731,14 +1748,14 @@
} }
}, },
"node_modules/@graphql-tools/load": { "node_modules/@graphql-tools/load": {
"version": "8.1.1", "version": "8.1.2",
"resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.1.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.2.tgz",
"integrity": "sha512-hqxk+8VHQcl68UFuuTx46DesAJmjQdiGxjicNoB4m4nqk6itWtPYn7Qj9W9iq95PvbicWQasrAQ2srUbIoWE2A==", "integrity": "sha512-WhDPv25/jRND+0uripofMX0IEwo6mrv+tJg6HifRmDu8USCD7nZhufT0PP7lIcuutqjIQFyogqT70BQsy6wOgw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/schema": "^10.0.24", "@graphql-tools/schema": "^10.0.25",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"p-limit": "3.1.0", "p-limit": "3.1.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
@@ -1750,14 +1767,13 @@
} }
}, },
"node_modules/@graphql-tools/merge": { "node_modules/@graphql-tools/merge": {
"version": "9.1.0", "version": "9.1.1",
"resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.1.1.tgz",
"integrity": "sha512-mKmjIVeu4ayPr+LbuhzukBOd67YdLhe9uPO/2tQ74iXP0EQMPlzAbUGPPym92gqCT5SxM6kXT65JUE9oBRX0sQ==", "integrity": "sha512-BJ5/7Y7GOhTuvzzO5tSBFL4NGr7PVqTJY3KeIDlVTT8YLcTXtBR+hlrC3uyEym7Ragn+zyWdHeJ9ev+nRX1X2w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"@theguild/federation-composition": "^0.19.0",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"engines": { "engines": {
@@ -1815,14 +1831,14 @@
} }
}, },
"node_modules/@graphql-tools/relay-operation-optimizer": { "node_modules/@graphql-tools/relay-operation-optimizer": {
"version": "7.0.20", "version": "7.0.21",
"resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.20.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.21.tgz",
"integrity": "sha512-8xl03O/xwME4oRP7BEQEI8OI+ph3oDqQapNEV3X5UIxxLwAj6EKtpXR0mr2LSN9Ico6phrj8cEwVY+hBqAMo0w==", "integrity": "sha512-vMdU0+XfeBh9RCwPqRsr3A05hPA3MsahFn/7OAwXzMySA5EVnSH5R4poWNs3h1a0yT0tDPLhxORhK7qJdSWj2A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ardatan/relay-compiler": "^12.0.3", "@ardatan/relay-compiler": "^12.0.3",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"engines": { "engines": {
@@ -1833,14 +1849,14 @@
} }
}, },
"node_modules/@graphql-tools/schema": { "node_modules/@graphql-tools/schema": {
"version": "10.0.24", "version": "10.0.25",
"resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.24.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.25.tgz",
"integrity": "sha512-SQfYg31/L4EShTygz9I/+Issa3IDS7DFB/gd7AvWeICCNMDm0917QmLDYpVaCmgvzeky7JPeXaJEd0OtZNIW4Q==", "integrity": "sha512-/PqE8US8kdQ7lB9M5+jlW8AyVjRGCKU7TSktuW3WNKSKmDO0MK1wakvb5gGdyT49MjAIb4a3LWxIpwo5VygZuw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/merge": "^9.1.0", "@graphql-tools/merge": "^9.1.1",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"engines": { "engines": {
@@ -1851,16 +1867,16 @@
} }
}, },
"node_modules/@graphql-tools/url-loader": { "node_modules/@graphql-tools/url-loader": {
"version": "8.0.32", "version": "8.0.33",
"resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.32.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.33.tgz",
"integrity": "sha512-dr4eu+/Twbq6bS4O2ASi6EdTLC2bcxo+Iw0j1eDkonw+U5lK/2+aHF/bWRXVTMYMrWOLxv0+iYeGVe/zMjDbEg==", "integrity": "sha512-Fu626qcNHcqAj8uYd7QRarcJn5XZ863kmxsg1sm0fyjyfBJnsvC7ddFt6Hayz5kxVKfsnjxiDfPMXanvsQVBKw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/executor-graphql-ws": "^2.0.1", "@graphql-tools/executor-graphql-ws": "^2.0.1",
"@graphql-tools/executor-http": "^1.1.9", "@graphql-tools/executor-http": "^1.1.9",
"@graphql-tools/executor-legacy-ws": "^1.1.18", "@graphql-tools/executor-legacy-ws": "^1.1.19",
"@graphql-tools/utils": "^10.9.0", "@graphql-tools/utils": "^10.9.1",
"@graphql-tools/wrap": "^10.0.16", "@graphql-tools/wrap": "^10.0.16",
"@types/ws": "^8.0.0", "@types/ws": "^8.0.0",
"@whatwg-node/fetch": "^0.10.0", "@whatwg-node/fetch": "^0.10.0",
@@ -1878,9 +1894,9 @@
} }
}, },
"node_modules/@graphql-tools/utils": { "node_modules/@graphql-tools/utils": {
"version": "10.9.0", "version": "10.9.1",
"resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.9.0.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.9.1.tgz",
"integrity": "sha512-LzFlJHNajdohRM+0pHTwcF9tZ0q7z5iZW0lwnTNJp7O6GYFcSvCQE5ijTQcXVQ/5WQf3SHn+Gpr56TR5XHmPtg==", "integrity": "sha512-B1wwkXk9UvU7LCBkPs8513WxOQ2H8Fo5p8HR1+Id9WmYE5+bd51vqN+MbrqvWczHCH2gwkREgHJN88tE0n1FCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1898,15 +1914,15 @@
} }
}, },
"node_modules/@graphql-tools/wrap": { "node_modules/@graphql-tools/wrap": {
"version": "10.1.2", "version": "10.1.3",
"resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.2.tgz", "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.3.tgz",
"integrity": "sha512-vjmPVrYCRelytltyzHy1+QP4mIBRcStjbDNsEC1TMth9KH9wGi3xToIjAAD4GTOnrc6UyZ9IqaIAhffEnhBTRQ==", "integrity": "sha512-YIcw7oZPlmlZKRBOQGNqKNY4lehB+U4NOP0BSuOd+23EZb8X7JjkruYUOjYsQ7GxS7aKmQpFbuqrfsLp9TRZnA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@graphql-tools/delegate": "^10.2.21", "@graphql-tools/delegate": "^10.2.22",
"@graphql-tools/schema": "^10.0.11", "@graphql-tools/schema": "^10.0.24",
"@graphql-tools/utils": "^10.8.1", "@graphql-tools/utils": "^10.9.0",
"@whatwg-node/promise-helpers": "^1.3.0", "@whatwg-node/promise-helpers": "^1.3.0",
"tslib": "^2.8.1" "tslib": "^2.8.1"
}, },
@@ -2390,9 +2406,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.0.14", "version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==", "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3257,9 +3273,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.187", "version": "1.5.190",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz",
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@@ -3294,9 +3310,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.6", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -3307,32 +3323,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.6", "@esbuild/aix-ppc64": "0.25.8",
"@esbuild/android-arm": "0.25.6", "@esbuild/android-arm": "0.25.8",
"@esbuild/android-arm64": "0.25.6", "@esbuild/android-arm64": "0.25.8",
"@esbuild/android-x64": "0.25.6", "@esbuild/android-x64": "0.25.8",
"@esbuild/darwin-arm64": "0.25.6", "@esbuild/darwin-arm64": "0.25.8",
"@esbuild/darwin-x64": "0.25.6", "@esbuild/darwin-x64": "0.25.8",
"@esbuild/freebsd-arm64": "0.25.6", "@esbuild/freebsd-arm64": "0.25.8",
"@esbuild/freebsd-x64": "0.25.6", "@esbuild/freebsd-x64": "0.25.8",
"@esbuild/linux-arm": "0.25.6", "@esbuild/linux-arm": "0.25.8",
"@esbuild/linux-arm64": "0.25.6", "@esbuild/linux-arm64": "0.25.8",
"@esbuild/linux-ia32": "0.25.6", "@esbuild/linux-ia32": "0.25.8",
"@esbuild/linux-loong64": "0.25.6", "@esbuild/linux-loong64": "0.25.8",
"@esbuild/linux-mips64el": "0.25.6", "@esbuild/linux-mips64el": "0.25.8",
"@esbuild/linux-ppc64": "0.25.6", "@esbuild/linux-ppc64": "0.25.8",
"@esbuild/linux-riscv64": "0.25.6", "@esbuild/linux-riscv64": "0.25.8",
"@esbuild/linux-s390x": "0.25.6", "@esbuild/linux-s390x": "0.25.8",
"@esbuild/linux-x64": "0.25.6", "@esbuild/linux-x64": "0.25.8",
"@esbuild/netbsd-arm64": "0.25.6", "@esbuild/netbsd-arm64": "0.25.8",
"@esbuild/netbsd-x64": "0.25.6", "@esbuild/netbsd-x64": "0.25.8",
"@esbuild/openbsd-arm64": "0.25.6", "@esbuild/openbsd-arm64": "0.25.8",
"@esbuild/openbsd-x64": "0.25.6", "@esbuild/openbsd-x64": "0.25.8",
"@esbuild/openharmony-arm64": "0.25.6", "@esbuild/openharmony-arm64": "0.25.8",
"@esbuild/sunos-x64": "0.25.6", "@esbuild/sunos-x64": "0.25.8",
"@esbuild/win32-arm64": "0.25.6", "@esbuild/win32-arm64": "0.25.8",
"@esbuild/win32-ia32": "0.25.6", "@esbuild/win32-ia32": "0.25.8",
"@esbuild/win32-x64": "0.25.6" "@esbuild/win32-x64": "0.25.8"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
@@ -3608,9 +3624,9 @@
} }
}, },
"node_modules/graphql-config/node_modules/jiti": { "node_modules/graphql-config/node_modules/jiti": {
"version": "2.4.2", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -5859,15 +5875,15 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.0.5", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
"integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.6", "fdir": "^6.4.6",
"picomatch": "^4.0.2", "picomatch": "^4.0.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rollup": "^4.40.0", "rollup": "^4.40.0",
"tinyglobby": "^0.2.14" "tinyglobby": "^0.2.14"

View File

@@ -1,6 +1,6 @@
{ {
"name": "publy-panel", "name": "publy-panel",
"version": "0.7.8", "version": "0.7.9",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -12,26 +12,26 @@
"codegen": "graphql-codegen --config codegen.ts" "codegen": "graphql-codegen --config codegen.ts"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.0.6", "@biomejs/biome": "^2.1.2",
"@graphql-codegen/cli": "^5.0.7", "@graphql-codegen/cli": "^5.0.7",
"@graphql-codegen/client-preset": "^4.8.3", "@graphql-codegen/client-preset": "^4.8.3",
"@graphql-codegen/typescript": "^4.0.6", "@graphql-codegen/typescript": "^4.1.6",
"@graphql-codegen/typescript-operations": "^4.2.0", "@graphql-codegen/typescript-operations": "^4.6.1",
"@graphql-codegen/typescript-resolvers": "^4.0.6", "@graphql-codegen/typescript-resolvers": "^4.5.1",
"@types/node": "^24.0.7", "@types/node": "^24.1.0",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"lightningcss": "^1.30.0", "lightningcss": "^1.30.1",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"solid-js": "^1.9.7", "solid-js": "^1.9.7",
"terser": "^5.39.0", "terser": "^5.43.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^7.0.0", "vite": "^7.0.6",
"vite-plugin-solid": "^2.11.7" "vite-plugin-solid": "^2.11.7"
}, },
"overrides": { "overrides": {
"vite": "^7.0.0" "vite": "^7.0.6"
}, },
"dependencies": { "dependencies": {
"@solidjs/router": "^0.15.3" "@solidjs/router": "^0.15.3"

View File

@@ -1,7 +1,6 @@
bcrypt bcrypt
PyJWT PyJWT
authlib authlib
passlib==1.7.4
google-analytics-data google-analytics-data
colorlog colorlog
psycopg2-binary psycopg2-binary
@@ -12,6 +11,7 @@ starlette
gql gql
ariadne ariadne
granian granian
bcrypt
# NLP and search # NLP and search
httpx httpx
@@ -21,7 +21,6 @@ pydantic
trafilatura trafilatura
types-requests types-requests
types-passlib
types-Authlib types-Authlib
types-orjson types-orjson
types-PyYAML types-PyYAML

View File

@@ -731,8 +731,8 @@ async def admin_get_reactions(
"deleted_at": shout.deleted_at, "deleted_at": shout.deleted_at,
}, },
"stat": { "stat": {
"comments_count": stats.comments_count or 0, "comments_count": stats.comments_count if stats else 0,
"rating": stats.rating or 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() community = session.query(Community).filter(Community.id == target_community_id).first()
if community: 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() default_roles = community.get_default_roles()
if not default_roles:
default_roles = ["reader", "author"]
except AttributeError:
default_roles = ["reader", "author"]
# Создаем CommunityAuthor с дефолтными ролями # Создаем CommunityAuthor с дефолтными ролями
community_author = CommunityAuthor( community_author = CommunityAuthor(

View File

@@ -72,19 +72,30 @@ class AdminService:
@staticmethod @staticmethod
def get_user_roles(user: Author, community_id: int = 1) -> list[str]: 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 from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
admin_emails = ADMIN_EMAILS_LIST.split(",") if ADMIN_EMAILS_LIST else [] admin_emails = ADMIN_EMAILS_LIST.split(",") if ADMIN_EMAILS_LIST else []
user_roles = [] user_roles = []
with local_session() as session: with local_session() as session:
# Получаем все CommunityAuthor для пользователя
all_community_authors = session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).all()
# Сначала ищем точное совпадение по community_id
community_author = ( community_author = (
session.query(CommunityAuthor) session.query(CommunityAuthor)
.filter(CommunityAuthor.author_id == user.id, CommunityAuthor.community_id == community_id) .filter(CommunityAuthor.author_id == user.id, CommunityAuthor.community_id == community_id)
.first() .first()
) )
# Если точного совпадения нет, используем первый найденный CommunityAuthor
if not community_author and all_community_authors:
community_author = all_community_authors[0]
if community_author: if community_author:
# Проверяем, что roles не None и не пустая строка
if community_author.roles is not None and community_author.roles.strip():
user_roles = community_author.role_list user_roles = community_author.role_list
# Добавляем синтетическую роль для системных админов # Добавляем синтетическую роль для системных админов
@@ -188,7 +199,15 @@ class AdminService:
community_author.set_roles(valid_roles) community_author.set_roles(valid_roles)
session.commit() session.commit()
logger.info(f"Пользователь {author.email or author.id} обновлен") 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: 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) 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: with local_session() as session:
session.add(user) 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() 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: if not community:
logger.warning(f"Сообщество {target_community_id} не найдено, используем ID=1") logger.warning(f"Сообщество {target_community_id} не найдено, используем ID=1")
target_community_id = 1 target_community_id = 1
community = session.query(Community).filter(Community.id == target_community_id).first() community = session.query(Community).filter(Community.id == target_community_id).first()
if community: if community:
# Инициализируем права сообщества default_roles = community.get_default_roles() or ["reader", "author"]
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"]
# Создаем CommunityAuthor с ролями # Создаем CommunityAuthor с ролями
community_author = CommunityAuthor( community_author = CommunityAuthor(
@@ -197,7 +214,12 @@ class AuthService:
follower = CommunityFollower(community=target_community_id, follower=int(user.id)) follower = CommunityFollower(community=target_community_id, follower=int(user.id))
session.add(follower) 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() session.commit()
return user return user
@@ -353,7 +375,7 @@ class AuthService:
# Проверяем роли через новую систему CommunityAuthor # Проверяем роли через новую систему CommunityAuthor
from orm.community import get_user_roles_in_community 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 has_reader_role = "reader" in user_roles
logger.debug(f"Роли пользователя {email}: {user_roles}") logger.debug(f"Роли пользователя {email}: {user_roles}")
@@ -676,7 +698,7 @@ class AuthService:
stats["checked"] += 1 stats["checked"] += 1
try: 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: if not had_reader:
stats["fixed"] += 1 stats["fixed"] += 1

View File

@@ -1,102 +1,34 @@
import builtins
import logging import logging
import math import math
import time import time
import traceback import traceback
import warnings import warnings
from io import TextIOWrapper from io import TextIOWrapper
from typing import Any, ClassVar, Type, TypeVar, Union from typing import Any, TypeVar
import orjson
import sqlalchemy 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.dialects.sqlite import insert
from sqlalchemy.engine import Connection, Engine 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 sqlalchemy.pool import StaticPool
from orm.base import BaseModel
from settings import DB_URL from settings import DB_URL
from utils.logger import root_logger as logger from utils.logger import root_logger as logger
# Global variables # Global variables
REGISTRY: dict[str, type["BaseModel"]] = {}
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Database configuration # Database configuration
engine = create_engine(DB_URL, echo=False, poolclass=StaticPool if "sqlite" in DB_URL else None) engine = create_engine(DB_URL, echo=False, poolclass=StaticPool if "sqlite" in DB_URL else None)
ENGINE = engine # Backward compatibility alias ENGINE = engine # Backward compatibility alias
inspector = inspect(engine) inspector = inspect(engine)
# Session = sessionmaker(engine)
configure_mappers() configure_mappers()
T = TypeVar("T") T = TypeVar("T")
FILTERED_FIELDS = ["_sa_instance_state", "search_vector"] 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) # make_searchable(Base.metadata)
# Base.metadata.create_all(bind=engine) # Base.metadata.create_all(bind=engine)
@@ -326,7 +258,5 @@ def local_session(src: str = "") -> Session:
return Session(bind=engine, expire_on_commit=False) return Session(bind=engine, expire_on_commit=False)
# Export Base for backward compatibility
Base = _Base
# Also export the type for type hints # 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 is not None
assert created_user.name == "Test User" assert created_user.name == "Test User"
assert created_user.email_verified is True 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): async def test_token_storage(redis_client):
"""Тест базовой функциональности TokenStorage с правильными fixtures""" """Тест базовой функциональности TokenStorage с правильными fixtures"""
try:
print("✅ Тестирование TokenStorage...") print("✅ Тестирование TokenStorage...")
# Тест создания сессии # Тест создания сессии
@@ -49,3 +50,9 @@ async def test_token_storage(redis_client):
print("Все тесты пройдены успешно!") print("Все тесты пройдены успешно!")
return True 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.orm import sessionmaker
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from services.db import Base from orm.base import BaseModel as Base
from services.redis import redis from services.redis import redis
from tests.test_config import get_test_client from tests.test_config import get_test_client

View File

@@ -3,7 +3,7 @@
Проверяет работу AdminService и AuthService с RBAC системой. Проверяет работу AdminService и AuthService с RBAC системой.
""" """
import logging
import pytest import pytest
from auth.orm import Author from auth.orm import Author
@@ -11,6 +11,8 @@ from orm.community import Community, CommunityAuthor
from services.admin import admin_service from services.admin import admin_service
from services.auth import auth_service from services.auth import auth_service
logger = logging.getLogger(__name__)
@pytest.fixture @pytest.fixture
def simple_user(db_session): def simple_user(db_session):
@@ -36,7 +38,7 @@ def simple_user(db_session):
# Очистка после теста # Очистка после теста
try: try:
# Удаляем связанные записи CommunityAuthor # Удаляем связанные записи 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.query(Author).filter(Author.id == user.id).delete()
db_session.commit() db_session.commit()
@@ -48,17 +50,18 @@ def simple_user(db_session):
def simple_community(db_session, simple_user): def simple_community(db_session, simple_user):
"""Создает простое тестовое сообщество""" """Создает простое тестовое сообщество"""
# Очищаем любые существующие записи с этим ID/slug # Очищаем любые существующие записи с этим ID/slug
db_session.query(Community).filter( db_session.query(Community).filter(Community.slug == "simple-test-community").delete()
(Community.id == 200) | (Community.slug == "simple-test-community")
).delete()
db_session.commit() db_session.commit()
community = Community( community = Community(
id=200,
name="Simple Test Community", name="Simple Test Community",
slug="simple-test-community", slug="simple-test-community",
desc="Simple community for tests", desc="Simple community for tests",
created_by=simple_user.id, created_by=simple_user.id,
settings={
"default_roles": ["reader", "author"],
"available_roles": ["reader", "author", "editor"]
}
) )
db_session.add(community) db_session.add(community)
db_session.commit() db_session.commit()
@@ -76,6 +79,52 @@ def simple_community(db_session, simple_user):
db_session.rollback() 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) @pytest.fixture(autouse=True)
def cleanup_test_users(db_session): 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() existing_user = db_session.query(Author).filter(Author.email == email).first()
if existing_user: if existing_user:
# Удаляем связанные записи CommunityAuthor # Удаляем связанные записи 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.delete(existing_user)
db_session.commit() db_session.commit()
@@ -154,70 +203,101 @@ class TestSimpleAdminService:
# Может быть пустой список или содержать системную роль админа # Может быть пустой список или содержать системную роль админа
assert len(roles) >= 0 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() db_session.commit()
print(f"DEBUG: Удалено записей CommunityAuthor: {deleted_count}")
# Создаем CommunityAuthor с ролями в дефолтном сообществе # Создаем CommunityAuthor с ролями в тестовом сообществе
ca = CommunityAuthor( ca = CommunityAuthor(
community_id=default_community_id, community_id=community_id,
author_id=simple_user.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"]) 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.add(ca)
db_session.commit() db_session.commit()
print(f"DEBUG: CA сохранен в БД с ID: {ca.id}")
# Проверяем что роли сохранились в БД # Явная проверка сохранения CommunityAuthor
saved_ca = db_session.query(CommunityAuthor).filter( check_ca = db_session.query(CommunityAuthor).filter(
CommunityAuthor.author_id == simple_user.id, CommunityAuthor.author_id == simple_user.id,
CommunityAuthor.community_id == default_community_id CommunityAuthor.community_id == community_id
).first() ).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() fresh_user = db_session.query(Author).filter(Author.id == simple_user.id).first()
roles = admin_service.get_user_roles(fresh_user) # Без указания community_id - использует дефолт roles = admin_service.get_user_roles(fresh_user, community_id)
print(f"DEBUG: AdminService вернул роли: {roles}")
assert "reader" in roles # Проверяем роли
assert "author" in roles 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): 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, "id": simple_user.id,
"email": simple_user.email,
"name": "Updated Name", "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() admin_service.update_user({
assert updated_user.name == "Updated Name" "id": simple_user.id,
"name": "Simple User",
# Восстанавливаем исходное имя для других тестов "email": simple_user.email
updated_user.name = original_name })
db_session.commit()
class TestSimpleAuthService: class TestSimpleAuthService:
@@ -227,11 +307,14 @@ class TestSimpleAuthService:
"""Тест базового создания пользователя""" """Тест базового создания пользователя"""
test_email = "test_create_unique@example.com" test_email = "test_create_unique@example.com"
# Удаляем пользователя если существует # Найдем существующих пользователей с таким email
existing = db_session.query(Author).filter(Author.email == test_email).first() existing_users = db_session.query(Author).filter(Author.email == test_email).all()
if existing:
db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == existing.id).delete() # Удаляем связанные записи CommunityAuthor для существующих пользователей
db_session.delete(existing) 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() db_session.commit()
user_dict = { user_dict = {
@@ -247,37 +330,102 @@ class TestSimpleAuthService:
assert user.name == "Test Create User" 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.delete(user)
db_session.commit() db_session.commit()
except Exception as e:
# Если возникла ошибка при удалении, просто логируем ее
print(f"Ошибка при очистке: {e}")
db_session.rollback()
def test_create_user_with_community(self, db_session, simple_community): def test_create_user_with_community(self, db_session):
"""Тест создания пользователя с привязкой к сообществу""" """Проверяем создание пользователя в конкретном сообществе"""
test_email = "test_community_unique@example.com" 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() system_author = db_session.query(Author).filter(Author.slug == "system").first()
if existing: if not system_author:
db_session.query(CommunityAuthor).filter(CommunityAuthor.author_id == existing.id).delete() system_author = Author(
db_session.delete(existing) name="System",
db_session.commit() 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 = { user_dict = {
"email": test_email, "name": unique_name,
"name": "Test Community User", "email": unique_email,
"slug": "test-community-user-unique", "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() from orm.community import get_user_roles_in_community
db_session.delete(user)
# Получаем роли
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() 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: class TestCommunityAuthorMethods:
"""Тесты методов CommunityAuthor""" """Тесты методов CommunityAuthor"""