This commit is contained in:
@@ -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]"
|
||||||
# ]
|
# ]
|
||||||
|
48
CHANGELOG.md
48
CHANGELOG.md
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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:
|
||||||
|
@@ -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
100
orm/base.py
Normal 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)
|
@@ -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):
|
||||||
|
@@ -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]:
|
||||||
"""
|
"""
|
||||||
|
@@ -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="")
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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):
|
||||||
|
@@ -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="")
|
||||||
|
@@ -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
482
package-lock.json
generated
@@ -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"
|
||||||
|
20
package.json
20
package.json
@@ -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"
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -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(
|
||||||
|
@@ -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),
|
||||||
|
}
|
||||||
|
|
||||||
# === ПУБЛИКАЦИИ ===
|
# === ПУБЛИКАЦИИ ===
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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"]
|
||||||
|
34
tests/auth/test_auth_service.py
Normal file
34
tests/auth/test_auth_service.py
Normal 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()
|
13
tests/auth/test_identity.py
Normal file
13
tests/auth/test_identity.py
Normal 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
|
@@ -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()
|
||||||
|
@@ -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()
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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"""
|
||||||
|
Reference in New Issue
Block a user