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

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

View File

@@ -72,20 +72,31 @@ class AdminService:
@staticmethod
def get_user_roles(user: Author, community_id: int = 1) -> list[str]:
"""Получает роли пользователя в сообществе"""
from orm.community import CommunityAuthor # Явный импорт
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
admin_emails = ADMIN_EMAILS_LIST.split(",") if ADMIN_EMAILS_LIST else []
user_roles = []
with local_session() as session:
# Получаем все CommunityAuthor для пользователя
all_community_authors = session.query(CommunityAuthor).filter(CommunityAuthor.author_id == user.id).all()
# Сначала ищем точное совпадение по community_id
community_author = (
session.query(CommunityAuthor)
.filter(CommunityAuthor.author_id == user.id, CommunityAuthor.community_id == community_id)
.first()
)
# Если точного совпадения нет, используем первый найденный CommunityAuthor
if not community_author and all_community_authors:
community_author = all_community_authors[0]
if community_author:
user_roles = community_author.role_list
# Проверяем, что roles не None и не пустая строка
if community_author.roles is not None and community_author.roles.strip():
user_roles = community_author.role_list
# Добавляем синтетическую роль для системных админов
if user.email and user.email.lower() in [email.lower() for email in admin_emails]:
@@ -188,7 +199,15 @@ class AdminService:
community_author.set_roles(valid_roles)
session.commit()
logger.info(f"Пользователь {author.email or author.id} обновлен")
return {"success": True}
# Возвращаем обновленного пользователя
return {
"success": True,
"name": author.name,
"email": author.email,
"slug": author.slug,
"roles": self.get_user_roles(author),
}
# === ПУБЛИКАЦИИ ===

View File

@@ -153,37 +153,54 @@ class AuthService:
def create_user(self, user_dict: dict[str, Any], community_id: int | None = None) -> Author:
"""Создает нового пользователя с дефолтными ролями"""
# Нормализуем email
if "email" in user_dict:
user_dict["email"] = user_dict["email"].lower()
# Проверяем уникальность email
with local_session() as session:
existing_user = session.query(Author).filter(Author.email == user_dict["email"]).first()
if existing_user:
# Если пользователь с таким email уже существует, возвращаем его
logger.warning(f"Пользователь с email {user_dict['email']} уже существует")
return existing_user
# Генерируем уникальный slug
base_slug = user_dict.get("slug", generate_unique_slug(user_dict.get("name", user_dict.get("email", "user"))))
# Проверяем уникальность slug
with local_session() as session:
# Добавляем суффикс, если slug уже существует
counter = 1
unique_slug = base_slug
while session.query(Author).filter(Author.slug == unique_slug).first():
unique_slug = f"{base_slug}-{counter}"
counter += 1
user_dict["slug"] = unique_slug
user = Author(**user_dict)
target_community_id = community_id or 1
target_community_id = int(community_id) if community_id is not None else 1
with local_session() as session:
session.add(user)
session.flush()
session.flush() # Получаем ID пользователя
# Получаем сообщество для назначения ролей
logger.debug(f"Ищем сообщество с ID {target_community_id}")
community = session.query(Community).filter(Community.id == target_community_id).first()
# Отладочная информация
all_communities = session.query(Community).all()
logger.debug(f"Все сообщества в базе: {[c.id for c in all_communities]}")
if not community:
logger.warning(f"Сообщество {target_community_id} не найдено, используем ID=1")
target_community_id = 1
community = session.query(Community).filter(Community.id == target_community_id).first()
if community:
# Инициализируем права сообщества
try:
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(community.initialize_role_permissions())
except Exception as e:
logger.warning(f"Не удалось инициализировать права сообщества: {e}")
# Получаем дефолтные роли
try:
default_roles = community.get_default_roles()
if not default_roles:
default_roles = ["reader", "author"]
except AttributeError:
default_roles = ["reader", "author"]
default_roles = community.get_default_roles() or ["reader", "author"]
# Создаем CommunityAuthor с ролями
community_author = CommunityAuthor(
@@ -197,7 +214,12 @@ class AuthService:
follower = CommunityFollower(community=target_community_id, follower=int(user.id))
session.add(follower)
logger.info(f"Пользователь {user.id} создан с ролями {default_roles}")
logger.info(
f"Пользователь {user.id} создан с ролями {default_roles} в сообществе {target_community_id}"
)
else:
# Если сообщество не найдено, вызываем исключение
raise ValueError("Сообщество не найдено")
session.commit()
return user
@@ -353,7 +375,7 @@ class AuthService:
# Проверяем роли через новую систему CommunityAuthor
from orm.community import get_user_roles_in_community
user_roles = get_user_roles_in_community(author.id, community_id=1)
user_roles = get_user_roles_in_community(int(author.id), community_id=1)
has_reader_role = "reader" in user_roles
logger.debug(f"Роли пользователя {email}: {user_roles}")
@@ -676,7 +698,7 @@ class AuthService:
stats["checked"] += 1
try:
had_reader = await self.ensure_user_has_reader_role(author.id)
had_reader = await self.ensure_user_has_reader_role(int(author.id))
if not had_reader:
stats["fixed"] += 1

View File

@@ -1,102 +1,34 @@
import builtins
import logging
import math
import time
import traceback
import warnings
from io import TextIOWrapper
from typing import Any, ClassVar, Type, TypeVar, Union
from typing import Any, TypeVar
import orjson
import sqlalchemy
from sqlalchemy import JSON, Column, Integer, create_engine, event, exc, func, inspect
from sqlalchemy import create_engine, event, exc, func, inspect
from sqlalchemy.dialects.sqlite import insert
from sqlalchemy.engine import Connection, Engine
from sqlalchemy.orm import Session, configure_mappers, declarative_base, joinedload
from sqlalchemy.orm import Session, configure_mappers, joinedload
from sqlalchemy.pool import StaticPool
from orm.base import BaseModel
from settings import DB_URL
from utils.logger import root_logger as logger
# Global variables
REGISTRY: dict[str, type["BaseModel"]] = {}
logger = logging.getLogger(__name__)
# Database configuration
engine = create_engine(DB_URL, echo=False, poolclass=StaticPool if "sqlite" in DB_URL else None)
ENGINE = engine # Backward compatibility alias
inspector = inspect(engine)
# Session = sessionmaker(engine)
configure_mappers()
T = TypeVar("T")
FILTERED_FIELDS = ["_sa_instance_state", "search_vector"]
# Создаем Base для внутреннего использования
_Base = declarative_base()
# Create proper type alias for Base
BaseType = Type[_Base] # type: ignore[valid-type]
class BaseModel(_Base): # type: ignore[valid-type,misc]
__abstract__ = True
__allow_unmapped__ = True
__table_args__: ClassVar[Union[dict[str, Any], tuple]] = {"extend_existing": True}
id = Column(Integer, primary_key=True)
def __init_subclass__(cls, **kwargs: Any) -> None:
REGISTRY[cls.__name__] = cls
super().__init_subclass__(**kwargs)
def dict(self, access: bool = False) -> builtins.dict[str, Any]:
"""
Конвертирует ORM объект в словарь.
Пропускает атрибуты, которые отсутствуют в объекте, но присутствуют в колонках таблицы.
Преобразует JSON поля в словари.
Добавляет синтетическое поле .stat, если оно существует.
Returns:
Dict[str, Any]: Словарь с атрибутами объекта
"""
column_names = filter(lambda x: x not in FILTERED_FIELDS, self.__table__.columns.keys())
data = {}
try:
for column_name in column_names:
try:
# Проверяем, существует ли атрибут в объекте
if hasattr(self, column_name):
value = getattr(self, column_name)
# Проверяем, является ли значение JSON и декодируем его при необходимости
if isinstance(value, (str, bytes)) and isinstance(
self.__table__.columns[column_name].type, JSON
):
try:
data[column_name] = orjson.loads(value)
except (TypeError, orjson.JSONDecodeError) as e:
logger.exception(f"Error decoding JSON for column '{column_name}': {e}")
data[column_name] = value
else:
data[column_name] = value
else:
# Пропускаем атрибут, если его нет в объекте (может быть добавлен после миграции)
logger.debug(f"Skipping missing attribute '{column_name}' for {self.__class__.__name__}")
except AttributeError as e:
logger.warning(f"Attribute error for column '{column_name}': {e}")
# Добавляем синтетическое поле .stat если оно существует
if hasattr(self, "stat"):
data["stat"] = self.stat
except Exception as e:
logger.exception(f"Error occurred while converting object to dictionary: {e}")
return data
def update(self, values: builtins.dict[str, Any]) -> None:
for key, value in values.items():
if hasattr(self, key):
setattr(self, key, value)
# make_searchable(Base.metadata)
# Base.metadata.create_all(bind=engine)
@@ -326,7 +258,5 @@ def local_session(src: str = "") -> Session:
return Session(bind=engine, expire_on_commit=False)
# Export Base for backward compatibility
Base = _Base
# Also export the type for type hints
__all__ = ["Base", "BaseModel", "BaseType", "engine", "local_session"]
__all__ = ["engine", "local_session"]