core/resolvers/admin.py

620 lines
27 KiB
Python
Raw Normal View History

2025-07-02 21:20:10 +00:00
"""
Админ-резолверы - тонкие GraphQL обёртки над AdminService
"""
from typing import Any
2025-05-29 09:37:39 +00:00
from graphql import GraphQLResolveInfo
2025-05-19 08:25:41 +00:00
from graphql.error import GraphQLError
from auth.decorators import admin_auth_required
2025-07-02 21:20:10 +00:00
from services.admin import admin_service
2025-05-29 09:37:39 +00:00
from services.schema import mutation, query
2025-05-19 08:25:41 +00:00
from utils.logger import root_logger as logger
2025-07-02 19:30:21 +00:00
2025-07-02 21:20:10 +00:00
def handle_error(operation: str, error: Exception) -> GraphQLError:
"""Обрабатывает ошибки в резолверах"""
logger.error(f"Ошибка при {operation}: {error}")
return GraphQLError(f"Не удалось {operation}: {error}")
2025-07-02 19:30:21 +00:00
2025-07-02 21:20:10 +00:00
# === ПОЛЬЗОВАТЕЛИ ===
2025-07-02 19:30:21 +00:00
2025-05-19 08:25:41 +00:00
@query.field("adminGetUsers")
@admin_auth_required
async def admin_get_users(
2025-07-02 19:30:21 +00:00
_: None, _info: GraphQLResolveInfo, limit: int = 20, offset: int = 0, search: str = ""
) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Получает список пользователей"""
2025-05-20 22:34:02 +00:00
try:
2025-07-02 21:20:10 +00:00
return admin_service.get_users(limit, offset, search)
2025-05-20 22:34:02 +00:00
except Exception as e:
2025-07-02 21:20:10 +00:00
raise handle_error("получении списка пользователей", e) from e
2025-05-20 22:34:02 +00:00
@mutation.field("adminUpdateUser")
@admin_auth_required
2025-07-02 21:20:10 +00:00
async def admin_update_user(_: None, _info: GraphQLResolveInfo, user: dict[str, Any]) -> dict[str, Any]:
"""Обновляет данные пользователя"""
2025-05-20 22:34:02 +00:00
try:
2025-07-02 21:20:10 +00:00
return admin_service.update_user(user)
2025-05-20 22:34:02 +00:00
except Exception as e:
2025-07-02 21:20:10 +00:00
logger.error(f"Ошибка обновления пользователя: {e}")
return {"success": False, "error": str(e)}
2025-06-28 10:47:08 +00:00
2025-07-02 21:20:10 +00:00
# === ПУБЛИКАЦИИ ===
2025-06-28 10:47:08 +00:00
@query.field("adminGetShouts")
@admin_auth_required
async def admin_get_shouts(
2025-07-02 19:30:21 +00:00
_: None,
2025-07-02 21:20:10 +00:00
_info: GraphQLResolveInfo,
2025-07-02 19:30:21 +00:00
limit: int = 20,
offset: int = 0,
search: str = "",
status: str = "all",
community: int = None,
2025-06-28 10:47:08 +00:00
) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Получает список публикаций"""
2025-06-28 10:47:08 +00:00
try:
2025-07-02 21:20:10 +00:00
return admin_service.get_shouts(limit, offset, search, status, community)
2025-06-28 10:47:08 +00:00
except Exception as e:
2025-07-02 21:20:10 +00:00
raise handle_error("получении списка публикаций", e) from e
2025-06-28 10:47:08 +00:00
@mutation.field("adminUpdateShout")
@admin_auth_required
async def admin_update_shout(_: None, info: GraphQLResolveInfo, shout: dict[str, Any]) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Обновляет публикацию через editor.py"""
2025-06-28 10:47:08 +00:00
try:
from resolvers.editor import update_shout
shout_id = shout.get("id")
if not shout_id:
return {"success": False, "error": "ID публикации не указан"}
shout_input = {k: v for k, v in shout.items() if k != "id"}
result = await update_shout(None, info, shout_id, shout_input)
if result.error:
return {"success": False, "error": result.error}
logger.info(f"Публикация {shout_id} обновлена через админ-панель")
return {"success": True}
except Exception as e:
2025-07-02 21:20:10 +00:00
logger.error(f"Ошибка обновления публикации: {e}")
return {"success": False, "error": str(e)}
2025-06-28 10:47:08 +00:00
@mutation.field("adminDeleteShout")
@admin_auth_required
async def admin_delete_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Удаляет публикацию через editor.py"""
2025-06-28 10:47:08 +00:00
try:
from resolvers.editor import delete_shout
result = await delete_shout(None, info, shout_id)
if result.error:
return {"success": False, "error": result.error}
logger.info(f"Публикация {shout_id} удалена через админ-панель")
return {"success": True}
except Exception as e:
2025-07-02 21:20:10 +00:00
logger.error(f"Ошибка удаления публикации: {e}")
return {"success": False, "error": str(e)}
2025-06-28 10:47:08 +00:00
@mutation.field("adminRestoreShout")
@admin_auth_required
2025-07-02 21:20:10 +00:00
async def admin_restore_shout(_: None, _info: GraphQLResolveInfo, shout_id: int) -> dict[str, Any]:
"""Восстанавливает удаленную публикацию"""
2025-06-28 10:47:08 +00:00
try:
2025-07-02 21:20:10 +00:00
return admin_service.restore_shout(shout_id)
2025-06-28 10:47:08 +00:00
except Exception as e:
2025-07-02 21:20:10 +00:00
logger.error(f"Ошибка восстановления публикации: {e}")
return {"success": False, "error": str(e)}
2025-06-30 19:19:46 +00:00
2025-07-02 21:20:10 +00:00
# === ПРИГЛАШЕНИЯ ===
2025-06-30 19:19:46 +00:00
@query.field("adminGetInvites")
@admin_auth_required
async def admin_get_invites(
2025-07-02 19:30:21 +00:00
_: None, _info: GraphQLResolveInfo, limit: int = 20, offset: int = 0, search: str = "", status: str = "all"
2025-06-30 19:19:46 +00:00
) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Получает список приглашений"""
2025-06-30 19:19:46 +00:00
try:
2025-07-02 21:20:10 +00:00
return admin_service.get_invites(limit, offset, search, status)
2025-06-30 19:19:46 +00:00
except Exception as e:
2025-07-02 21:20:10 +00:00
raise handle_error("получении списка приглашений", e) from e
2025-06-30 19:19:46 +00:00
@mutation.field("adminUpdateInvite")
@admin_auth_required
async def admin_update_invite(_: None, _info: GraphQLResolveInfo, invite: dict[str, Any]) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Обновляет приглашение"""
2025-06-30 19:19:46 +00:00
try:
2025-07-02 21:20:10 +00:00
return admin_service.update_invite(invite)
2025-06-30 19:19:46 +00:00
except Exception as e:
2025-07-02 21:20:10 +00:00
logger.error(f"Ошибка обновления приглашения: {e}")
return {"success": False, "error": str(e)}
2025-06-30 19:19:46 +00:00
@mutation.field("adminDeleteInvite")
@admin_auth_required
async def admin_delete_invite(
_: None, _info: GraphQLResolveInfo, inviter_id: int, author_id: int, shout_id: int
) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Удаляет приглашение"""
2025-06-30 19:19:46 +00:00
try:
2025-07-02 21:20:10 +00:00
return admin_service.delete_invite(inviter_id, author_id, shout_id)
except Exception as e:
logger.error(f"Ошибка удаления приглашения: {e}")
return {"success": False, "error": str(e)}
2025-06-30 19:19:46 +00:00
2025-07-02 21:20:10 +00:00
# === ТОПИКИ ===
2025-06-30 19:19:46 +00:00
2025-07-02 21:20:10 +00:00
@query.field("adminGetTopics")
@admin_auth_required
async def admin_get_topics(_: None, _info: GraphQLResolveInfo, community_id: int) -> list[dict[str, Any]]:
"""Получает все топики сообщества для админ-панели"""
try:
from orm.topic import Topic
from services.db import local_session
with local_session() as session:
# Получаем все топики сообщества без лимитов
topics = session.query(Topic).filter(Topic.community == community_id).order_by(Topic.id).all()
2025-06-30 19:19:46 +00:00
2025-07-02 21:20:10 +00:00
# Сериализуем топики в простой формат для админки
result: list[dict[str, Any]] = [
{
"id": topic.id,
"title": topic.title or "",
"slug": topic.slug or f"topic-{topic.id}",
"body": topic.body or "",
"community": topic.community,
"parent_ids": topic.parent_ids or [],
"pic": topic.pic,
"oid": getattr(topic, "oid", None),
"is_main": getattr(topic, "is_main", False),
}
for topic in topics
]
2025-07-03 09:15:10 +00:00
logger.info(f"Загружено топиков для сообщества: {len(result)}")
2025-07-02 21:20:10 +00:00
return result
2025-06-30 19:19:46 +00:00
except Exception as e:
2025-07-02 21:20:10 +00:00
raise handle_error("получении списка топиков", e) from e
2025-06-30 20:37:21 +00:00
2025-07-03 09:15:10 +00:00
@mutation.field("adminUpdateTopic")
@admin_auth_required
async def admin_update_topic(_: None, _info: GraphQLResolveInfo, topic: dict[str, Any]) -> dict[str, Any]:
"""Обновляет топик через админ-панель"""
try:
from orm.topic import Topic
from resolvers.topic import invalidate_topics_cache
from services.db import local_session
from services.redis import redis
topic_id = topic.get("id")
if not topic_id:
return {"success": False, "error": "ID топика не указан"}
with local_session() as session:
existing_topic = session.query(Topic).filter(Topic.id == topic_id).first()
if not existing_topic:
return {"success": False, "error": "Топик не найден"}
# Сохраняем старый slug для удаления из кеша
old_slug = str(getattr(existing_topic, "slug", ""))
# Обновляем поля топика
for key, value in topic.items():
if key != "id" and hasattr(existing_topic, key):
setattr(existing_topic, key, value)
session.add(existing_topic)
session.commit()
# Инвалидируем кеш
await invalidate_topics_cache(topic_id)
# Если slug изменился, удаляем старый ключ
new_slug = str(getattr(existing_topic, "slug", ""))
if old_slug != new_slug:
await redis.execute("DEL", f"topic:slug:{old_slug}")
logger.debug(f"Удален ключ кеша для старого slug: {old_slug}")
logger.info(f"Топик {topic_id} обновлен через админ-панель")
return {"success": True, "topic": existing_topic}
except Exception as e:
logger.error(f"Ошибка обновления топика: {e}")
return {"success": False, "error": str(e)}
@mutation.field("adminCreateTopic")
@admin_auth_required
async def admin_create_topic(_: None, _info: GraphQLResolveInfo, topic: dict[str, Any]) -> dict[str, Any]:
"""Создает новый топик через админ-панель"""
try:
from orm.topic import Topic
from resolvers.topic import invalidate_topics_cache
from services.db import local_session
with local_session() as session:
# Создаем новый топик
new_topic = Topic(**topic)
session.add(new_topic)
session.commit()
# Инвалидируем кеш всех тем
await invalidate_topics_cache()
logger.info(f"Топик {new_topic.id} создан через админ-панель")
return {"success": True, "topic": new_topic}
except Exception as e:
logger.error(f"Ошибка создания топика: {e}")
return {"success": False, "error": str(e)}
@mutation.field("adminMergeTopics")
@admin_auth_required
async def admin_merge_topics(_: None, _info: GraphQLResolveInfo, merge_input: dict[str, Any]) -> dict[str, Any]:
"""
Административное слияние топиков с переносом всех публикаций и подписчиков
Args:
merge_input: Данные для слияния:
- target_topic_id: ID целевой темы (в которую сливаем)
- source_topic_ids: Список ID исходных тем (которые сливаем)
- preserve_target_properties: Сохранить свойства целевой темы
Returns:
dict: Результат операции с информацией о слиянии
"""
try:
from orm.draft import DraftTopic
from orm.shout import ShoutTopic
from orm.topic import Topic, TopicFollower
from resolvers.topic import invalidate_topic_followers_cache, invalidate_topics_cache
from services.db import local_session
from services.redis import redis
target_topic_id = merge_input["target_topic_id"]
source_topic_ids = merge_input["source_topic_ids"]
preserve_target = merge_input.get("preserve_target_properties", True)
# Проверяем что ID не пересекаются
if target_topic_id in source_topic_ids:
return {"success": False, "error": "Целевая тема не может быть в списке исходных тем"}
with local_session() as session:
# Получаем целевую тему
target_topic = session.query(Topic).filter(Topic.id == target_topic_id).first()
if not target_topic:
return {"success": False, "error": f"Целевая тема с ID {target_topic_id} не найдена"}
# Получаем исходные темы
source_topics = session.query(Topic).filter(Topic.id.in_(source_topic_ids)).all()
if len(source_topics) != len(source_topic_ids):
found_ids = [t.id for t in source_topics]
missing_ids = [topic_id for topic_id in source_topic_ids if topic_id not in found_ids]
return {"success": False, "error": f"Исходные темы с ID {missing_ids} не найдены"}
# Проверяем что все темы принадлежат одному сообществу
target_community = target_topic.community
for source_topic in source_topics:
if source_topic.community != target_community:
return {"success": False, "error": f"Тема '{source_topic.title}' принадлежит другому сообществу"}
# Собираем статистику для отчета
merge_stats = {"followers_moved": 0, "publications_moved": 0, "drafts_moved": 0, "source_topics_deleted": 0}
# Переносим подписчиков из исходных тем в целевую
for source_topic in source_topics:
# Получаем подписчиков исходной темы
source_followers = session.query(TopicFollower).filter(TopicFollower.topic == source_topic.id).all()
for follower in source_followers:
# Проверяем, не подписан ли уже пользователь на целевую тему
existing = (
session.query(TopicFollower)
.filter(TopicFollower.topic == target_topic_id, TopicFollower.follower == follower.follower)
.first()
)
if not existing:
# Создаем новую подписку на целевую тему
new_follower = TopicFollower(
topic=target_topic_id,
follower=follower.follower,
created_at=follower.created_at,
auto=follower.auto,
)
session.add(new_follower)
merge_stats["followers_moved"] += 1
# Удаляем старую подписку
session.delete(follower)
# Переносим публикации из исходных тем в целевую
for source_topic in source_topics:
# Получаем связи публикаций с исходной темой
shout_topics = session.query(ShoutTopic).filter(ShoutTopic.topic == source_topic.id).all()
for shout_topic in shout_topics:
# Проверяем, не связана ли уже публикация с целевой темой
existing = (
session.query(ShoutTopic)
.filter(ShoutTopic.topic == target_topic_id, ShoutTopic.shout == shout_topic.shout)
.first()
)
if not existing:
# Создаем новую связь с целевой темой
new_shout_topic = ShoutTopic(
topic=target_topic_id, shout=shout_topic.shout, main=shout_topic.main
)
session.add(new_shout_topic)
merge_stats["publications_moved"] += 1
# Удаляем старую связь
session.delete(shout_topic)
# Переносим черновики из исходных тем в целевую
for source_topic in source_topics:
# Получаем связи черновиков с исходной темой
draft_topics = session.query(DraftTopic).filter(DraftTopic.topic == source_topic.id).all()
for draft_topic in draft_topics:
# Проверяем, не связан ли уже черновик с целевой темой
existing = (
session.query(DraftTopic)
.filter(DraftTopic.topic == target_topic_id, DraftTopic.shout == draft_topic.shout)
.first()
)
if not existing:
# Создаем новую связь с целевой темой
new_draft_topic = DraftTopic(
topic=target_topic_id, shout=draft_topic.shout, main=draft_topic.main
)
session.add(new_draft_topic)
merge_stats["drafts_moved"] += 1
# Удаляем старую связь
session.delete(draft_topic)
# Обновляем parent_ids дочерних топиков
for source_topic in source_topics:
# Находим всех детей исходной темы
child_topics = session.query(Topic).filter(Topic.parent_ids.contains(int(source_topic.id))).all() # type: ignore[arg-type]
for child_topic in child_topics:
current_parent_ids = list(child_topic.parent_ids or [])
# Заменяем ID исходной темы на ID целевой темы
updated_parent_ids = [
target_topic_id if parent_id == source_topic.id else parent_id
for parent_id in current_parent_ids
]
child_topic.parent_ids = updated_parent_ids
# Объединяем parent_ids если не сохраняем только целевые свойства
if not preserve_target:
current_parent_ids = list(target_topic.parent_ids or [])
all_parent_ids = set(current_parent_ids)
for source_topic in source_topics:
source_parent_ids = list(source_topic.parent_ids or [])
if source_parent_ids:
all_parent_ids.update(source_parent_ids)
# Убираем IDs исходных тем из parent_ids
all_parent_ids.discard(target_topic_id)
for source_id in source_topic_ids:
all_parent_ids.discard(source_id)
target_topic.parent_ids = list(all_parent_ids) if all_parent_ids else []
# Инвалидируем кеши ПЕРЕД удалением тем
for source_topic in source_topics:
await invalidate_topic_followers_cache(int(source_topic.id))
if source_topic.slug:
await redis.execute("DEL", f"topic:slug:{source_topic.slug}")
await redis.execute("DEL", f"topic:id:{source_topic.id}")
# Удаляем исходные темы
for source_topic in source_topics:
session.delete(source_topic)
merge_stats["source_topics_deleted"] += 1
logger.info(f"Удалена исходная тема: {source_topic.title} (ID: {source_topic.id})")
# Сохраняем изменения
session.commit()
# Инвалидируем кеши целевой темы и общие кеши
await invalidate_topics_cache(target_topic_id)
await invalidate_topic_followers_cache(target_topic_id)
logger.info(f"Успешно слиты темы {source_topic_ids} в тему {target_topic_id} через админ-панель")
logger.info(f"Статистика слияния: {merge_stats}")
return {
"success": True,
"topic": target_topic,
"message": f"Успешно слито {len(source_topics)} тем в '{target_topic.title}'",
"stats": merge_stats,
}
except Exception as e:
logger.error(f"Ошибка при слиянии тем через админ-панель: {e}")
return {"success": False, "error": f"Ошибка при слиянии тем: {e}"}
2025-07-02 21:20:10 +00:00
# === ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ ===
2025-06-30 20:37:21 +00:00
2025-07-02 21:20:10 +00:00
@query.field("getEnvVariables")
@admin_auth_required
async def get_env_variables(_: None, _info: GraphQLResolveInfo) -> list[dict[str, Any]]:
"""Получает переменные окружения"""
2025-06-30 20:37:21 +00:00
try:
2025-07-02 21:20:10 +00:00
return await admin_service.get_env_variables()
except Exception as e:
2025-07-03 09:15:10 +00:00
logger.error(f"Ошибка получения переменных окружения: {e}")
raise GraphQLError("Не удалось получить переменные окружения") from e
2025-06-30 20:37:21 +00:00
2025-07-02 21:20:10 +00:00
@mutation.field("updateEnvVariable")
@admin_auth_required
async def update_env_variable(_: None, _info: GraphQLResolveInfo, key: str, value: str) -> dict[str, Any]:
"""Обновляет переменную окружения"""
return await admin_service.update_env_variable(key, value)
2025-06-30 20:37:21 +00:00
2025-07-02 21:20:10 +00:00
@mutation.field("updateEnvVariables")
@admin_auth_required
async def update_env_variables(_: None, _info: GraphQLResolveInfo, variables: list[dict[str, Any]]) -> dict[str, Any]:
"""Массовое обновление переменных окружения"""
return await admin_service.update_env_variables(variables)
2025-06-30 20:37:21 +00:00
2025-07-02 21:20:10 +00:00
# === РОЛИ ===
2025-06-30 20:37:21 +00:00
2025-07-02 21:20:10 +00:00
@query.field("adminGetRoles")
@admin_auth_required
async def admin_get_roles(_: None, _info: GraphQLResolveInfo, community: int = None) -> list[dict[str, Any]]:
"""Получает список ролей"""
try:
return admin_service.get_roles(community)
2025-06-30 20:37:21 +00:00
except Exception as e:
2025-07-03 09:15:10 +00:00
logger.error(f"Ошибка получения ролей: {e}")
raise GraphQLError("Не удалось получить роли") from e
2025-07-02 21:20:10 +00:00
# === ЗАГЛУШКИ ДЛЯ ОСТАЛЬНЫХ РЕЗОЛВЕРОВ ===
# [предположение] Эти резолверы пока оставляем как есть, но их тоже нужно будет упростить
2025-07-02 19:30:21 +00:00
@query.field("adminGetUserCommunityRoles")
@admin_auth_required
async def admin_get_user_community_roles(
2025-07-02 21:20:10 +00:00
_: None, _info: GraphQLResolveInfo, author_id: int, community_id: int
2025-07-02 19:30:21 +00:00
) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Получает роли пользователя в сообществе"""
# [непроверенное] Временная заглушка - нужно вынести в сервис
from orm.community import CommunityAuthor
from services.db import local_session
2025-07-02 19:30:21 +00:00
try:
with local_session() as session:
community_author = (
session.query(CommunityAuthor)
.filter(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
.first()
)
roles = []
if community_author and community_author.roles:
roles = [role.strip() for role in community_author.roles.split(",") if role.strip()]
return {"author_id": author_id, "community_id": community_id, "roles": roles}
except Exception as e:
2025-07-02 21:20:10 +00:00
raise handle_error("получении ролей пользователя в сообществе", e) from e
2025-07-02 19:30:21 +00:00
@query.field("adminGetCommunityMembers")
@admin_auth_required
async def admin_get_community_members(
2025-07-02 21:20:10 +00:00
_: None, _info: GraphQLResolveInfo, community_id: int, limit: int = 20, offset: int = 0
2025-07-02 19:30:21 +00:00
) -> dict[str, Any]:
2025-07-02 21:20:10 +00:00
"""Получает участников сообщества"""
# [непроверенное] Временная заглушка - нужно вынести в сервис
from sqlalchemy.sql import func
2025-07-02 19:30:21 +00:00
2025-07-02 21:20:10 +00:00
from auth.orm import Author
from orm.community import CommunityAuthor
from services.db import local_session
2025-07-02 19:30:21 +00:00
try:
with local_session() as session:
members_query = (
session.query(Author, CommunityAuthor)
.join(CommunityAuthor, Author.id == CommunityAuthor.author_id)
.filter(CommunityAuthor.community_id == community_id)
.offset(offset)
.limit(limit)
)
members = []
for author, community_author in members_query:
roles = []
if community_author.roles:
roles = [role.strip() for role in community_author.roles.split(",") if role.strip()]
members.append(
{
"id": author.id,
"name": author.name,
"email": author.email,
"slug": author.slug,
"roles": roles,
}
)
total = (
session.query(func.count(CommunityAuthor.author_id))
.filter(CommunityAuthor.community_id == community_id)
.scalar()
)
return {"members": members, "total": total, "community_id": community_id}
except Exception as e:
logger.error(f"Ошибка получения участников сообщества: {e}")
return {"members": [], "total": 0, "community_id": community_id}
@query.field("adminGetCommunityRoleSettings")
@admin_auth_required
2025-07-02 21:20:10 +00:00
async def admin_get_community_role_settings(_: None, _info: GraphQLResolveInfo, community_id: int) -> dict[str, Any]:
"""Получает настройки ролей сообщества"""
# [непроверенное] Временная заглушка - нужно вынести в сервис
from orm.community import Community
from services.db import local_session
2025-07-02 19:30:21 +00:00
try:
with local_session() as session:
community = session.query(Community).filter(Community.id == community_id).first()
if not community:
return {
"community_id": community_id,
"default_roles": ["reader"],
"available_roles": ["reader", "author", "artist", "expert", "editor", "admin"],
"error": "Сообщество не найдено",
}
return {
"community_id": community_id,
"default_roles": community.get_default_roles(),
"available_roles": community.get_available_roles(),
"error": None,
}
except Exception as e:
2025-07-02 21:20:10 +00:00
logger.error(f"Ошибка получения настроек ролей: {e}")
2025-07-02 19:30:21 +00:00
return {
"community_id": community_id,
"default_roles": ["reader"],
"available_roles": ["reader", "author", "artist", "expert", "editor", "admin"],
"error": str(e),
}