create_draft fix
This commit is contained in:
parent
ebf9dfcf62
commit
5874d3ccae
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -8,16 +8,16 @@
|
||||||
- Управление пользователями (блокировка, изменение ролей, отключение звука)
|
- Управление пользователями (блокировка, изменение ролей, отключение звука)
|
||||||
- Пагинация и поиск пользователей по email, имени и ID
|
- Пагинация и поиск пользователей по email, имени и ID
|
||||||
- Расширение GraphQL схемы для админки:
|
- Расширение GraphQL схемы для админки:
|
||||||
- Типы AdminUserInfo, AdminUserUpdateInput, AuthResult, Permission, SessionInfo
|
- Типы `AdminUserInfo`, `AdminUserUpdateInput`, `AuthResult`, `Permission`, `SessionInfo`
|
||||||
- Мутации для управления пользователями и авторизации
|
- Мутации для управления пользователями и авторизации
|
||||||
- Улучшения серверной части:
|
- Улучшения серверной части:
|
||||||
- Поддержка HTTPS через Granian с помощью mkcert
|
- Поддержка HTTPS через `Granian` с помощью `mkcert`
|
||||||
- Параметры запуска `--https`, `--workers`, `--domain`
|
- Параметры запуска `--https`, `--workers`, `--domain`
|
||||||
- Система авторизации и аутентификации:
|
- Система авторизации и аутентификации:
|
||||||
- Локальная система аутентификации с сессиями в Redis
|
- Локальная система аутентификации с сессиями в `Redis`
|
||||||
- Система ролей и разрешений (RBAC)
|
- Система ролей и разрешений (RBAC)
|
||||||
- Защита от брутфорс атак
|
- Защита от брутфорс атак
|
||||||
- Поддержка httpOnly cookies для токенов
|
- Поддержка `httpOnly` cookies для токенов
|
||||||
- Мультиязычные email уведомления
|
- Мультиязычные email уведомления
|
||||||
|
|
||||||
### Изменено
|
### Изменено
|
||||||
|
@ -44,6 +44,7 @@
|
||||||
- "Cannot return null for non-nullable field Mutation.login"
|
- "Cannot return null for non-nullable field Mutation.login"
|
||||||
- "Author password is empty" при авторизации
|
- "Author password is empty" при авторизации
|
||||||
- "Author object has no attribute username"
|
- "Author object has no attribute username"
|
||||||
|
- Метод dict() класса Author теперь корректно сериализует роли как список словарей
|
||||||
- Обработка ошибок:
|
- Обработка ошибок:
|
||||||
- Улучшена валидация email и username
|
- Улучшена валидация email и username
|
||||||
- Исправлена обработка истекших токенов
|
- Исправлена обработка истекших токенов
|
||||||
|
@ -258,7 +259,7 @@
|
||||||
#### [0.4.4]
|
#### [0.4.4]
|
||||||
- `followers_stat` removed for shout
|
- `followers_stat` removed for shout
|
||||||
- sqlite3 support added
|
- sqlite3 support added
|
||||||
- `rating_stat` and `comments_count` fixes
|
- `rating_stat` and `commented_stat` fixes
|
||||||
|
|
||||||
#### [0.4.3]
|
#### [0.4.3]
|
||||||
- cache reimplemented
|
- cache reimplemented
|
||||||
|
@ -414,4 +415,22 @@
|
||||||
|
|
||||||
|
|
||||||
#### [0.2.7]
|
#### [0.2.7]
|
||||||
- `loadFollowedReactions` now with `
|
- `loadFollowedReactions` now with `login_required`
|
||||||
|
- notifier service api draft
|
||||||
|
- added `shout` visibility kind in schema
|
||||||
|
- community isolated from author in orm
|
||||||
|
|
||||||
|
|
||||||
|
#### [0.2.6]
|
||||||
|
- redis connection pool
|
||||||
|
- auth context fixes
|
||||||
|
- communities orm, resolvers, schema
|
||||||
|
|
||||||
|
|
||||||
|
#### [0.2.5]
|
||||||
|
- restructured
|
||||||
|
- all users have their profiles as authors in core
|
||||||
|
- `gittask`, `inbox` and `auth` logics removed
|
||||||
|
- `settings` moved to base and now smaller
|
||||||
|
- new outside auth schema
|
||||||
|
- removed `gittask`, `auth`, `inbox`, `migration`
|
||||||
|
|
9
cache/cache.py
vendored
9
cache/cache.py
vendored
|
@ -384,11 +384,8 @@ async def invalidate_shouts_cache(cache_keys: List[str]):
|
||||||
"""
|
"""
|
||||||
Инвалидирует кэш выборок публикаций по переданным ключам.
|
Инвалидирует кэш выборок публикаций по переданным ключам.
|
||||||
"""
|
"""
|
||||||
for key in cache_keys:
|
for cache_key in cache_keys:
|
||||||
try:
|
try:
|
||||||
# Формируем полный ключ кэша
|
|
||||||
cache_key = f"shouts:{key}"
|
|
||||||
|
|
||||||
# Удаляем основной кэш
|
# Удаляем основной кэш
|
||||||
await redis.execute("DEL", cache_key)
|
await redis.execute("DEL", cache_key)
|
||||||
logger.debug(f"Invalidated cache key: {cache_key}")
|
logger.debug(f"Invalidated cache key: {cache_key}")
|
||||||
|
@ -397,8 +394,8 @@ async def invalidate_shouts_cache(cache_keys: List[str]):
|
||||||
await redis.execute("SETEX", f"{cache_key}:invalidated", CACHE_TTL, "1")
|
await redis.execute("SETEX", f"{cache_key}:invalidated", CACHE_TTL, "1")
|
||||||
|
|
||||||
# Если это кэш темы, инвалидируем также связанные ключи
|
# Если это кэш темы, инвалидируем также связанные ключи
|
||||||
if key.startswith("topic_"):
|
if cache_key.startswith("topic_"):
|
||||||
topic_id = key.split("_")[1]
|
topic_id = cache_key.split("_")[1]
|
||||||
related_keys = [
|
related_keys = [
|
||||||
f"topic:id:{topic_id}",
|
f"topic:id:{topic_id}",
|
||||||
f"topic:authors:{topic_id}",
|
f"topic:authors:{topic_id}",
|
||||||
|
|
121
docs/caching.md
121
docs/caching.md
|
@ -249,16 +249,129 @@ async def get_topics_with_stats(limit=10, offset=0, by="title"):
|
||||||
### Точечная инвалидация кеша при изменении данных
|
### Точечная инвалидация кеша при изменении данных
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def update_topic(topic_id, new_data):
|
async def update_author(author_id, data):
|
||||||
# Обновление данных в базе
|
# Обновление данных в базе
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
# Точечная инвалидация кеша только для измененной темы
|
# Инвалидация только кеша этого автора
|
||||||
await invalidate_topics_cache(topic_id)
|
await invalidate_authors_cache(author_id)
|
||||||
|
|
||||||
return updated_topic
|
return result
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Ключи кеширования
|
||||||
|
|
||||||
|
Ниже приведен полный список форматов ключей, используемых в системе кеширования Discours.
|
||||||
|
|
||||||
|
### Ключи для публикаций (Shout)
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `shouts:{id}` | Публикация по ID | `shouts:123` |
|
||||||
|
| `shouts:{id}:invalidated` | Флаг инвалидации публикации | `shouts:123:invalidated` |
|
||||||
|
| `shouts:feed:limit={n}:offset={m}` | Основная лента публикаций | `shouts:feed:limit=20:offset=0` |
|
||||||
|
| `shouts:recent:limit={n}` | Последние публикации | `shouts:recent:limit=10` |
|
||||||
|
| `shouts:random_top:limit={n}` | Случайные топовые публикации | `shouts:random_top:limit=5` |
|
||||||
|
| `shouts:unrated:limit={n}` | Неоцененные публикации | `shouts:unrated:limit=20` |
|
||||||
|
| `shouts:coauthored:limit={n}` | Совместные публикации | `shouts:coauthored:limit=10` |
|
||||||
|
|
||||||
|
### Ключи для авторов (Author)
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `author:id:{id}` | Автор по ID | `author:id:123` |
|
||||||
|
| `author:slug:{slug}` | Автор по слагу | `author:slug:john-doe` |
|
||||||
|
| `author:user_id:{user_id}` | Автор по ID пользователя | `author:user_id:abc123` |
|
||||||
|
| `author:{id}` | Публикации автора | `author:123` |
|
||||||
|
| `authored:{id}` | Публикации, созданные автором | `authored:123` |
|
||||||
|
| `authors:all:basic` | Базовый список всех авторов | `authors:all:basic` |
|
||||||
|
| `authors:stats:limit={n}:offset={m}:sort={field}` | Список авторов с пагинацией и сортировкой | `authors:stats:limit=20:offset=0:sort=name` |
|
||||||
|
| `author:followers:{id}` | Подписчики автора | `author:followers:123` |
|
||||||
|
| `author:following:{id}` | Авторы, на которых подписан автор | `author:following:123` |
|
||||||
|
|
||||||
|
### Ключи для тем (Topic)
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `topic:id:{id}` | Тема по ID | `topic:id:123` |
|
||||||
|
| `topic:slug:{slug}` | Тема по слагу | `topic:slug:technology` |
|
||||||
|
| `topic:{id}` | Публикации по теме | `topic:123` |
|
||||||
|
| `topic_shouts_{id}` | Публикации по теме (старый формат) | `topic_shouts_123` |
|
||||||
|
| `topics:all:basic` | Базовый список всех тем | `topics:all:basic` |
|
||||||
|
| `topics:stats:limit={n}:offset={m}:sort={field}` | Список тем с пагинацией и сортировкой | `topics:stats:limit=20:offset=0:sort=name` |
|
||||||
|
| `topic:authors:{id}` | Авторы темы | `topic:authors:123` |
|
||||||
|
| `topic:followers:{id}` | Подписчики темы | `topic:followers:123` |
|
||||||
|
| `topic:stats:{id}` | Статистика темы | `topic:stats:123` |
|
||||||
|
|
||||||
|
### Ключи для реакций (Reaction)
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `reactions:shout:{id}:limit={n}:offset={m}` | Реакции на публикацию | `reactions:shout:123:limit=20:offset=0` |
|
||||||
|
| `reactions:comment:{id}:limit={n}:offset={m}` | Реакции на комментарий | `reactions:comment:456:limit=20:offset=0` |
|
||||||
|
| `reactions:author:{id}:limit={n}:offset={m}` | Реакции автора | `reactions:author:123:limit=20:offset=0` |
|
||||||
|
| `reactions:followed:author:{id}:limit={n}` | Реакции авторов, на которых подписан пользователь | `reactions:followed:author:123:limit=20` |
|
||||||
|
|
||||||
|
### Ключи для сообществ (Community)
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `community:id:{id}` | Сообщество по ID | `community:id:123` |
|
||||||
|
| `community:slug:{slug}` | Сообщество по слагу | `community:slug:tech-club` |
|
||||||
|
| `communities:all:basic` | Базовый список всех сообществ | `communities:all:basic` |
|
||||||
|
| `community:authors:{id}` | Авторы сообщества | `community:authors:123` |
|
||||||
|
| `community:shouts:{id}:limit={n}:offset={m}` | Публикации сообщества | `community:shouts:123:limit=20:offset=0` |
|
||||||
|
|
||||||
|
### Ключи для подписок (Follow)
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `follow:author:{follower_id}:authors` | Авторы, на которых подписан пользователь | `follow:author:123:authors` |
|
||||||
|
| `follow:author:{follower_id}:topics` | Темы, на которые подписан пользователь | `follow:author:123:topics` |
|
||||||
|
| `follow:topic:{topic_id}:authors` | Авторы, подписанные на тему | `follow:topic:456:authors` |
|
||||||
|
| `follow:author:{author_id}:followers` | Подписчики автора | `follow:author:123:followers` |
|
||||||
|
|
||||||
|
### Ключи для черновиков (Draft)
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `draft:id:{id}` | Черновик по ID | `draft:id:123` |
|
||||||
|
| `drafts:author:{id}` | Черновики автора | `drafts:author:123` |
|
||||||
|
| `drafts:all:limit={n}:offset={m}` | Список всех черновиков с пагинацией | `drafts:all:limit=20:offset=0` |
|
||||||
|
|
||||||
|
### Ключи для статистики
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `stats:shout:{id}` | Статистика публикации | `stats:shout:123` |
|
||||||
|
| `stats:author:{id}` | Статистика автора | `stats:author:123` |
|
||||||
|
| `stats:topic:{id}` | Статистика темы | `stats:topic:123` |
|
||||||
|
| `stats:community:{id}` | Статистика сообщества | `stats:community:123` |
|
||||||
|
|
||||||
|
### Ключи для поиска
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `search:query:{query}:limit={n}:offset={m}` | Результаты поиска | `search:query:технологии:limit=20:offset=0` |
|
||||||
|
| `search:author:{query}:limit={n}` | Результаты поиска авторов | `search:author:иван:limit=10` |
|
||||||
|
| `search:topic:{query}:limit={n}` | Результаты поиска тем | `search:topic:наука:limit=10` |
|
||||||
|
|
||||||
|
### Служебные ключи
|
||||||
|
|
||||||
|
| Формат ключа | Описание | Пример |
|
||||||
|
|--------------|----------|--------|
|
||||||
|
| `revalidation:{entity_type}:{entity_id}` | Метка для ревалидации | `revalidation:author:123` |
|
||||||
|
| `revalidation:batch:{entity_type}` | Батчевая ревалидация | `revalidation:batch:shouts` |
|
||||||
|
| `lock:{resource}` | Блокировка ресурса | `lock:precache` |
|
||||||
|
| `views:shout:{id}` | Счетчик просмотров публикации | `views:shout:123` |
|
||||||
|
|
||||||
|
### Важные замечания по использованию ключей
|
||||||
|
|
||||||
|
1. При инвалидации кеша публикаций через `invalidate_shouts_cache()` необходимо передавать список ID публикаций, а не ключи кеша.
|
||||||
|
2. Функция `invalidate_shout_related_cache()` автоматически инвалидирует все связанные ключи для публикации, включая ключи авторов и тем.
|
||||||
|
3. Для большинства операций с кешем следует использовать асинхронные функции с префиксом `await`.
|
||||||
|
4. При создании новых ключей кеша следует придерживаться существующих конвенций именования.
|
||||||
|
|
||||||
## Отладка и мониторинг
|
## Отладка и мониторинг
|
||||||
|
|
||||||
Система кеширования использует логгер для отслеживания операций:
|
Система кеширования использует логгер для отслеживания операций:
|
||||||
|
|
|
@ -32,17 +32,51 @@ from auth.internal import verify_internal_auth
|
||||||
@mutation.field("getSession")
|
@mutation.field("getSession")
|
||||||
@login_required
|
@login_required
|
||||||
async def get_current_user(_, info):
|
async def get_current_user(_, info):
|
||||||
"""get current user"""
|
"""
|
||||||
auth: AuthCredentials = info.context["request"].auth
|
Получает информацию о текущем пользователе.
|
||||||
token = info.context["request"].headers.get(SESSION_TOKEN_HEADER)
|
|
||||||
|
Требует авторизации через декоратор login_required.
|
||||||
with local_session() as session:
|
|
||||||
author = session.query(Author).where(Author.id == auth.author_id).one()
|
Args:
|
||||||
author.last_seen = int(time.time())
|
_: Родительский объект (не используется)
|
||||||
session.commit()
|
info: Контекст GraphQL запроса
|
||||||
|
|
||||||
# Здесь можно не применять фильтрацию, так как пользователь получает свои данные
|
Returns:
|
||||||
return {"token": token, "author": author}
|
dict: Объект с токеном и данными автора
|
||||||
|
"""
|
||||||
|
# Получаем данные авторизации из контекста запроса
|
||||||
|
user_id = info.context.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
logger.error("[getSession] Пользователь не авторизован")
|
||||||
|
from graphql.error import GraphQLError
|
||||||
|
raise GraphQLError("Требуется авторизация")
|
||||||
|
|
||||||
|
# Получаем токен из заголовка
|
||||||
|
req = info.context.get("request")
|
||||||
|
token = req.headers.get(SESSION_TOKEN_HEADER)
|
||||||
|
if token and token.startswith("Bearer "):
|
||||||
|
token = token.split("Bearer ")[-1].strip()
|
||||||
|
|
||||||
|
# Получаем данные автора
|
||||||
|
author = info.context.get("author")
|
||||||
|
|
||||||
|
# Если автор не найден в контексте, пробуем получить из БД
|
||||||
|
if not author:
|
||||||
|
logger.debug(f"[getSession] Автор не найден в контексте для пользователя {user_id}, получаем из БД")
|
||||||
|
with local_session() as session:
|
||||||
|
try:
|
||||||
|
db_author = session.query(Author).filter(Author.id == user_id).one()
|
||||||
|
db_author.last_seen = int(time.time())
|
||||||
|
session.commit()
|
||||||
|
author = db_author
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[getSession] Ошибка при получении автора из БД: {e}")
|
||||||
|
from graphql.error import GraphQLError
|
||||||
|
raise GraphQLError("Ошибка при получении данных пользователя")
|
||||||
|
|
||||||
|
# Возвращаем данные сессии
|
||||||
|
logger.info(f"[getSession] Успешно получена сессия для пользователя {user_id}")
|
||||||
|
return {"token": token or '', "author": author}
|
||||||
|
|
||||||
|
|
||||||
@mutation.field("confirmEmail")
|
@mutation.field("confirmEmail")
|
||||||
|
@ -63,7 +97,7 @@ async def confirm_email(_, info, token):
|
||||||
user = session.query(Author).where(Author.id == user_id).first()
|
user = session.query(Author).where(Author.id == user_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
logger.warning(f"[auth] confirmEmail: Пользователь с ID {user_id} не найден.")
|
logger.warning(f"[auth] confirmEmail: Пользователь с ID {user_id} не найден.")
|
||||||
return {"success": False, "error": "Пользователь не найден"}
|
return {"success": False, "token": None, "author": None, "error": "Пользователь не найден"}
|
||||||
|
|
||||||
# Создаем сессионный токен с новым форматом вызова и явным временем истечения
|
# Создаем сессионный токен с новым форматом вызова и явным временем истечения
|
||||||
device_info = {"email": user.email} if hasattr(user, "email") else None
|
device_info = {"email": user.email} if hasattr(user, "email") else None
|
||||||
|
|
|
@ -403,7 +403,11 @@ async def get_author_follows(_, info, slug="", user=None, author_id=0):
|
||||||
if hasattr(temp_author, key):
|
if hasattr(temp_author, key):
|
||||||
setattr(temp_author, key, value)
|
setattr(temp_author, key, value)
|
||||||
# Добавляем отфильтрованную версию
|
# Добавляем отфильтрованную версию
|
||||||
followed_authors.append(temp_author.dict(current_user_id, is_admin))
|
# temp_author - это объект Author, который мы хотим сериализовать
|
||||||
|
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||||
|
# is_admin - булево значение, является ли текущий пользователь админом
|
||||||
|
has_access = is_admin or (current_user_id is not None and str(current_user_id) == str(temp_author.id))
|
||||||
|
followed_authors.append(temp_author.dict(access=has_access))
|
||||||
|
|
||||||
# TODO: Get followed communities too
|
# TODO: Get followed communities too
|
||||||
return {
|
return {
|
||||||
|
@ -447,7 +451,11 @@ async def get_author_follows_authors(_, info, slug="", user=None, author_id=None
|
||||||
if hasattr(temp_author, key):
|
if hasattr(temp_author, key):
|
||||||
setattr(temp_author, key, value)
|
setattr(temp_author, key, value)
|
||||||
# Добавляем отфильтрованную версию
|
# Добавляем отфильтрованную версию
|
||||||
followed_authors.append(temp_author.dict(current_user_id, is_admin))
|
# temp_author - это объект Author, который мы хотим сериализовать
|
||||||
|
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||||
|
# is_admin - булево значение, является ли текущий пользователь админом
|
||||||
|
has_access = is_admin or (current_user_id is not None and str(current_user_id) == str(temp_author.id))
|
||||||
|
followed_authors.append(temp_author.dict(access=has_access))
|
||||||
|
|
||||||
return followed_authors
|
return followed_authors
|
||||||
|
|
||||||
|
@ -488,6 +496,10 @@ async def get_author_followers(_, info, slug: str = "", user: str = "", author_i
|
||||||
if hasattr(temp_author, key):
|
if hasattr(temp_author, key):
|
||||||
setattr(temp_author, key, value)
|
setattr(temp_author, key, value)
|
||||||
# Добавляем отфильтрованную версию
|
# Добавляем отфильтрованную версию
|
||||||
followers.append(temp_author.dict(current_user_id, is_admin))
|
# temp_author - это объект Author, который мы хотим сериализовать
|
||||||
|
# current_user_id - ID текущего авторизованного пользователя (может быть None)
|
||||||
|
# is_admin - булево значение, является ли текущий пользователь админом
|
||||||
|
has_access = is_admin or (current_user_id is not None and str(current_user_id) == str(temp_author.id))
|
||||||
|
followers.append(temp_author.dict(access=has_access))
|
||||||
|
|
||||||
return followers
|
return followers
|
||||||
|
|
|
@ -461,8 +461,9 @@ async def publish_draft(_, info, draft_id: int):
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
# Инвалидируем кеш
|
# Инвалидируем кеш
|
||||||
invalidate_shouts_cache()
|
cache_keys = [f"shouts:{shout.id}", ]
|
||||||
invalidate_shout_related_cache(shout.id)
|
await invalidate_shouts_cache(cache_keys)
|
||||||
|
await invalidate_shout_related_cache(shout, author_id)
|
||||||
|
|
||||||
# Уведомляем о публикации
|
# Уведомляем о публикации
|
||||||
await notify_shout(shout.id)
|
await notify_shout(shout.id)
|
||||||
|
|
121
services/auth.py
121
services/auth.py
|
@ -1,6 +1,8 @@
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
from cache.cache import get_cached_author_by_user_id
|
from cache.cache import get_cached_author_by_user_id
|
||||||
from resolvers.stat import get_with_stat
|
from resolvers.stat import get_with_stat
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
|
@ -8,12 +10,13 @@ from auth.internal import verify_internal_auth
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
from services.db import local_session
|
from services.db import local_session
|
||||||
from auth.orm import Author, Role
|
from auth.orm import Author, Role
|
||||||
|
from settings import SESSION_TOKEN_HEADER
|
||||||
|
|
||||||
# Список разрешенных заголовков
|
# Список разрешенных заголовков
|
||||||
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
|
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
|
||||||
|
|
||||||
|
|
||||||
async def check_auth(req) -> Tuple[str, list[str], bool]:
|
async def check_auth(req: Request) -> Tuple[str, list[str], bool]:
|
||||||
"""
|
"""
|
||||||
Проверка авторизации пользователя.
|
Проверка авторизации пользователя.
|
||||||
|
|
||||||
|
@ -27,50 +30,54 @@ async def check_auth(req) -> Tuple[str, list[str], bool]:
|
||||||
- user_roles: list[str] - Список ролей пользователя
|
- user_roles: list[str] - Список ролей пользователя
|
||||||
- is_admin: bool - Флаг наличия у пользователя административных прав
|
- is_admin: bool - Флаг наличия у пользователя административных прав
|
||||||
"""
|
"""
|
||||||
# Проверяем наличие токена
|
logger.debug(f"[check_auth] Проверка авторизации...")
|
||||||
token = req.headers.get("Authorization")
|
|
||||||
|
# Получаем заголовок авторизации
|
||||||
|
token = None
|
||||||
|
|
||||||
|
# Проверяем заголовок с учетом регистра
|
||||||
|
headers_dict = dict(req.headers.items())
|
||||||
|
logger.debug(f"[check_auth] Все заголовки: {headers_dict}")
|
||||||
|
|
||||||
|
# Ищем заголовок Authorization независимо от регистра
|
||||||
|
for header_name, header_value in headers_dict.items():
|
||||||
|
if header_name.lower() == SESSION_TOKEN_HEADER.lower():
|
||||||
|
token = header_value
|
||||||
|
logger.debug(f"[check_auth] Найден заголовок {header_name}: {token[:10]}...")
|
||||||
|
break
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
|
logger.debug(f"[check_auth] Токен не найден в заголовках")
|
||||||
return "", [], False
|
return "", [], False
|
||||||
|
|
||||||
# Очищаем токен от префикса Bearer если он есть
|
# Очищаем токен от префикса Bearer если он есть
|
||||||
if token.startswith("Bearer "):
|
if token.startswith("Bearer "):
|
||||||
token = token.split("Bearer ")[-1].strip()
|
token = token.split("Bearer ")[-1].strip()
|
||||||
|
|
||||||
logger.debug(f"Checking auth token: {token[:10]}...")
|
|
||||||
|
|
||||||
# Проверяем авторизацию внутренним механизмом
|
# Проверяем авторизацию внутренним механизмом
|
||||||
logger.debug("Using internal authentication")
|
logger.debug("[check_auth] Вызов verify_internal_auth...")
|
||||||
user_id, user_roles = await verify_internal_auth(token)
|
user_id, user_roles, is_admin = await verify_internal_auth(token)
|
||||||
|
logger.debug(f"[check_auth] Результат verify_internal_auth: user_id={user_id}, roles={user_roles}, is_admin={is_admin}")
|
||||||
|
|
||||||
# Проверяем наличие административных прав у пользователя
|
# Если в ролях нет админа, но есть ID - проверяем в БД
|
||||||
is_admin = False
|
if user_id and not is_admin:
|
||||||
if user_id:
|
try:
|
||||||
# Быстрая проверка на админ роли в кэше
|
with local_session() as session:
|
||||||
admin_roles = ['admin', 'super']
|
# Преобразуем user_id в число
|
||||||
for role in user_roles:
|
try:
|
||||||
if role in admin_roles:
|
user_id_int = int(user_id.strip())
|
||||||
is_admin = True
|
except (ValueError, TypeError):
|
||||||
break
|
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
|
||||||
|
else:
|
||||||
# Если в ролях нет админа, но есть ID - проверяем в БД
|
# Проверяем наличие админских прав через БД
|
||||||
if not is_admin:
|
from auth.orm import AuthorRole
|
||||||
try:
|
admin_role = session.query(AuthorRole).filter(
|
||||||
with local_session() as session:
|
AuthorRole.author == user_id_int,
|
||||||
# Преобразуем user_id в число
|
AuthorRole.role.in_(["admin", "super"])
|
||||||
try:
|
).first()
|
||||||
user_id_int = int(user_id.strip())
|
is_admin = admin_role is not None
|
||||||
except (ValueError, TypeError):
|
except Exception as e:
|
||||||
logger.error(f"Невозможно преобразовать user_id {user_id} в число")
|
logger.error(f"Ошибка при проверке прав администратора: {e}")
|
||||||
else:
|
|
||||||
# Проверяем наличие админских прав через БД
|
|
||||||
from auth.orm import AuthorRole
|
|
||||||
admin_role = session.query(AuthorRole).filter(
|
|
||||||
AuthorRole.author == user_id_int,
|
|
||||||
AuthorRole.role.in_(["admin", "super"])
|
|
||||||
).first()
|
|
||||||
is_admin = admin_role is not None
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при проверке прав администратора: {e}")
|
|
||||||
|
|
||||||
return user_id, user_roles, is_admin
|
return user_id, user_roles, is_admin
|
||||||
|
|
||||||
|
@ -124,13 +131,18 @@ def login_required(f):
|
||||||
|
|
||||||
info = args[1]
|
info = args[1]
|
||||||
req = info.context.get("request")
|
req = info.context.get("request")
|
||||||
|
|
||||||
|
logger.debug(f"[login_required] Проверка авторизации для запроса: {req.method} {req.url.path}")
|
||||||
|
logger.debug(f"[login_required] Заголовки: {req.headers}")
|
||||||
|
|
||||||
user_id, user_roles, is_admin = await check_auth(req)
|
user_id, user_roles, is_admin = await check_auth(req)
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
|
logger.debug(f"[login_required] Пользователь не авторизован, {dict(req)}, {info}")
|
||||||
raise GraphQLError("Требуется авторизация")
|
raise GraphQLError("Требуется авторизация")
|
||||||
|
|
||||||
# Проверяем наличие роли reader
|
# Проверяем наличие роли reader
|
||||||
if 'reader' not in user_roles and not is_admin:
|
if 'reader' not in user_roles:
|
||||||
logger.error(f"Пользователь {user_id} не имеет роли 'reader'")
|
logger.error(f"Пользователь {user_id} не имеет роли 'reader'")
|
||||||
raise GraphQLError("У вас нет необходимых прав для доступа")
|
raise GraphQLError("У вас нет необходимых прав для доступа")
|
||||||
|
|
||||||
|
@ -192,38 +204,3 @@ def login_accepted(f):
|
||||||
return await f(*args, **kwargs)
|
return await f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
def author_required(f):
|
|
||||||
"""Декоратор для проверки наличия роли 'author' у пользователя."""
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
async def decorated_function(*args, **kwargs):
|
|
||||||
from graphql.error import GraphQLError
|
|
||||||
|
|
||||||
info = args[1]
|
|
||||||
req = info.context.get("request")
|
|
||||||
user_id, user_roles, is_admin = await check_auth(req)
|
|
||||||
|
|
||||||
if not user_id:
|
|
||||||
raise GraphQLError("Требуется авторизация")
|
|
||||||
|
|
||||||
# Проверяем наличие роли author
|
|
||||||
if 'author' not in user_roles and not is_admin:
|
|
||||||
logger.error(f"Пользователь {user_id} не имеет роли 'author'")
|
|
||||||
raise GraphQLError("Для выполнения этого действия необходимы права автора")
|
|
||||||
|
|
||||||
logger.info(f"Авторизован автор {user_id} с ролями: {user_roles}")
|
|
||||||
info.context["user_id"] = user_id.strip()
|
|
||||||
info.context["roles"] = user_roles
|
|
||||||
|
|
||||||
# Проверяем права администратора
|
|
||||||
info.context["is_admin"] = is_admin
|
|
||||||
|
|
||||||
author = await get_cached_author_by_user_id(user_id, get_with_stat)
|
|
||||||
if not author:
|
|
||||||
logger.error(f"Профиль автора не найден для пользователя {user_id}")
|
|
||||||
info.context["author"] = author
|
|
||||||
|
|
||||||
return await f(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorated_function
|
|
||||||
|
|
|
@ -2,10 +2,12 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from opensearchpy import OpenSearch
|
from opensearchpy import OpenSearch
|
||||||
|
|
||||||
|
from orm.shout import Shout
|
||||||
from services.redis import redis
|
from services.redis import redis
|
||||||
from utils.encoders import CustomJSONEncoder
|
from utils.encoders import CustomJSONEncoder
|
||||||
|
|
||||||
|
@ -156,7 +158,18 @@ class SearchService:
|
||||||
else:
|
else:
|
||||||
logger.error("клиент не инициализован, невозможно проверить индекс")
|
logger.error("клиент не инициализован, невозможно проверить индекс")
|
||||||
|
|
||||||
def index(self, shout):
|
def index_shouts(self, shouts: List[Shout]):
|
||||||
|
if not SEARCH_ENABLED:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.client:
|
||||||
|
for shout in shouts:
|
||||||
|
self.index(shout)
|
||||||
|
|
||||||
|
def index(self, shout: Shout):
|
||||||
|
return self.index_shout(shout)
|
||||||
|
|
||||||
|
def index_shout(self, shout: Shout):
|
||||||
if not SEARCH_ENABLED:
|
if not SEARCH_ENABLED:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user