upgrade schema, resolvers, panel added
This commit is contained in:
158
services/auth.py
158
services/auth.py
@@ -1,120 +1,90 @@
|
||||
from functools import wraps
|
||||
from typing import Tuple
|
||||
|
||||
from cache.cache import get_cached_author_by_user_id
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.schema import request_graphql_data
|
||||
from settings import ADMIN_SECRET, AUTH_URL
|
||||
from utils.logger import root_logger as logger
|
||||
from auth.internal import verify_internal_auth
|
||||
from sqlalchemy import exc
|
||||
from services.db import local_session
|
||||
from auth.orm import Author, Role
|
||||
|
||||
# Список разрешенных заголовков
|
||||
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
|
||||
|
||||
|
||||
async def check_auth(req):
|
||||
async def check_auth(req) -> Tuple[str, list[str]]:
|
||||
"""
|
||||
Проверка авторизации пользователя.
|
||||
|
||||
Эта функция проверяет токен авторизации, переданный в заголовках запроса,
|
||||
и возвращает идентификатор пользователя и его роли.
|
||||
Проверяет токен и получает данные из локальной БД.
|
||||
|
||||
Параметры:
|
||||
- req: Входящий GraphQL запрос, содержащий заголовок авторизации.
|
||||
|
||||
Возвращает:
|
||||
- user_id: str - Идентификатор пользователя.
|
||||
- user_roles: list[str] - Список ролей пользователя.
|
||||
- user_id: str - Идентификатор пользователя
|
||||
- user_roles: list[str] - Список ролей пользователя
|
||||
"""
|
||||
# Проверяем наличие токена
|
||||
token = req.headers.get("Authorization")
|
||||
if not token:
|
||||
return "", []
|
||||
|
||||
host = req.headers.get("host", "")
|
||||
logger.debug(f"check_auth: host={host}")
|
||||
auth_url = AUTH_URL
|
||||
if ".dscrs.site" in host or "localhost" in host:
|
||||
auth_url = "https://auth.dscrs.site/graphql"
|
||||
user_id = ""
|
||||
user_roles = []
|
||||
if token:
|
||||
# Проверяем и очищаем токен от префикса Bearer если он есть
|
||||
if token.startswith("Bearer "):
|
||||
token = token.split("Bearer ")[-1].strip()
|
||||
# Logging the authentication token
|
||||
logger.debug(f"TOKEN: {token}")
|
||||
query_name = "validate_jwt_token"
|
||||
operation = "ValidateToken"
|
||||
variables = {"params": {"token_type": "access_token", "token": token}}
|
||||
# Очищаем токен от префикса Bearer если он есть
|
||||
if token.startswith("Bearer "):
|
||||
token = token.split("Bearer ")[-1].strip()
|
||||
|
||||
# Только необходимые заголовки для GraphQL запроса
|
||||
headers = {"Content-Type": "application/json"}
|
||||
logger.debug(f"Checking auth token: {token[:10]}...")
|
||||
|
||||
gql = {
|
||||
"query": f"query {operation}($params: ValidateJWTTokenInput!)"
|
||||
+ "{"
|
||||
+ f"{query_name}(params: $params) {{ is_valid claims }} "
|
||||
+ "}",
|
||||
"variables": variables,
|
||||
"operationName": operation,
|
||||
}
|
||||
data = await request_graphql_data(gql, url=auth_url, headers=headers)
|
||||
if data:
|
||||
logger.debug(f"Auth response: {data}")
|
||||
validation_result = data.get("data", {}).get(query_name, {})
|
||||
logger.debug(f"Validation result: {validation_result}")
|
||||
is_valid = validation_result.get("is_valid", False)
|
||||
if not is_valid:
|
||||
logger.error(f"Token validation failed: {validation_result}")
|
||||
return "", []
|
||||
user_data = validation_result.get("claims", {})
|
||||
logger.debug(f"User claims: {user_data}")
|
||||
user_id = user_data.get("sub", "")
|
||||
user_roles = user_data.get("allowed_roles", [])
|
||||
return user_id, user_roles
|
||||
# Проверяем авторизацию внутренним механизмом
|
||||
logger.debug("Using internal authentication")
|
||||
return await verify_internal_auth(token)
|
||||
|
||||
|
||||
async def add_user_role(user_id):
|
||||
async def add_user_role(user_id: str, roles: list[str] = None):
|
||||
"""
|
||||
Добавление роли пользователя.
|
||||
Добавление ролей пользователю в локальной БД.
|
||||
|
||||
Эта функция добавляет роли "author" и "reader" для указанного пользователя
|
||||
в системе авторизации.
|
||||
|
||||
Параметры:
|
||||
- user_id: str - Идентификатор пользователя, которому нужно добавить роли.
|
||||
|
||||
Возвращает:
|
||||
- user_id: str - Идентификатор пользователя, если операция прошла успешно.
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
roles: Список ролей для добавления. По умолчанию ["author", "reader"]
|
||||
"""
|
||||
logger.info(f"add author role for user_id: {user_id}")
|
||||
query_name = "_update_user"
|
||||
operation = "UpdateUserRoles"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"x-authorizer-admin-secret": ADMIN_SECRET,
|
||||
}
|
||||
variables = {"params": {"roles": "author, reader", "id": user_id}}
|
||||
gql = {
|
||||
"query": f"mutation {operation}($params: UpdateUserInput!) {{ {query_name}(params: $params) {{ id roles }} }}",
|
||||
"variables": variables,
|
||||
"operationName": operation,
|
||||
}
|
||||
data = await request_graphql_data(gql, headers=headers)
|
||||
if data:
|
||||
user_id = data.get("data", {}).get(query_name, {}).get("id")
|
||||
return user_id
|
||||
if not roles:
|
||||
roles = ["author", "reader"]
|
||||
|
||||
logger.info(f"Adding roles {roles} to user {user_id}")
|
||||
|
||||
logger.debug("Using local authentication")
|
||||
with local_session() as session:
|
||||
try:
|
||||
author = session.query(Author).filter(Author.id == user_id).one()
|
||||
|
||||
# Получаем существующие роли
|
||||
existing_roles = set(role.name for role in author.roles)
|
||||
|
||||
# Добавляем новые роли
|
||||
for role_name in roles:
|
||||
if role_name not in existing_roles:
|
||||
# Получаем или создаем роль
|
||||
role = session.query(Role).filter(Role.name == role_name).first()
|
||||
if not role:
|
||||
role = Role(id=role_name, name=role_name)
|
||||
session.add(role)
|
||||
|
||||
# Добавляем роль автору
|
||||
author.roles.append(role)
|
||||
|
||||
session.commit()
|
||||
return user_id
|
||||
|
||||
except exc.NoResultFound:
|
||||
logger.error(f"Author {user_id} not found")
|
||||
return None
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""
|
||||
Декоратор для проверки авторизации пользователя.
|
||||
|
||||
Этот декоратор проверяет, авторизован ли пользователь, <20><> добавляет
|
||||
информацию о пользователе в контекст функции.
|
||||
|
||||
Параметры:
|
||||
- f: Функция, которую нужно декорировать.
|
||||
|
||||
Возвращает:
|
||||
- Обернутую функцию с добавленной проверкой авторизации.
|
||||
"""
|
||||
"""Декоратор для проверки авторизации пользователя."""
|
||||
|
||||
@wraps(f)
|
||||
async def decorated_function(*args, **kwargs):
|
||||
@@ -135,18 +105,7 @@ def login_required(f):
|
||||
|
||||
|
||||
def login_accepted(f):
|
||||
"""
|
||||
Декоратор для добавления данных авторизации в контекст.
|
||||
|
||||
Этот декоратор добавляет данные авторизации в контекст, если они доступны,
|
||||
но не блокирует доступ для неавторизованных пользователей.
|
||||
|
||||
Параметры:
|
||||
- f: Функция, которую нужно декорировать.
|
||||
|
||||
Возвращает:
|
||||
- Обернутую функцию с добавленной проверкой авторизации.
|
||||
"""
|
||||
"""Декоратор для добавления данных авторизации в контекст."""
|
||||
|
||||
@wraps(f)
|
||||
async def decorated_function(*args, **kwargs):
|
||||
@@ -166,12 +125,11 @@ def login_accepted(f):
|
||||
author = await get_cached_author_by_user_id(user_id, get_with_stat)
|
||||
if author:
|
||||
logger.debug(f"login_accepted: Найден профиль автора: {author}")
|
||||
# Предполагается, что `author` является объектом с атрибутом `id`
|
||||
info.context["author"] = author.dict()
|
||||
else:
|
||||
logger.error(
|
||||
f"login_accepted: Профиль автора не найден для пользователя {user_id}. Используем базовые данные."
|
||||
) # Используем базовую информацию об автор
|
||||
)
|
||||
else:
|
||||
logger.debug("login_accepted: Пользователь не авторизован. Очищаем контекст.")
|
||||
info.context["user_id"] = None
|
||||
|
@@ -50,10 +50,25 @@ FILTERED_FIELDS = ["_sa_instance_state", "search_vector"]
|
||||
|
||||
|
||||
def create_table_if_not_exists(engine, table):
|
||||
"""
|
||||
Создает таблицу, если она не существует в базе данных.
|
||||
|
||||
Args:
|
||||
engine: SQLAlchemy движок базы данных
|
||||
table: Класс модели SQLAlchemy
|
||||
"""
|
||||
inspector = inspect(engine)
|
||||
if table and not inspector.has_table(table.__tablename__):
|
||||
table.__table__.create(engine)
|
||||
logger.info(f"Table '{table.__tablename__}' created.")
|
||||
try:
|
||||
table.__table__.create(engine)
|
||||
logger.info(f"Table '{table.__tablename__}' created.")
|
||||
except exc.OperationalError as e:
|
||||
# Проверяем, содержит ли ошибка упоминание о том, что индекс уже существует
|
||||
if "already exists" in str(e):
|
||||
logger.warning(f"Skipping index creation for table '{table.__tablename__}': {e}")
|
||||
else:
|
||||
# Перевыбрасываем ошибку, если она не связана с дублированием
|
||||
raise
|
||||
else:
|
||||
logger.info(f"Table '{table.__tablename__}' ok.")
|
||||
|
||||
@@ -154,21 +169,43 @@ class Base(declarative_base()):
|
||||
REGISTRY[cls.__name__] = cls
|
||||
|
||||
def dict(self) -> 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:
|
||||
value = getattr(self, column_name)
|
||||
# Check if the value is JSON and decode it if necessary
|
||||
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.error(f"Error decoding JSON for column '{column_name}': {e}")
|
||||
data[column_name] = value
|
||||
else:
|
||||
data[column_name] = value
|
||||
# Add synthetic field .stat if it exists
|
||||
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.error(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:
|
||||
@@ -186,7 +223,9 @@ class Base(declarative_base()):
|
||||
|
||||
|
||||
# Функция для вывода полного трейсбека при предупреждениях
|
||||
def warning_with_traceback(message: Warning | str, category, filename: str, lineno: int, file=None, line=None):
|
||||
def warning_with_traceback(
|
||||
message: Warning | str, category, filename: str, lineno: int, file=None, line=None
|
||||
):
|
||||
tb = traceback.format_stack()
|
||||
tb_str = "".join(tb)
|
||||
return f"{message} ({filename}, {lineno}): {category.__name__}\n{tb_str}"
|
||||
|
111
services/env.py
Normal file
111
services/env.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from redis import Redis
|
||||
from settings import REDIS_URL
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvVariable:
|
||||
key: str
|
||||
value: str
|
||||
description: Optional[str] = None
|
||||
type: str = "string"
|
||||
is_secret: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvSection:
|
||||
name: str
|
||||
variables: List[EnvVariable]
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class EnvManager:
|
||||
"""
|
||||
Менеджер переменных окружения с хранением в Redis
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.redis = Redis.from_url(REDIS_URL)
|
||||
self.prefix = "env:"
|
||||
|
||||
def get_all_variables(self) -> List[EnvSection]:
|
||||
"""
|
||||
Получение всех переменных окружения, сгруппированных по секциям
|
||||
"""
|
||||
try:
|
||||
# Получаем все ключи с префиксом env:
|
||||
keys = self.redis.keys(f"{self.prefix}*")
|
||||
variables: Dict[str, str] = {}
|
||||
|
||||
for key in keys:
|
||||
var_key = key.decode("utf-8").replace(self.prefix, "")
|
||||
value = self.redis.get(key)
|
||||
if value:
|
||||
variables[var_key] = value.decode("utf-8")
|
||||
|
||||
# Группируем переменные по секциям
|
||||
sections = [
|
||||
EnvSection(
|
||||
name="Авторизация",
|
||||
description="Настройки системы авторизации",
|
||||
variables=[
|
||||
EnvVariable(
|
||||
key="JWT_SECRET",
|
||||
value=variables.get("JWT_SECRET", ""),
|
||||
description="Секретный ключ для JWT токенов",
|
||||
type="string",
|
||||
is_secret=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
EnvSection(
|
||||
name="Redis",
|
||||
description="Настройки подключения к Redis",
|
||||
variables=[
|
||||
EnvVariable(
|
||||
key="REDIS_URL",
|
||||
value=variables.get("REDIS_URL", ""),
|
||||
description="URL подключения к Redis",
|
||||
type="string",
|
||||
)
|
||||
],
|
||||
),
|
||||
# Добавьте другие секции по необходимости
|
||||
]
|
||||
|
||||
return sections
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения переменных: {e}")
|
||||
return []
|
||||
|
||||
def update_variable(self, key: str, value: str) -> bool:
|
||||
"""
|
||||
Обновление значения переменной
|
||||
"""
|
||||
try:
|
||||
full_key = f"{self.prefix}{key}"
|
||||
self.redis.set(full_key, value)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления переменной {key}: {e}")
|
||||
return False
|
||||
|
||||
def update_variables(self, variables: List[EnvVariable]) -> bool:
|
||||
"""
|
||||
Массовое обновление переменных
|
||||
"""
|
||||
try:
|
||||
pipe = self.redis.pipeline()
|
||||
for var in variables:
|
||||
full_key = f"{self.prefix}{var.key}"
|
||||
pipe.set(full_key, var.value)
|
||||
pipe.execute()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка массового обновления переменных: {e}")
|
||||
return False
|
||||
|
||||
|
||||
env_manager = EnvManager()
|
@@ -14,4 +14,7 @@ class ExceptionHandlerMiddleware(BaseHTTPMiddleware):
|
||||
return response
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
return JSONResponse({"detail": "An error occurred. Please try again later."}, status_code=500)
|
||||
return JSONResponse(
|
||||
{"detail": "An error occurred. Please try again later."},
|
||||
status_code=500,
|
||||
)
|
||||
|
@@ -94,8 +94,7 @@ async def notify_draft(draft_data, action: str = "publish"):
|
||||
# Если переданы связанные атрибуты, добавим их
|
||||
if hasattr(draft_data, "topics") and draft_data.topics is not None:
|
||||
draft_payload["topics"] = [
|
||||
{"id": t.id, "name": t.name, "slug": t.slug}
|
||||
for t in draft_data.topics
|
||||
{"id": t.id, "name": t.name, "slug": t.slug} for t in draft_data.topics
|
||||
]
|
||||
|
||||
if hasattr(draft_data, "authors") and draft_data.authors is not None:
|
||||
|
@@ -40,6 +40,17 @@ class RedisService:
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def pipeline(self):
|
||||
"""
|
||||
Возвращает пайплайн Redis для выполнения нескольких команд в одной транзакции.
|
||||
|
||||
Returns:
|
||||
Pipeline: объект pipeline Redis
|
||||
"""
|
||||
if self._client:
|
||||
return self._client.pipeline()
|
||||
raise Exception("Redis client is not initialized")
|
||||
|
||||
async def subscribe(self, *channels):
|
||||
if self._client:
|
||||
async with self._client.pubsub() as pubsub:
|
||||
@@ -75,6 +86,82 @@ class RedisService:
|
||||
async def get(self, key):
|
||||
return await self.execute("get", key)
|
||||
|
||||
async def delete(self, *keys):
|
||||
"""
|
||||
Удаляет ключи из Redis.
|
||||
|
||||
Args:
|
||||
*keys: Ключи для удаления
|
||||
|
||||
Returns:
|
||||
int: Количество удаленных ключей
|
||||
"""
|
||||
if not self._client or not keys:
|
||||
return 0
|
||||
return await self._client.delete(*keys)
|
||||
|
||||
async def hmset(self, key, mapping):
|
||||
"""
|
||||
Устанавливает несколько полей хеша.
|
||||
|
||||
Args:
|
||||
key: Ключ хеша
|
||||
mapping: Словарь с полями и значениями
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
await self._client.hset(key, mapping=mapping)
|
||||
|
||||
async def expire(self, key, seconds):
|
||||
"""
|
||||
Устанавливает время жизни ключа.
|
||||
|
||||
Args:
|
||||
key: Ключ
|
||||
seconds: Время жизни в секундах
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
await self._client.expire(key, seconds)
|
||||
|
||||
async def sadd(self, key, *values):
|
||||
"""
|
||||
Добавляет значения в множество.
|
||||
|
||||
Args:
|
||||
key: Ключ множества
|
||||
*values: Значения для добавления
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
await self._client.sadd(key, *values)
|
||||
|
||||
async def srem(self, key, *values):
|
||||
"""
|
||||
Удаляет значения из множества.
|
||||
|
||||
Args:
|
||||
key: Ключ множества
|
||||
*values: Значения для удаления
|
||||
"""
|
||||
if not self._client:
|
||||
return
|
||||
await self._client.srem(key, *values)
|
||||
|
||||
async def smembers(self, key):
|
||||
"""
|
||||
Получает все элементы множества.
|
||||
|
||||
Args:
|
||||
key: Ключ множества
|
||||
|
||||
Returns:
|
||||
set: Множество элементов
|
||||
"""
|
||||
if not self._client:
|
||||
return set()
|
||||
return await self._client.smembers(key)
|
||||
|
||||
|
||||
redis = RedisService()
|
||||
|
||||
|
@@ -1,10 +1,8 @@
|
||||
from asyncio.log import logger
|
||||
|
||||
import httpx
|
||||
from ariadne import MutationType, ObjectType, QueryType
|
||||
|
||||
from services.db import create_table_if_not_exists, local_session
|
||||
from settings import AUTH_URL
|
||||
|
||||
query = QueryType()
|
||||
mutation = MutationType()
|
||||
@@ -12,50 +10,19 @@ type_draft = ObjectType("Draft")
|
||||
resolvers = [query, mutation, type_draft]
|
||||
|
||||
|
||||
async def request_graphql_data(gql, url=AUTH_URL, headers=None):
|
||||
"""
|
||||
Выполняет GraphQL запрос к указанному URL
|
||||
|
||||
:param gql: GraphQL запрос
|
||||
:param url: URL для запроса, по умолчанию AUTH_URL
|
||||
:param headers: Заголовки запроса
|
||||
:return: Результат запроса или None в случае ошибки
|
||||
"""
|
||||
if not url:
|
||||
return None
|
||||
if headers is None:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, json=gql, headers=headers)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
errors = data.get("errors")
|
||||
if errors:
|
||||
logger.error(f"{url} response: {data}")
|
||||
else:
|
||||
return data
|
||||
else:
|
||||
logger.error(f"{url}: {response.status_code} {response.text}")
|
||||
except Exception as _e:
|
||||
import traceback
|
||||
|
||||
logger.error(f"request_graphql_data error: {traceback.format_exc()}")
|
||||
return None
|
||||
|
||||
|
||||
def create_all_tables():
|
||||
"""Create all database tables in the correct order."""
|
||||
from orm import author, community, draft, notification, reaction, shout, topic
|
||||
from auth.orm import Author, AuthorFollower, AuthorBookmark, AuthorRating
|
||||
from orm import community, draft, notification, reaction, shout, topic
|
||||
|
||||
# Порядок важен - сначала таблицы без внешних ключей, затем зависимые таблицы
|
||||
models_in_order = [
|
||||
# user.User, # Базовая таблица auth
|
||||
author.Author, # Базовая таблица
|
||||
Author, # Базовая таблица
|
||||
community.Community, # Базовая таблица
|
||||
topic.Topic, # Базовая таблица
|
||||
# Связи для базовых таблиц
|
||||
author.AuthorFollower, # Зависит от Author
|
||||
AuthorFollower, # Зависит от Author
|
||||
community.CommunityFollower, # Зависит от Community
|
||||
topic.TopicFollower, # Зависит от Topic
|
||||
# Черновики (теперь без зависимости от Shout)
|
||||
@@ -70,7 +37,8 @@ def create_all_tables():
|
||||
reaction.Reaction, # Зависит от Author и Shout
|
||||
shout.ShoutReactionsFollower, # Зависит от Shout и Reaction
|
||||
# Дополнительные таблицы
|
||||
author.AuthorRating, # Зависит от Author
|
||||
AuthorRating, # Зависит от Author
|
||||
AuthorBookmark, # Зависит от Author
|
||||
notification.Notification, # Зависит от Author
|
||||
notification.NotificationSeen, # Зависит от Notification
|
||||
# collection.Collection,
|
||||
|
@@ -171,11 +171,16 @@ class SearchService:
|
||||
}
|
||||
asyncio.create_task(self.perform_index(shout, index_body))
|
||||
|
||||
def close(self):
|
||||
if self.client:
|
||||
self.client.close()
|
||||
|
||||
async def perform_index(self, shout, index_body):
|
||||
if self.client:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
self.client.index(index=self.index_name, id=str(shout.id), body=index_body), timeout=40.0
|
||||
self.client.index(index=self.index_name, id=str(shout.id), body=index_body),
|
||||
timeout=40.0,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"Indexing timeout for shout {shout.id}")
|
||||
@@ -188,7 +193,9 @@ class SearchService:
|
||||
|
||||
logger.info(f"Ищем: {text} {offset}+{limit}")
|
||||
search_body = {
|
||||
"query": {"multi_match": {"query": text, "fields": ["title", "lead", "subtitle", "body", "media"]}}
|
||||
"query": {
|
||||
"multi_match": {"query": text, "fields": ["title", "lead", "subtitle", "body", "media"]}
|
||||
}
|
||||
}
|
||||
|
||||
if self.client:
|
||||
|
@@ -14,7 +14,7 @@ from google.analytics.data_v1beta.types import (
|
||||
)
|
||||
from google.analytics.data_v1beta.types import Filter as GAFilter
|
||||
|
||||
from orm.author import Author
|
||||
from auth.orm import Author
|
||||
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
||||
from orm.topic import Topic
|
||||
from services.db import local_session
|
||||
@@ -228,12 +228,20 @@ class ViewedStorage:
|
||||
|
||||
# Обновление тем и авторов с использованием вспомогательной функции
|
||||
for [_st, topic] in (
|
||||
session.query(ShoutTopic, Topic).join(Topic).join(Shout).where(Shout.slug == shout_slug).all()
|
||||
session.query(ShoutTopic, Topic)
|
||||
.join(Topic)
|
||||
.join(Shout)
|
||||
.where(Shout.slug == shout_slug)
|
||||
.all()
|
||||
):
|
||||
update_groups(self.shouts_by_topic, topic.slug, shout_slug)
|
||||
|
||||
for [_st, author] in (
|
||||
session.query(ShoutAuthor, Author).join(Author).join(Shout).where(Shout.slug == shout_slug).all()
|
||||
session.query(ShoutAuthor, Author)
|
||||
.join(Author)
|
||||
.join(Shout)
|
||||
.where(Shout.slug == shout_slug)
|
||||
.all()
|
||||
):
|
||||
update_groups(self.shouts_by_author, author.slug, shout_slug)
|
||||
|
||||
@@ -266,7 +274,9 @@ class ViewedStorage:
|
||||
if failed == 0:
|
||||
when = datetime.now(timezone.utc) + timedelta(seconds=self.period)
|
||||
t = format(when.astimezone().isoformat())
|
||||
logger.info(" ⎩ next update: %s" % (t.split("T")[0] + " " + t.split("T")[1].split(".")[0]))
|
||||
logger.info(
|
||||
" ⎩ next update: %s" % (t.split("T")[0] + " " + t.split("T")[1].split(".")[0])
|
||||
)
|
||||
await asyncio.sleep(self.period)
|
||||
else:
|
||||
await asyncio.sleep(10)
|
||||
|
@@ -1,175 +0,0 @@
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
from asyncio.log import logger
|
||||
|
||||
from sqlalchemy import select
|
||||
from starlette.endpoints import HTTPEndpoint
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from cache.cache import cache_author
|
||||
from orm.author import Author
|
||||
from resolvers.stat import get_with_stat
|
||||
from services.db import local_session
|
||||
from services.schema import request_graphql_data
|
||||
from settings import ADMIN_SECRET, WEBHOOK_SECRET
|
||||
|
||||
|
||||
async def check_webhook_existence():
|
||||
"""
|
||||
Проверяет существование вебхука для user.login события
|
||||
|
||||
Returns:
|
||||
tuple: (bool, str, str) - существует ли вебхук, его id и endpoint если существует
|
||||
"""
|
||||
logger.info("check_webhook_existence called")
|
||||
if not ADMIN_SECRET:
|
||||
logger.error("ADMIN_SECRET is not set")
|
||||
return False, None, None
|
||||
|
||||
headers = {"Content-Type": "application/json", "X-Authorizer-Admin-Secret": ADMIN_SECRET}
|
||||
|
||||
operation = "GetWebhooks"
|
||||
query_name = "_webhooks"
|
||||
variables = {"params": {}}
|
||||
# https://docs.authorizer.dev/core/graphql-api#_webhooks
|
||||
gql = {
|
||||
"query": f"query {operation}($params: PaginatedInput!)"
|
||||
+ "{"
|
||||
+ f"{query_name}(params: $params) {{ webhooks {{ id event_name endpoint }} }} "
|
||||
+ "}",
|
||||
"variables": variables,
|
||||
"operationName": operation,
|
||||
}
|
||||
result = await request_graphql_data(gql, headers=headers)
|
||||
if result:
|
||||
webhooks = result.get("data", {}).get(query_name, {}).get("webhooks", [])
|
||||
logger.info(webhooks)
|
||||
for webhook in webhooks:
|
||||
if webhook["event_name"].startswith("user.login"):
|
||||
return True, webhook["id"], webhook["endpoint"]
|
||||
return False, None, None
|
||||
|
||||
|
||||
async def create_webhook_endpoint():
|
||||
"""
|
||||
Создает вебхук для user.login события.
|
||||
Если существует старый вебхук - удаляет его и создает новый.
|
||||
"""
|
||||
logger.info("create_webhook_endpoint called")
|
||||
|
||||
headers = {"Content-Type": "application/json", "X-Authorizer-Admin-Secret": ADMIN_SECRET}
|
||||
|
||||
exists, webhook_id, current_endpoint = await check_webhook_existence()
|
||||
|
||||
# Определяем endpoint в зависимости от окружения
|
||||
host = os.environ.get("HOST", "core.dscrs.site")
|
||||
endpoint = f"https://{host}/new-author"
|
||||
|
||||
if exists:
|
||||
# Если вебхук существует, но с другим endpoint или с модифицированным именем
|
||||
if current_endpoint != endpoint or webhook_id:
|
||||
# https://docs.authorizer.dev/core/graphql-api#_delete_webhook
|
||||
operation = "DeleteWebhook"
|
||||
query_name = "_delete_webhook"
|
||||
variables = {"params": {"id": webhook_id}} # Изменено с id на webhook_id
|
||||
gql = {
|
||||
"query": f"mutation {operation}($params: WebhookRequest!)"
|
||||
+ "{"
|
||||
+ f"{query_name}(params: $params) {{ message }} "
|
||||
+ "}",
|
||||
"variables": variables,
|
||||
"operationName": operation,
|
||||
}
|
||||
try:
|
||||
await request_graphql_data(gql, headers=headers)
|
||||
exists = False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete webhook: {e}")
|
||||
# Продолжаем выполнение даже при ошибке удаления
|
||||
exists = False
|
||||
else:
|
||||
logger.info(f"Webhook already exists and configured correctly: {webhook_id}")
|
||||
return
|
||||
|
||||
if not exists:
|
||||
# https://docs.authorizer.dev/core/graphql-api#_add_webhook
|
||||
operation = "AddWebhook"
|
||||
query_name = "_add_webhook"
|
||||
variables = {
|
||||
"params": {
|
||||
"event_name": "user.login",
|
||||
"endpoint": endpoint,
|
||||
"enabled": True,
|
||||
"headers": {"Authorization": WEBHOOK_SECRET},
|
||||
}
|
||||
}
|
||||
gql = {
|
||||
"query": f"mutation {operation}($params: AddWebhookRequest!)"
|
||||
+ "{"
|
||||
+ f"{query_name}(params: $params) {{ message }} "
|
||||
+ "}",
|
||||
"variables": variables,
|
||||
"operationName": operation,
|
||||
}
|
||||
try:
|
||||
result = await request_graphql_data(gql, headers=headers)
|
||||
logger.info(result)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create webhook: {e}")
|
||||
|
||||
|
||||
class WebhookEndpoint(HTTPEndpoint):
|
||||
async def post(self, request: Request) -> JSONResponse:
|
||||
try:
|
||||
data = await request.json()
|
||||
if not data:
|
||||
raise HTTPException(status_code=400, detail="Request body is empty")
|
||||
auth = request.headers.get("Authorization")
|
||||
if not auth or auth != os.environ.get("WEBHOOK_SECRET"):
|
||||
raise HTTPException(status_code=401, detail="Invalid Authorization header")
|
||||
# logger.debug(data)
|
||||
user = data.get("user")
|
||||
if not isinstance(user, dict):
|
||||
raise HTTPException(status_code=400, detail="User data is not a dictionary")
|
||||
#
|
||||
name: str = (
|
||||
f"{user.get('given_name', user.get('slug'))} {user.get('middle_name', '')}"
|
||||
+ f"{user.get('family_name', '')}".strip()
|
||||
) or "Аноним"
|
||||
user_id: str = user.get("id", "")
|
||||
email: str = user.get("email", "")
|
||||
pic: str = user.get("picture", "")
|
||||
if user_id:
|
||||
with local_session() as session:
|
||||
author = session.query(Author).filter(Author.user == user_id).first()
|
||||
if not author:
|
||||
# If the author does not exist, create a new one
|
||||
slug: str = email.split("@")[0].replace(".", "-").lower()
|
||||
slug: str = re.sub("[^0-9a-z]+", "-", slug)
|
||||
while True:
|
||||
author = session.query(Author).filter(Author.slug == slug).first()
|
||||
if not author:
|
||||
break
|
||||
slug = f"{slug}-{len(session.query(Author).filter(Author.email == email).all()) + 1}"
|
||||
author = Author(user=user_id, slug=slug, name=name, pic=pic)
|
||||
session.add(author)
|
||||
session.commit()
|
||||
author_query = select(Author).filter(Author.user == user_id)
|
||||
result = get_with_stat(author_query)
|
||||
if result:
|
||||
author_with_stat = result[0]
|
||||
author_dict = author_with_stat.dict()
|
||||
# await cache_author(author_with_stat)
|
||||
asyncio.create_task(cache_author(author_dict))
|
||||
|
||||
return JSONResponse({"status": "success"})
|
||||
except HTTPException as e:
|
||||
return JSONResponse({"status": "error", "message": str(e.detail)}, status_code=e.status_code)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return JSONResponse({"status": "error", "message": str(e)}, status_code=500)
|
Reference in New Issue
Block a user