This commit is contained in:
parent
82111ed0f6
commit
27c5a57709
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,6 +1,19 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
|
||||||
|
## [0.7.1] - 2025-07-02
|
||||||
|
|
||||||
|
### Исправления системы переменных среды и RBAC
|
||||||
|
- **ИСПРАВЛЕНО**: Ошибка `'Author' object has no attribute 'get_permissions'` в нескольких местах:
|
||||||
|
- `auth/decorators.py` - функция `validate_graphql_context`
|
||||||
|
- `auth/middleware.py` - функция `authenticate_user`
|
||||||
|
- `orm/community.py` - метод `get_community_members`
|
||||||
|
- **ИСПРАВЛЕНО**: Резолвер `getEnvVariables` теперь использует `@admin_auth_required` вместо `@admin_only`
|
||||||
|
- **ИСПРАВЛЕНО**: Функция `get_user_roles_from_context` в RBAC системе добавляет роль `admin` для системных администраторов из `ADMIN_EMAILS`
|
||||||
|
- **ИСПРАВЛЕНО**: Циклические импорты в `services/rbac.py` через обработку исключений
|
||||||
|
- **УЛУЧШЕНО**: Корректная работа вкладки переменных среды в админ-панели когда переменных нет
|
||||||
|
- **УЛУЧШЕНО**: Системные администраторы (`ADMIN_EMAILS`) теперь автоматически получают роль `admin` в RBAC декораторах
|
||||||
|
|
||||||
## [0.7.0] - 2025-07-02
|
## [0.7.0] - 2025-07-02
|
||||||
|
|
||||||
### Исправления RBAC системы в админ-панели
|
### Исправления RBAC системы в админ-панели
|
||||||
|
|
|
@ -210,13 +210,11 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
||||||
author = session.query(Author).filter(Author.id == auth_state.author_id).one()
|
author = session.query(Author).filter(Author.id == auth_state.author_id).one()
|
||||||
logger.debug(f"[validate_graphql_context] Найден автор: id={author.id}, email={author.email}")
|
logger.debug(f"[validate_graphql_context] Найден автор: id={author.id}, email={author.email}")
|
||||||
|
|
||||||
# Получаем разрешения из ролей
|
# Создаем объект авторизации с пустыми разрешениями
|
||||||
scopes = await author.get_permissions()
|
# Разрешения будут проверяться через RBAC систему по требованию
|
||||||
|
|
||||||
# Создаем объект авторизации
|
|
||||||
auth_cred = AuthCredentials(
|
auth_cred = AuthCredentials(
|
||||||
author_id=author.id,
|
author_id=author.id,
|
||||||
scopes=scopes,
|
scopes={}, # Пустой словарь разрешений
|
||||||
logged_in=True,
|
logged_in=True,
|
||||||
error_message="",
|
error_message="",
|
||||||
email=author.email,
|
email=author.email,
|
||||||
|
|
|
@ -117,8 +117,9 @@ class AuthMiddleware:
|
||||||
token=None,
|
token=None,
|
||||||
), UnauthenticatedUser()
|
), UnauthenticatedUser()
|
||||||
|
|
||||||
# Получаем разрешения из ролей
|
# Создаем пустой словарь разрешений
|
||||||
scopes = await author.get_permissions()
|
# Разрешения будут проверяться через RBAC систему по требованию
|
||||||
|
scopes = {}
|
||||||
|
|
||||||
# Получаем роли для пользователя
|
# Получаем роли для пользователя
|
||||||
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
|
ca = session.query(CommunityAuthor).filter_by(author_id=author.id, community_id=1).first()
|
||||||
|
|
|
@ -235,7 +235,14 @@ class Community(BaseModel):
|
||||||
|
|
||||||
if with_roles:
|
if with_roles:
|
||||||
member_info["roles"] = ca.role_list # type: ignore[assignment]
|
member_info["roles"] = ca.role_list # type: ignore[assignment]
|
||||||
member_info["permissions"] = ca.get_permissions() # type: ignore[assignment]
|
# Получаем разрешения синхронно
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
member_info["permissions"] = asyncio.run(ca.get_permissions()) # type: ignore[assignment]
|
||||||
|
except Exception:
|
||||||
|
# Если не удается получить разрешения асинхронно, используем пустой список
|
||||||
|
member_info["permissions"] = [] # type: ignore[assignment]
|
||||||
|
|
||||||
members.append(member_info)
|
members.append(member_info)
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,65 @@
|
||||||
"community": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
|
"community": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
|
||||||
"draft": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
|
"draft": ["create", "read", "update_own", "update_any", "delete_own", "delete_any"],
|
||||||
"reaction": [
|
"reaction": [
|
||||||
"create:LIKE", "read:LIKE", "update_own:LIKE", "update_any:LIKE", "delete_own:LIKE", "delete_any:LIKE",
|
"create:LIKE",
|
||||||
"create:COMMENT", "read:COMMENT", "update_own:COMMENT", "update_any:COMMENT", "delete_own:COMMENT", "delete_any:COMMENT",
|
"read:LIKE",
|
||||||
"create:QUOTE", "read:QUOTE", "update_own:QUOTE", "update_any:QUOTE", "delete_own:QUOTE", "delete_any:QUOTE",
|
"update_own:LIKE",
|
||||||
"create:DISLIKE", "read:DISLIKE", "update_own:DISLIKE", "update_any:DISLIKE", "delete_own:DISLIKE", "delete_any:DISLIKE",
|
"update_any:LIKE",
|
||||||
"create:CREDIT", "read:CREDIT", "update_own:CREDIT", "update_any:CREDIT", "delete_own:CREDIT", "delete_any:CREDIT",
|
"delete_own:LIKE",
|
||||||
"create:PROOF", "read:PROOF", "update_own:PROOF", "update_any:PROOF", "delete_own:PROOF", "delete_any:PROOF",
|
"delete_any:LIKE",
|
||||||
"create:DISPROOF", "read:DISPROOF", "update_own:DISPROOF", "update_any:DISPROOF", "delete_own:DISPROOF", "delete_any:DISPROOF",
|
"create:COMMENT",
|
||||||
"create:AGREE", "read:AGREE", "update_own:AGREE", "update_any:AGREE", "delete_own:AGREE", "delete_any:AGREE",
|
"read:COMMENT",
|
||||||
"create:DISAGREE", "read:DISAGREE", "update_own:DISAGREE", "update_any:DISAGREE", "delete_own:DISAGREE", "delete_any:DISAGREE",
|
"update_own:COMMENT",
|
||||||
"create:SILENT", "read:SILENT", "update_own:SILENT", "update_any:SILENT", "delete_own:SILENT", "delete_any:SILENT"
|
"update_any:COMMENT",
|
||||||
|
"delete_own:COMMENT",
|
||||||
|
"delete_any:COMMENT",
|
||||||
|
"create:QUOTE",
|
||||||
|
"read:QUOTE",
|
||||||
|
"update_own:QUOTE",
|
||||||
|
"update_any:QUOTE",
|
||||||
|
"delete_own:QUOTE",
|
||||||
|
"delete_any:QUOTE",
|
||||||
|
"create:DISLIKE",
|
||||||
|
"read:DISLIKE",
|
||||||
|
"update_own:DISLIKE",
|
||||||
|
"update_any:DISLIKE",
|
||||||
|
"delete_own:DISLIKE",
|
||||||
|
"delete_any:DISLIKE",
|
||||||
|
"create:CREDIT",
|
||||||
|
"read:CREDIT",
|
||||||
|
"update_own:CREDIT",
|
||||||
|
"update_any:CREDIT",
|
||||||
|
"delete_own:CREDIT",
|
||||||
|
"delete_any:CREDIT",
|
||||||
|
"create:PROOF",
|
||||||
|
"read:PROOF",
|
||||||
|
"update_own:PROOF",
|
||||||
|
"update_any:PROOF",
|
||||||
|
"delete_own:PROOF",
|
||||||
|
"delete_any:PROOF",
|
||||||
|
"create:DISPROOF",
|
||||||
|
"read:DISPROOF",
|
||||||
|
"update_own:DISPROOF",
|
||||||
|
"update_any:DISPROOF",
|
||||||
|
"delete_own:DISPROOF",
|
||||||
|
"delete_any:DISPROOF",
|
||||||
|
"create:AGREE",
|
||||||
|
"read:AGREE",
|
||||||
|
"update_own:AGREE",
|
||||||
|
"update_any:AGREE",
|
||||||
|
"delete_own:AGREE",
|
||||||
|
"delete_any:AGREE",
|
||||||
|
"create:DISAGREE",
|
||||||
|
"read:DISAGREE",
|
||||||
|
"update_own:DISAGREE",
|
||||||
|
"update_any:DISAGREE",
|
||||||
|
"delete_own:DISAGREE",
|
||||||
|
"delete_any:DISAGREE",
|
||||||
|
"create:SILENT",
|
||||||
|
"read:SILENT",
|
||||||
|
"update_own:SILENT",
|
||||||
|
"update_any:SILENT",
|
||||||
|
"delete_own:SILENT",
|
||||||
|
"delete_any:SILENT"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ from orm.invite import Invite, InviteStatus
|
||||||
from orm.shout import Shout
|
from orm.shout import Shout
|
||||||
from services.db import local_session
|
from services.db import local_session
|
||||||
from services.env import EnvManager, EnvVariable
|
from services.env import EnvManager, EnvVariable
|
||||||
from services.rbac import admin_only
|
|
||||||
from services.schema import mutation, query
|
from services.schema import mutation, query
|
||||||
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
|
@ -42,6 +41,99 @@ default_role_descriptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# === ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ DRY ===
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_pagination(limit: int = 20, offset: int = 0) -> tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Нормализует параметры пагинации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Максимальное количество записей
|
||||||
|
offset: Смещение
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Кортеж (limit, offset) с нормализованными значениями
|
||||||
|
"""
|
||||||
|
return max(1, min(100, limit or 20)), max(0, offset or 0)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_pagination_info(total_count: int, limit: int, offset: int) -> dict[str, int]:
|
||||||
|
"""
|
||||||
|
Вычисляет информацию о пагинации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
total_count: Общее количество записей
|
||||||
|
limit: Количество записей на странице
|
||||||
|
offset: Смещение
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Словарь с информацией о пагинации
|
||||||
|
"""
|
||||||
|
per_page = limit
|
||||||
|
if total_count is None or per_page in (None, 0):
|
||||||
|
total_pages = 1
|
||||||
|
else:
|
||||||
|
total_pages = ceil(total_count / per_page)
|
||||||
|
current_page = (offset // per_page) + 1 if per_page > 0 else 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"total": total_count,
|
||||||
|
"page": current_page,
|
||||||
|
"perPage": per_page,
|
||||||
|
"totalPages": total_pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_admin_error(operation: str, error: Exception) -> GraphQLError:
|
||||||
|
"""
|
||||||
|
Обрабатывает ошибки в админ-резолверах.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation: Название операции
|
||||||
|
error: Исключение
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
GraphQLError для возврата клиенту
|
||||||
|
"""
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
logger.error(f"Ошибка при {operation}: {error!s}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
msg = f"Не удалось {operation}: {error!s}"
|
||||||
|
return GraphQLError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def get_author_info(author_id: int, session) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Получает информацию об авторе для отображения в админ-панели.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
author_id: ID автора
|
||||||
|
session: Сессия БД
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Словарь с информацией об авторе
|
||||||
|
"""
|
||||||
|
if not author_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
author = session.query(Author).filter(Author.id == author_id).first()
|
||||||
|
if author:
|
||||||
|
return {
|
||||||
|
"id": author.id,
|
||||||
|
"email": author.email,
|
||||||
|
"name": author.name,
|
||||||
|
"slug": author.slug or f"user-{author.id}",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"id": author_id,
|
||||||
|
"email": "unknown",
|
||||||
|
"name": "unknown",
|
||||||
|
"slug": f"user-{author_id}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _get_user_roles(user: Author, community_id: int = 1) -> list[str]:
|
def _get_user_roles(user: Author, community_id: int = 1) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Получает полный список ролей пользователя в указанном сообществе, включая
|
Получает полный список ролей пользователя в указанном сообществе, включая
|
||||||
|
@ -86,7 +178,7 @@ async def admin_get_users(
|
||||||
Получает список пользователей для админ-панели с поддержкой пагинации и поиска
|
Получает список пользователей для админ-панели с поддержкой пагинации и поиска
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
info: Контекст GraphQL запроса
|
_info: Контекст GraphQL запроса
|
||||||
limit: Максимальное количество записей для получения
|
limit: Максимальное количество записей для получения
|
||||||
offset: Смещение в списке результатов
|
offset: Смещение в списке результатов
|
||||||
search: Строка поиска (по email, имени или ID)
|
search: Строка поиска (по email, имени или ID)
|
||||||
|
@ -95,9 +187,8 @@ async def admin_get_users(
|
||||||
Пагинированный список пользователей
|
Пагинированный список пользователей
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Нормализуем параметры
|
# Нормализуем параметры пагинации
|
||||||
limit = max(1, min(100, limit or 20)) # Ограничиваем количество записей от 1 до 100
|
limit, offset = normalize_pagination(limit, offset)
|
||||||
offset = max(0, offset or 0) # Смещение не может быть отрицательным
|
|
||||||
|
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
# Базовый запрос
|
# Базовый запрос
|
||||||
|
@ -117,17 +208,12 @@ async def admin_get_users(
|
||||||
# Получаем общее количество записей
|
# Получаем общее количество записей
|
||||||
total_count = query.count()
|
total_count = query.count()
|
||||||
|
|
||||||
# Вычисляем информацию о пагинации
|
|
||||||
per_page = limit
|
|
||||||
if total_count is None or per_page in (None, 0):
|
|
||||||
total_pages = 1
|
|
||||||
else:
|
|
||||||
total_pages = ceil(total_count / per_page)
|
|
||||||
current_page = (offset // per_page) + 1 if per_page > 0 else 1
|
|
||||||
|
|
||||||
# Применяем пагинацию
|
# Применяем пагинацию
|
||||||
authors = query.order_by(Author.id).offset(offset).limit(limit).all()
|
authors = query.order_by(Author.id).offset(offset).limit(limit).all()
|
||||||
|
|
||||||
|
# Вычисляем информацию о пагинации
|
||||||
|
pagination_info = calculate_pagination_info(total_count, limit, offset)
|
||||||
|
|
||||||
# Преобразуем в формат для API
|
# Преобразуем в формат для API
|
||||||
return {
|
return {
|
||||||
"authors": [
|
"authors": [
|
||||||
|
@ -142,19 +228,11 @@ async def admin_get_users(
|
||||||
}
|
}
|
||||||
for user in authors
|
for user in authors
|
||||||
],
|
],
|
||||||
"total": total_count,
|
**pagination_info,
|
||||||
"page": current_page,
|
|
||||||
"perPage": per_page,
|
|
||||||
"totalPages": total_pages,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
raise handle_admin_error("получении списка пользователей", e) from e
|
||||||
|
|
||||||
logger.error(f"Ошибка при получении списка пользователей: {e!s}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
msg = f"Не удалось получить список пользователей: {e!s}"
|
|
||||||
raise GraphQLError(msg) from e
|
|
||||||
|
|
||||||
|
|
||||||
@query.field("adminGetRoles")
|
@query.field("adminGetRoles")
|
||||||
|
@ -224,7 +302,7 @@ async def admin_get_roles(_: None, info: GraphQLResolveInfo, community: int = No
|
||||||
|
|
||||||
|
|
||||||
@query.field("getEnvVariables")
|
@query.field("getEnvVariables")
|
||||||
@admin_only
|
@admin_auth_required
|
||||||
async def get_env_variables(_: None, info: GraphQLResolveInfo) -> list[dict[str, Any]]:
|
async def get_env_variables(_: None, info: GraphQLResolveInfo) -> list[dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Получает список переменных окружения, сгруппированных по секциям
|
Получает список переменных окружения, сгруппированных по секциям
|
||||||
|
@ -908,9 +986,8 @@ async def admin_get_invites(
|
||||||
Пагинированный список приглашений
|
Пагинированный список приглашений
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Нормализуем параметры
|
# Нормализуем параметры пагинации
|
||||||
limit = max(1, min(100, limit or 10))
|
limit, offset = normalize_pagination(limit, offset)
|
||||||
offset = max(0, offset or 0)
|
|
||||||
|
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
# Базовый запрос с загрузкой связанных объектов
|
# Базовый запрос с загрузкой связанных объектов
|
||||||
|
@ -957,26 +1034,19 @@ async def admin_get_invites(
|
||||||
# Получаем общее количество записей
|
# Получаем общее количество записей
|
||||||
total_count = query.count()
|
total_count = query.count()
|
||||||
|
|
||||||
# Вычисляем информацию о пагинации
|
|
||||||
per_page = limit
|
|
||||||
if total_count is None or per_page in (None, 0):
|
|
||||||
total_pages = 1
|
|
||||||
else:
|
|
||||||
total_pages = ceil(total_count / per_page)
|
|
||||||
current_page = (offset // per_page) + 1 if per_page > 0 else 1
|
|
||||||
|
|
||||||
# Применяем пагинацию и сортировку (по ID приглашающего, затем автора, затем публикации)
|
# Применяем пагинацию и сортировку (по ID приглашающего, затем автора, затем публикации)
|
||||||
invites = (
|
invites = (
|
||||||
query.order_by(Invite.inviter_id, Invite.author_id, Invite.shout_id).offset(offset).limit(limit).all()
|
query.order_by(Invite.inviter_id, Invite.author_id, Invite.shout_id).offset(offset).limit(limit).all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Вычисляем информацию о пагинации
|
||||||
|
pagination_info = calculate_pagination_info(total_count, limit, offset)
|
||||||
|
|
||||||
# Преобразуем в формат для API
|
# Преобразуем в формат для API
|
||||||
result_invites = []
|
result_invites = []
|
||||||
for invite in invites:
|
for invite in invites:
|
||||||
# Получаем автора публикации
|
# Получаем информацию о создателе публикации
|
||||||
created_by_author = None
|
created_by_info = get_author_info(invite.shout.created_by if invite.shout else None, session)
|
||||||
if invite.shout and invite.shout.created_by:
|
|
||||||
created_by_author = session.query(Author).filter(Author.id == invite.shout.created_by).first()
|
|
||||||
|
|
||||||
invite_dict = {
|
invite_dict = {
|
||||||
"inviter_id": invite.inviter_id,
|
"inviter_id": invite.inviter_id,
|
||||||
|
@ -987,86 +1057,32 @@ async def admin_get_invites(
|
||||||
"id": invite.inviter.id,
|
"id": invite.inviter.id,
|
||||||
"name": invite.inviter.name or "Без имени",
|
"name": invite.inviter.name or "Без имени",
|
||||||
"email": invite.inviter.email,
|
"email": invite.inviter.email,
|
||||||
"slug": invite.inviter.slug or f"user-{invite.inviter.id}", # Добавляем значение по умолчанию
|
"slug": invite.inviter.slug or f"user-{invite.inviter.id}",
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"id": invite.author.id,
|
"id": invite.author.id,
|
||||||
"name": invite.author.name or "Без имени",
|
"name": invite.author.name or "Без имени",
|
||||||
"email": invite.author.email,
|
"email": invite.author.email,
|
||||||
"slug": invite.author.slug or f"user-{invite.author.id}", # Добавляем значение по умолчанию
|
"slug": invite.author.slug or f"user-{invite.author.id}",
|
||||||
},
|
},
|
||||||
"shout": {
|
"shout": {
|
||||||
"id": invite.shout.id,
|
"id": invite.shout.id,
|
||||||
"title": invite.shout.title,
|
"title": invite.shout.title,
|
||||||
"slug": invite.shout.slug,
|
"slug": invite.shout.slug,
|
||||||
|
"created_by": created_by_info,
|
||||||
},
|
},
|
||||||
"created_at": None, # У приглашений нет created_at поля в текущей модели
|
"created_at": None, # У приглашений нет created_at поля в текущей модели
|
||||||
}
|
}
|
||||||
|
|
||||||
# Добавляем информацию о создателе публикации, если она доступна
|
|
||||||
if created_by_author:
|
|
||||||
# Создаем новый словарь для shout
|
|
||||||
shout_dict = {}
|
|
||||||
|
|
||||||
# Копируем основные поля
|
|
||||||
if isinstance(invite_dict["shout"], dict):
|
|
||||||
shout_info = invite_dict["shout"]
|
|
||||||
shout_dict["id"] = shout_info.get("id")
|
|
||||||
shout_dict["title"] = shout_info.get("title")
|
|
||||||
shout_dict["slug"] = shout_info.get("slug")
|
|
||||||
else:
|
|
||||||
# Если это не словарь, берем данные напрямую из объекта invite.shout
|
|
||||||
shout_dict["id"] = invite.shout.id
|
|
||||||
shout_dict["title"] = invite.shout.title
|
|
||||||
shout_dict["slug"] = invite.shout.slug
|
|
||||||
|
|
||||||
# Добавляем информацию о создателе
|
|
||||||
shout_dict["created_by"] = {
|
|
||||||
"id": created_by_author.id,
|
|
||||||
"name": created_by_author.name or "Без имени",
|
|
||||||
"email": created_by_author.email,
|
|
||||||
"slug": created_by_author.slug or f"user-{created_by_author.id}",
|
|
||||||
}
|
|
||||||
|
|
||||||
invite_dict["shout"] = shout_dict
|
|
||||||
else:
|
|
||||||
# Создаем новый словарь для shout
|
|
||||||
shout_dict = {}
|
|
||||||
|
|
||||||
# Копируем основные поля
|
|
||||||
if isinstance(invite_dict["shout"], dict):
|
|
||||||
shout_info = invite_dict["shout"]
|
|
||||||
shout_dict["id"] = shout_info.get("id")
|
|
||||||
shout_dict["title"] = shout_info.get("title")
|
|
||||||
shout_dict["slug"] = shout_info.get("slug")
|
|
||||||
else:
|
|
||||||
# Если это не словарь, берем данные напрямую из объекта invite.shout
|
|
||||||
shout_dict["id"] = invite.shout.id
|
|
||||||
shout_dict["title"] = invite.shout.title
|
|
||||||
shout_dict["slug"] = invite.shout.slug
|
|
||||||
|
|
||||||
# Указываем, что created_by отсутствует
|
|
||||||
shout_dict["created_by"] = None
|
|
||||||
|
|
||||||
invite_dict["shout"] = shout_dict
|
|
||||||
|
|
||||||
result_invites.append(invite_dict)
|
result_invites.append(invite_dict)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"invites": result_invites,
|
"invites": result_invites,
|
||||||
"total": total_count,
|
**pagination_info,
|
||||||
"page": current_page,
|
|
||||||
"perPage": per_page,
|
|
||||||
"totalPages": total_pages,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
raise handle_admin_error("получении списка приглашений", e) from e
|
||||||
|
|
||||||
logger.error(f"Ошибка при получении списка приглашений: {e!s}")
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
msg = f"Не удалось получить список приглашений: {e!s}"
|
|
||||||
raise GraphQLError(msg) from e
|
|
||||||
|
|
||||||
|
|
||||||
@mutation.field("adminUpdateInvite")
|
@mutation.field("adminUpdateInvite")
|
||||||
|
|
|
@ -138,17 +138,21 @@ def get_user_roles_in_community(author_id: int, community_id: int) -> list[str]:
|
||||||
Returns:
|
Returns:
|
||||||
Список ролей пользователя в сообществе
|
Список ролей пользователя в сообществе
|
||||||
"""
|
"""
|
||||||
from orm.community import CommunityAuthor
|
try:
|
||||||
from services.db import local_session
|
from orm.community import CommunityAuthor
|
||||||
|
from services.db import local_session
|
||||||
|
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
ca = (
|
ca = (
|
||||||
session.query(CommunityAuthor)
|
session.query(CommunityAuthor)
|
||||||
.filter(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
.filter(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
return ca.role_list if ca else []
|
return ca.role_list if ca else []
|
||||||
|
except ImportError:
|
||||||
|
# Если есть циклический импорт, возвращаем пустой список
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def user_has_permission(author_id: int, permission: str, community_id: int) -> bool:
|
async def user_has_permission(author_id: int, permission: str, community_id: int) -> bool:
|
||||||
|
@ -209,6 +213,24 @@ def get_user_roles_from_context(info) -> tuple[list[str], int]:
|
||||||
# Получаем роли пользователя в этом сообществе
|
# Получаем роли пользователя в этом сообществе
|
||||||
user_roles = get_user_roles_in_community(author_id, community_id)
|
user_roles = get_user_roles_in_community(author_id, community_id)
|
||||||
|
|
||||||
|
# Проверяем, является ли пользователь системным администратором
|
||||||
|
try:
|
||||||
|
from auth.orm import Author
|
||||||
|
from services.db import local_session
|
||||||
|
from settings import ADMIN_EMAILS
|
||||||
|
|
||||||
|
admin_emails = ADMIN_EMAILS.split(",") if ADMIN_EMAILS else []
|
||||||
|
|
||||||
|
with local_session() as session:
|
||||||
|
author = session.query(Author).filter(Author.id == author_id).first()
|
||||||
|
if author and author.email and author.email in admin_emails:
|
||||||
|
# Системный администратор автоматически получает роль admin в любом сообществе
|
||||||
|
if "admin" not in user_roles:
|
||||||
|
user_roles = [*user_roles, "admin"]
|
||||||
|
except Exception:
|
||||||
|
# Если не удалось проверить email (включая циклические импорты), продолжаем с существующими ролями
|
||||||
|
pass
|
||||||
|
|
||||||
return user_roles, community_id
|
return user_roles, community_id
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user