changelog-restored+internal-auth-fix
All checks were successful
Deploy on push / deploy (push) Successful in 6s
All checks were successful
Deploy on push / deploy (push) Successful in 6s
This commit is contained in:
parent
ab65fd4fd8
commit
b01de1fdc1
190
CHANGELOG.md
190
CHANGELOG.md
|
@ -799,17 +799,10 @@
|
||||||
- error logging added via `logger.error()`
|
- error logging added via `logger.error()`
|
||||||
|
|
||||||
#### [0.4.6]
|
#### [0.4.6]
|
||||||
- login_accepted decorator added
|
|
||||||
- `docs` added
|
- `docs` added
|
||||||
- optimized and unified `load_shouts_*` resolvers with `LoadShoutsOptions`
|
- optimized and unified `load_shouts_*` resolvers with `LoadShoutsOptions`
|
||||||
- `load_shouts_bookmarked` resolver fixed
|
- `load_shouts_bookmarked` resolver fixed
|
||||||
- resolvers updates:
|
- refactored with `resolvers/feed`
|
||||||
- new resolvers group `feed`
|
|
||||||
- `load_shouts_authored_by` resolver added
|
|
||||||
- `load_shouts_with_topic` resolver added
|
|
||||||
- `load_shouts_followed` removed
|
|
||||||
- `load_shouts_random_topic` removed
|
|
||||||
- `get_topics_random` removed
|
|
||||||
- model updates:
|
- model updates:
|
||||||
- `ShoutsOrderBy` enum added
|
- `ShoutsOrderBy` enum added
|
||||||
- `Shout.main_topic` from `ShoutTopic.main` as `Topic` type output
|
- `Shout.main_topic` from `ShoutTopic.main` as `Topic` type output
|
||||||
|
@ -827,4 +820,183 @@
|
||||||
- `CommunityFollowerRole` enum added
|
- `CommunityFollowerRole` enum added
|
||||||
- `InviteStatus` enum added
|
- `InviteStatus` enum added
|
||||||
- `Topic.parents` ids added
|
- `Topic.parents` ids added
|
||||||
- `
|
- `get_shout` resolver accepts slug or shout_id
|
||||||
|
|
||||||
|
#### [0.4.4]
|
||||||
|
- `followers_stat` removed for shout
|
||||||
|
- sqlite3 support added
|
||||||
|
- `rating_stat` and `commented_stat` fixes
|
||||||
|
|
||||||
|
#### [0.4.3]
|
||||||
|
- cache reimplemented
|
||||||
|
- load shouts queries unified
|
||||||
|
- `followers_stat` removed from shout
|
||||||
|
|
||||||
|
#### [0.4.2]
|
||||||
|
- reactions load resolvers separated for ratings (no stats) and comments
|
||||||
|
- reactions stats improved
|
||||||
|
- `load_comment_ratings` separate resolver
|
||||||
|
|
||||||
|
#### [0.4.1]
|
||||||
|
- follow/unfollow logic updated and unified with cache
|
||||||
|
|
||||||
|
#### [0.4.0]
|
||||||
|
- chore: version migrator synced
|
||||||
|
- feat: precache_data on start
|
||||||
|
- fix: store id list for following cache data
|
||||||
|
- fix: shouts stat filter out deleted
|
||||||
|
|
||||||
|
#### [0.3.5]
|
||||||
|
- cache isolated to services
|
||||||
|
- topics followers and authors cached
|
||||||
|
- redis stores lists of ids
|
||||||
|
|
||||||
|
#### [0.3.4]
|
||||||
|
- `load_authors_by` from cache
|
||||||
|
|
||||||
|
#### [0.3.3]
|
||||||
|
- feat: sentry integration enabled with glitchtip
|
||||||
|
- fix: reindex on update shout
|
||||||
|
- packages upgrade, isort
|
||||||
|
- separated stats queries for author and topic
|
||||||
|
- fix: feed featured filter
|
||||||
|
- fts search removed
|
||||||
|
|
||||||
|
#### [0.3.2]
|
||||||
|
- redis cache for what author follows
|
||||||
|
- redis cache for followers
|
||||||
|
- graphql add query: get topic followers
|
||||||
|
|
||||||
|
#### [0.3.1]
|
||||||
|
- enabling sentry
|
||||||
|
- long query log report added
|
||||||
|
- editor fixes
|
||||||
|
- authors links cannot be updated by `update_shout` anymore
|
||||||
|
|
||||||
|
#### [0.3.0]
|
||||||
|
- `Shout.featured_at` timestamp of the frontpage featuring event
|
||||||
|
- added proposal accepting logics
|
||||||
|
- schema modulized
|
||||||
|
- Shout.visibility removed
|
||||||
|
|
||||||
|
#### [0.2.22]
|
||||||
|
- added precommit hook
|
||||||
|
- fmt
|
||||||
|
- granian asgi
|
||||||
|
|
||||||
|
#### [0.2.21]
|
||||||
|
- fix: rating logix
|
||||||
|
- fix: `load_top_random_shouts`
|
||||||
|
- resolvers: `add_stat_*` refactored
|
||||||
|
- services: use google analytics
|
||||||
|
- services: minor fixes search
|
||||||
|
|
||||||
|
#### [0.2.20]
|
||||||
|
- services: ackee removed
|
||||||
|
- services: following manager fixed
|
||||||
|
- services: import views.json
|
||||||
|
|
||||||
|
#### [0.2.19]
|
||||||
|
- fix: adding `author` role
|
||||||
|
- fix: stripping `user_id` in auth connector
|
||||||
|
|
||||||
|
#### [0.2.18]
|
||||||
|
- schema: added `Shout.seo` string field
|
||||||
|
- resolvers: added `/new-author` webhook resolver
|
||||||
|
- resolvers: added reader.load_shouts_top_random
|
||||||
|
- resolvers: added reader.load_shouts_unrated
|
||||||
|
- resolvers: community follower id property name is `.author`
|
||||||
|
- resolvers: `get_authors_all` and `load_authors_by`
|
||||||
|
- services: auth connector upgraded
|
||||||
|
|
||||||
|
#### [0.2.17]
|
||||||
|
- schema: enum types workaround, `ReactionKind`, `InviteStatus`, `ShoutVisibility`
|
||||||
|
- schema: `Shout.created_by`, `Shout.updated_by`
|
||||||
|
- schema: `Shout.authors` can be empty
|
||||||
|
- resolvers: optimized `reacted_shouts_updates` query
|
||||||
|
|
||||||
|
#### [0.2.16]
|
||||||
|
- resolvers: collab inviting logics
|
||||||
|
- resolvers: queries and mutations revision and renaming
|
||||||
|
- resolvers: `delete_topic(slug)` implemented
|
||||||
|
- resolvers: added `get_shout_followers`
|
||||||
|
- resolvers: `load_shouts_by` filters implemented
|
||||||
|
- orm: invite entity
|
||||||
|
- schema: `Reaction.range` -> `Reaction.quote`
|
||||||
|
- filters: `time_ago` -> `after`
|
||||||
|
- httpx -> aiohttp
|
||||||
|
|
||||||
|
#### [0.2.15]
|
||||||
|
- schema: `Shout.created_by` removed
|
||||||
|
- schema: `Shout.mainTopic` removed
|
||||||
|
- services: cached elasticsearch connector
|
||||||
|
- services: auth is using `user_id` from authorizer
|
||||||
|
- resolvers: `notify_*` usage fixes
|
||||||
|
- resolvers: `getAuthor` now accepts slug, `user_id` or `author_id`
|
||||||
|
- resolvers: login_required usage fixes
|
||||||
|
|
||||||
|
#### [0.2.14]
|
||||||
|
- schema: some fixes from migrator
|
||||||
|
- schema: `.days` -> `.time_ago`
|
||||||
|
- schema: `excludeLayout` + `layout` in filters -> `layouts`
|
||||||
|
- services: db access simpler, no contextmanager
|
||||||
|
- services: removed Base.create() method
|
||||||
|
- services: rediscache updated
|
||||||
|
- resolvers: get_reacted_shouts_updates as followedReactions query
|
||||||
|
|
||||||
|
#### [0.2.13]
|
||||||
|
- services: db context manager
|
||||||
|
- services: `ViewedStorage` fixes
|
||||||
|
- services: views are not stored in core db anymore
|
||||||
|
- schema: snake case in model fields names
|
||||||
|
- schema: no DateTime scalar
|
||||||
|
- resolvers: `get_my_feed` comments filter reactions body.is_not('')
|
||||||
|
- resolvers: `get_my_feed` query fix
|
||||||
|
- resolvers: `LoadReactionsBy.days` -> `LoadReactionsBy.time_ago`
|
||||||
|
- resolvers: `LoadShoutsBy.days` -> `LoadShoutsBy.time_ago`
|
||||||
|
|
||||||
|
#### [0.2.12]
|
||||||
|
- `Author.userpic` -> `Author.pic`
|
||||||
|
- `CommunityFollower.role` is string now
|
||||||
|
- `Author.user` is string now
|
||||||
|
|
||||||
|
#### [0.2.11]
|
||||||
|
- redis interface updated
|
||||||
|
- `viewed` interface updated
|
||||||
|
- `presence` interface updated
|
||||||
|
- notify on create, update, delete for reaction and shout
|
||||||
|
- notify on follow / unfollow author
|
||||||
|
- use pyproject
|
||||||
|
- devmode fixed
|
||||||
|
|
||||||
|
#### [0.2.10]
|
||||||
|
- community resolvers connected
|
||||||
|
|
||||||
|
#### [0.2.9]
|
||||||
|
- starlette is back, aiohttp removed
|
||||||
|
- aioredis replaced with aredis
|
||||||
|
|
||||||
|
#### [0.2.8]
|
||||||
|
- refactored
|
||||||
|
|
||||||
|
|
||||||
|
#### [0.2.7]
|
||||||
|
- `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`
|
||||||
|
|
|
@ -141,29 +141,39 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
||||||
Raises:
|
Raises:
|
||||||
GraphQLError: если контекст невалиден или пользователь не авторизован
|
GraphQLError: если контекст невалиден или пользователь не авторизован
|
||||||
"""
|
"""
|
||||||
|
# Подробное логирование для диагностики
|
||||||
|
logger.debug("[validate_graphql_context] Начало проверки контекста и авторизации")
|
||||||
|
|
||||||
# Проверка базовой структуры контекста
|
# Проверка базовой структуры контекста
|
||||||
if info is None or not hasattr(info, "context"):
|
if info is None or not hasattr(info, "context"):
|
||||||
logger.error("[decorators] Missing GraphQL context information")
|
logger.error("[validate_graphql_context] Missing GraphQL context information")
|
||||||
msg = "Internal server error: missing context"
|
msg = "Internal server error: missing context"
|
||||||
raise GraphQLError(msg)
|
raise GraphQLError(msg)
|
||||||
|
|
||||||
request = info.context.get("request")
|
request = info.context.get("request")
|
||||||
if not request:
|
if not request:
|
||||||
logger.error("[decorators] Missing request in context")
|
logger.error("[validate_graphql_context] Missing request in context")
|
||||||
msg = "Internal server error: missing request"
|
msg = "Internal server error: missing request"
|
||||||
raise GraphQLError(msg)
|
raise GraphQLError(msg)
|
||||||
|
|
||||||
|
# Логируем детали запроса
|
||||||
|
client_info = {
|
||||||
|
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
|
||||||
|
"headers_keys": list(get_safe_headers(request).keys()),
|
||||||
|
}
|
||||||
|
logger.debug(f"[validate_graphql_context] Детали запроса: {client_info}")
|
||||||
|
|
||||||
# Проверяем auth из контекста - если уже авторизован, просто возвращаем
|
# Проверяем auth из контекста - если уже авторизован, просто возвращаем
|
||||||
auth = getattr(request, "auth", None)
|
auth = getattr(request, "auth", None)
|
||||||
if auth and auth.logged_in:
|
if auth and auth.logged_in:
|
||||||
logger.debug(f"[decorators] Пользователь уже авторизован: {auth.author_id}")
|
logger.debug(f"[validate_graphql_context] Пользователь уже авторизован через request.auth: {auth.author_id}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Если аутентификации нет в request.auth, пробуем получить ее из scope
|
# Если аутентификации нет в request.auth, пробуем получить ее из scope
|
||||||
if hasattr(request, "scope") and "auth" in request.scope:
|
if hasattr(request, "scope") and "auth" in request.scope:
|
||||||
auth_cred = request.scope.get("auth")
|
auth_cred = request.scope.get("auth")
|
||||||
if isinstance(auth_cred, AuthCredentials) and auth_cred.logged_in:
|
if isinstance(auth_cred, AuthCredentials) and auth_cred.logged_in:
|
||||||
logger.debug(f"[decorators] Пользователь авторизован через scope: {auth_cred.author_id}")
|
logger.debug(f"[validate_graphql_context] Пользователь авторизован через scope: {auth_cred.author_id}")
|
||||||
# Больше не устанавливаем request.auth напрямую
|
# Больше не устанавливаем request.auth напрямую
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -175,16 +185,22 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
||||||
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
|
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
|
||||||
"headers": get_safe_headers(request),
|
"headers": get_safe_headers(request),
|
||||||
}
|
}
|
||||||
logger.warning(f"[decorators] Токен авторизации не найден: {client_info}")
|
logger.warning(f"[validate_graphql_context] Токен авторизации не найден: {client_info}")
|
||||||
msg = "Unauthorized - please login"
|
msg = "Unauthorized - please login"
|
||||||
raise GraphQLError(msg)
|
raise GraphQLError(msg)
|
||||||
|
|
||||||
|
# Логируем информацию о найденном токене
|
||||||
|
logger.debug(f"[validate_graphql_context] Токен найден, длина: {len(token)}")
|
||||||
|
|
||||||
# Используем единый механизм проверки токена из auth.internal
|
# Используем единый механизм проверки токена из auth.internal
|
||||||
auth_state = await authenticate(request)
|
auth_state = await authenticate(request)
|
||||||
|
logger.debug(
|
||||||
|
f"[validate_graphql_context] Результат аутентификации: logged_in={auth_state.logged_in}, author_id={auth_state.author_id}, error={auth_state.error}"
|
||||||
|
)
|
||||||
|
|
||||||
if not auth_state.logged_in:
|
if not auth_state.logged_in:
|
||||||
error_msg = auth_state.error or "Invalid or expired token"
|
error_msg = auth_state.error or "Invalid or expired token"
|
||||||
logger.warning(f"[decorators] Недействительный токен: {error_msg}")
|
logger.warning(f"[validate_graphql_context] Недействительный токен: {error_msg}")
|
||||||
msg = f"Unauthorized - {error_msg}"
|
msg = f"Unauthorized - {error_msg}"
|
||||||
raise GraphQLError(msg)
|
raise GraphQLError(msg)
|
||||||
|
|
||||||
|
@ -192,6 +208,8 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
try:
|
try:
|
||||||
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}")
|
||||||
|
|
||||||
# Получаем разрешения из ролей
|
# Получаем разрешения из ролей
|
||||||
scopes = author.get_permissions()
|
scopes = author.get_permissions()
|
||||||
|
|
||||||
|
@ -209,12 +227,12 @@ async def validate_graphql_context(info: GraphQLResolveInfo) -> None:
|
||||||
if hasattr(request, "scope") and isinstance(request.scope, dict):
|
if hasattr(request, "scope") and isinstance(request.scope, dict):
|
||||||
request.scope["auth"] = auth_cred
|
request.scope["auth"] = auth_cred
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"[decorators] Токен успешно проверен и установлен для пользователя {auth_state.author_id}"
|
f"[validate_graphql_context] Токен успешно проверен и установлен для пользователя {auth_state.author_id}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.error("[decorators] Не удалось установить auth: отсутствует request.scope")
|
logger.error("[validate_graphql_context] Не удалось установить auth: отсутствует request.scope")
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
logger.error(f"[decorators] Пользователь с ID {auth_state.author_id} не найден в базе данных")
|
logger.error(f"[validate_graphql_context] Пользователь с ID {auth_state.author_id} не найден в базе данных")
|
||||||
msg = "Unauthorized - user not found"
|
msg = "Unauthorized - user not found"
|
||||||
raise GraphQLError(msg) from None
|
raise GraphQLError(msg) from None
|
||||||
|
|
||||||
|
@ -244,18 +262,43 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
||||||
@wraps(resolver)
|
@wraps(resolver)
|
||||||
async def wrapper(root: Any = None, info: Optional[GraphQLResolveInfo] = None, **kwargs: dict[str, Any]) -> Any:
|
async def wrapper(root: Any = None, info: Optional[GraphQLResolveInfo] = None, **kwargs: dict[str, Any]) -> Any:
|
||||||
try:
|
try:
|
||||||
|
# Подробное логирование для диагностики
|
||||||
|
logger.debug(f"[admin_auth_required] Начало проверки авторизации для {resolver.__name__}")
|
||||||
|
|
||||||
# Проверяем авторизацию пользователя
|
# Проверяем авторизацию пользователя
|
||||||
if info is None:
|
if info is None:
|
||||||
logger.error("[admin_auth_required] GraphQL info is None")
|
logger.error("[admin_auth_required] GraphQL info is None")
|
||||||
msg = "Invalid GraphQL context"
|
msg = "Invalid GraphQL context"
|
||||||
raise GraphQLError(msg)
|
raise GraphQLError(msg)
|
||||||
|
|
||||||
|
# Логируем детали запроса
|
||||||
|
request = info.context.get("request")
|
||||||
|
client_info = {
|
||||||
|
"ip": getattr(request.client, "host", "unknown") if hasattr(request, "client") else "unknown",
|
||||||
|
"headers": {k: v for k, v in get_safe_headers(request).items() if k not in ["authorization", "cookie"]},
|
||||||
|
}
|
||||||
|
logger.debug(f"[admin_auth_required] Детали запроса: {client_info}")
|
||||||
|
|
||||||
|
# Проверяем наличие токена до validate_graphql_context
|
||||||
|
token = get_auth_token(request)
|
||||||
|
logger.debug(f"[admin_auth_required] Токен найден: {bool(token)}, длина: {len(token) if token else 0}")
|
||||||
|
|
||||||
|
# Проверяем авторизацию
|
||||||
await validate_graphql_context(info)
|
await validate_graphql_context(info)
|
||||||
|
logger.debug("[admin_auth_required] validate_graphql_context успешно пройден")
|
||||||
|
|
||||||
if info:
|
if info:
|
||||||
# Получаем объект авторизации
|
# Получаем объект авторизации
|
||||||
auth = None
|
auth = None
|
||||||
if hasattr(info.context["request"], "scope") and "auth" in info.context["request"].scope:
|
if hasattr(info.context["request"], "scope") and "auth" in info.context["request"].scope:
|
||||||
auth = info.context["request"].scope.get("auth")
|
auth = info.context["request"].scope.get("auth")
|
||||||
|
logger.debug(f"[admin_auth_required] Auth из scope: {auth.author_id if auth else None}")
|
||||||
|
elif hasattr(info.context["request"], "auth"):
|
||||||
|
auth = info.context["request"].auth
|
||||||
|
logger.debug(f"[admin_auth_required] Auth из request: {auth.author_id if auth else None}")
|
||||||
|
else:
|
||||||
|
logger.error("[admin_auth_required] Auth не найден ни в scope, ни в request")
|
||||||
|
|
||||||
if not auth or not getattr(auth, "logged_in", False):
|
if not auth or not getattr(auth, "logged_in", False):
|
||||||
logger.error("[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
|
logger.error("[admin_auth_required] Пользователь не авторизован после validate_graphql_context")
|
||||||
msg = "Unauthorized - please login"
|
msg = "Unauthorized - please login"
|
||||||
|
@ -272,6 +315,7 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
||||||
raise GraphQLError(msg)
|
raise GraphQLError(msg)
|
||||||
|
|
||||||
author = session.query(Author).filter(Author.id == author_id).one()
|
author = session.query(Author).filter(Author.id == author_id).one()
|
||||||
|
logger.debug(f"[admin_auth_required] Найден автор: {author.id}, {author.email}")
|
||||||
|
|
||||||
# Проверяем, является ли пользователь администратором
|
# Проверяем, является ли пользователь администратором
|
||||||
if author.email in ADMIN_EMAILS:
|
if author.email in ADMIN_EMAILS:
|
||||||
|
@ -281,6 +325,7 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
||||||
# Проверяем роли пользователя
|
# Проверяем роли пользователя
|
||||||
admin_roles = ["admin", "super"]
|
admin_roles = ["admin", "super"]
|
||||||
user_roles = [role.id for role in author.roles] if author.roles else []
|
user_roles = [role.id for role in author.roles] if author.roles else []
|
||||||
|
logger.debug(f"[admin_auth_required] Роли пользователя: {user_roles}")
|
||||||
|
|
||||||
if any(role in admin_roles for role in user_roles):
|
if any(role in admin_roles for role in user_roles):
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -303,6 +348,7 @@ def admin_auth_required(resolver: Callable) -> Callable:
|
||||||
if not isinstance(e, GraphQLError):
|
if not isinstance(e, GraphQLError):
|
||||||
error_msg = f"Admin access error: {error_msg}"
|
error_msg = f"Admin access error: {error_msg}"
|
||||||
logger.error(f"Error in admin_auth_required: {error_msg}")
|
logger.error(f"Error in admin_auth_required: {error_msg}")
|
||||||
|
logger.error(f"[admin_auth_required] Ошибка авторизации: {error_msg}")
|
||||||
raise GraphQLError(error_msg) from e
|
raise GraphQLError(error_msg) from e
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
136
auth/internal.py
136
auth/internal.py
|
@ -4,17 +4,15 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy.orm import exc
|
from sqlalchemy.orm import exc
|
||||||
|
|
||||||
from auth.credentials import AuthCredentials
|
|
||||||
from auth.orm import Author
|
from auth.orm import Author
|
||||||
from auth.state import AuthState
|
from auth.state import AuthState
|
||||||
from auth.tokens.storage import TokenStorage as TokenManager
|
from auth.tokens.storage import TokenStorage as TokenManager
|
||||||
from services.db import local_session
|
from services.db import local_session
|
||||||
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
|
||||||
from settings import SESSION_COOKIE_NAME, SESSION_TOKEN_HEADER
|
|
||||||
from utils.logger import root_logger as logger
|
from utils.logger import root_logger as logger
|
||||||
|
|
||||||
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
|
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
|
||||||
|
@ -90,99 +88,63 @@ async def create_internal_session(author: Author, device_info: Optional[dict] =
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def authenticate(request: Any) -> AuthState:
|
async def authenticate(request) -> AuthState:
|
||||||
"""
|
"""
|
||||||
Аутентифицирует запрос по токену из разных источников.
|
Аутентифицирует пользователя по токену из запроса.
|
||||||
Порядок проверки:
|
|
||||||
1. Проверяет токен в заголовке Authorization
|
|
||||||
2. Проверяет токен в cookie
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: Запрос (обычно из middleware)
|
request: Объект запроса
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
AuthState: Состояние авторизации
|
AuthState: Состояние аутентификации
|
||||||
"""
|
"""
|
||||||
state = AuthState()
|
from auth.decorators import get_auth_token
|
||||||
state.logged_in = False # Изначально считаем, что пользователь не авторизован
|
from auth.tokens.sessions import SessionTokenManager
|
||||||
token = None
|
from utils.logger import root_logger as logger
|
||||||
|
|
||||||
# Проверяем наличие auth в scope (установлено middleware)
|
logger.debug("[authenticate] Начало аутентификации")
|
||||||
if hasattr(request, "scope") and isinstance(request.scope, dict) and "auth" in request.scope:
|
|
||||||
auth_info = request.scope.get("auth", {})
|
|
||||||
if isinstance(auth_info, dict) and "token" in auth_info:
|
|
||||||
token = auth_info["token"]
|
|
||||||
logger.debug("[auth.authenticate] Извлечен токен из request.scope['auth']")
|
|
||||||
|
|
||||||
# Если токен не найден в scope, проверяем заголовок
|
# Получаем токен из запроса
|
||||||
|
token = get_auth_token(request)
|
||||||
if not token:
|
if not token:
|
||||||
|
logger.warning("[authenticate] Токен не найден в запросе")
|
||||||
|
auth_state = AuthState()
|
||||||
|
auth_state.logged_in = False
|
||||||
|
auth_state.author_id = None
|
||||||
|
auth_state.error = "No authentication token provided"
|
||||||
|
auth_state.token = None
|
||||||
|
return auth_state
|
||||||
|
|
||||||
|
logger.debug(f"[authenticate] Токен найден, длина: {len(token)}")
|
||||||
|
|
||||||
|
# Проверяем токен
|
||||||
try:
|
try:
|
||||||
headers = {}
|
# Создаем экземпляр SessionTokenManager
|
||||||
if hasattr(request, "headers"):
|
session_manager = SessionTokenManager()
|
||||||
headers = dict(request.headers()) if callable(request.headers) else dict(request.headers)
|
# Проверяем токен
|
||||||
|
auth_result = await session_manager.verify_session(token)
|
||||||
|
|
||||||
auth_header = headers.get(SESSION_TOKEN_HEADER, "")
|
if auth_result and hasattr(auth_result, "user_id"):
|
||||||
if auth_header and auth_header.startswith("Bearer "):
|
logger.debug(f"[authenticate] Успешная аутентификация, user_id: {auth_result.user_id}")
|
||||||
token = auth_header[7:].strip()
|
auth_state = AuthState()
|
||||||
logger.debug(f"[auth.authenticate] Токен получен из заголовка {SESSION_TOKEN_HEADER}")
|
auth_state.logged_in = True
|
||||||
elif auth_header:
|
auth_state.author_id = auth_result.user_id
|
||||||
token = auth_header.strip()
|
auth_state.error = None
|
||||||
logger.debug(f"[auth.authenticate] Прямой токен получен из заголовка {SESSION_TOKEN_HEADER}")
|
auth_state.token = token
|
||||||
|
return auth_state
|
||||||
|
error_msg = "Invalid or expired token"
|
||||||
|
logger.warning(f"[authenticate] Недействительный токен: {error_msg}")
|
||||||
|
auth_state = AuthState()
|
||||||
|
auth_state.logged_in = False
|
||||||
|
auth_state.author_id = None
|
||||||
|
auth_state.error = error_msg
|
||||||
|
auth_state.token = None
|
||||||
|
return auth_state
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[auth.authenticate] Ошибка при доступе к заголовкам: {e}")
|
logger.error(f"[authenticate] Ошибка при проверке токена: {e}")
|
||||||
|
auth_state = AuthState()
|
||||||
# Если и в заголовке не найден, проверяем cookie
|
auth_state.logged_in = False
|
||||||
if not token and hasattr(request, "cookies") and request.cookies:
|
auth_state.author_id = None
|
||||||
token = request.cookies.get(SESSION_COOKIE_NAME)
|
auth_state.error = f"Authentication error: {e!s}"
|
||||||
if token:
|
auth_state.token = None
|
||||||
logger.debug(f"[auth.authenticate] Токен получен из cookie {SESSION_COOKIE_NAME}")
|
return auth_state
|
||||||
|
|
||||||
# Если токен все еще не найден, возвращаем не авторизованное состояние
|
|
||||||
if not token:
|
|
||||||
logger.debug("[auth.authenticate] Токен не найден")
|
|
||||||
return state
|
|
||||||
|
|
||||||
# Проверяем токен через TokenStorage, который теперь совместим с TokenStorage
|
|
||||||
payload = await TokenManager.verify_session(token)
|
|
||||||
if not payload:
|
|
||||||
logger.warning("[auth.authenticate] Токен не валиден: не найдена сессия")
|
|
||||||
state.error = "Invalid or expired token"
|
|
||||||
return state
|
|
||||||
|
|
||||||
# Создаем успешное состояние авторизации
|
|
||||||
state.logged_in = True
|
|
||||||
state.author_id = payload.user_id
|
|
||||||
state.token = token
|
|
||||||
state.username = payload.username
|
|
||||||
|
|
||||||
# Если запрос имеет атрибут auth, устанавливаем в него авторизационные данные
|
|
||||||
if hasattr(request, "scope") and isinstance(request.scope, dict):
|
|
||||||
try:
|
|
||||||
# Получаем информацию о пользователе для создания AuthCredentials
|
|
||||||
with local_session() as session:
|
|
||||||
author = session.query(Author).filter(Author.id == payload.user_id).one_or_none()
|
|
||||||
if author:
|
|
||||||
# Получаем разрешения из ролей
|
|
||||||
scopes = author.get_permissions()
|
|
||||||
|
|
||||||
# Создаем объект авторизации
|
|
||||||
auth_cred = AuthCredentials(
|
|
||||||
author_id=author.id,
|
|
||||||
scopes=scopes,
|
|
||||||
logged_in=True,
|
|
||||||
email=author.email,
|
|
||||||
token=token,
|
|
||||||
error_message="",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Устанавливаем auth в request.scope вместо прямого присваивания к request.auth
|
|
||||||
request.scope["auth"] = auth_cred
|
|
||||||
logger.debug(
|
|
||||||
f"[auth.authenticate] Авторизационные данные установлены в request.scope['auth'] для {payload.user_id}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"[auth.authenticate] Ошибка при установке auth в request.scope: {e}")
|
|
||||||
|
|
||||||
logger.info(f"[auth.authenticate] Успешная аутентификация пользователя {state.author_id}")
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|
|
@ -66,9 +66,15 @@ export async function query<T = unknown>(
|
||||||
console.log(`[GraphQL] Making request to ${endpoint}`)
|
console.log(`[GraphQL] Making request to ${endpoint}`)
|
||||||
console.log(`[GraphQL] Query: ${query.substring(0, 100)}...`)
|
console.log(`[GraphQL] Query: ${query.substring(0, 100)}...`)
|
||||||
|
|
||||||
|
// Используем существующую функцию для получения всех необходимых заголовков
|
||||||
|
const headers = getRequestHeaders()
|
||||||
|
console.log(
|
||||||
|
`[GraphQL] Заголовки установлены, Authorization: ${headers['Authorization'] ? 'присутствует' : 'отсутствует'}`
|
||||||
|
)
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getRequestHeaders(),
|
headers,
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query,
|
query,
|
||||||
|
|
|
@ -132,15 +132,13 @@ const InviteEditModal: Component<InviteEditModalProps> = (props) => {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={formData().inviter_id}
|
value={formData().inviter_id}
|
||||||
onInput={(e) => updateField('inviter_id', parseInt(e.target.value) || 0)}
|
onInput={(e) => updateField('inviter_id', Number.parseInt(e.target.value) || 0)}
|
||||||
class={`${formStyles.input} ${errors().inviter_id ? formStyles.inputError : ''}`}
|
class={`${formStyles.input} ${errors().inviter_id ? formStyles.inputError : ''}`}
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
required
|
required
|
||||||
disabled={!isCreating()} // При редактировании ID нельзя менять
|
disabled={!isCreating()} // При редактировании ID нельзя менять
|
||||||
/>
|
/>
|
||||||
<div class={formStyles.fieldHint}>
|
<div class={formStyles.fieldHint}>ID автора, который отправляет приглашение</div>
|
||||||
ID автора, который отправляет приглашение
|
|
||||||
</div>
|
|
||||||
{errors().inviter_id && <div class={formStyles.fieldError}>{errors().inviter_id}</div>}
|
{errors().inviter_id && <div class={formStyles.fieldError}>{errors().inviter_id}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -151,15 +149,13 @@ const InviteEditModal: Component<InviteEditModalProps> = (props) => {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={formData().author_id}
|
value={formData().author_id}
|
||||||
onInput={(e) => updateField('author_id', parseInt(e.target.value) || 0)}
|
onInput={(e) => updateField('author_id', Number.parseInt(e.target.value) || 0)}
|
||||||
class={`${formStyles.input} ${errors().author_id ? formStyles.inputError : ''}`}
|
class={`${formStyles.input} ${errors().author_id ? formStyles.inputError : ''}`}
|
||||||
placeholder="2"
|
placeholder="2"
|
||||||
required
|
required
|
||||||
disabled={!isCreating()} // При редактировании ID нельзя менять
|
disabled={!isCreating()} // При редактировании ID нельзя менять
|
||||||
/>
|
/>
|
||||||
<div class={formStyles.fieldHint}>
|
<div class={formStyles.fieldHint}>ID автора, которого приглашают к сотрудничеству</div>
|
||||||
ID автора, которого приглашают к сотрудничеству
|
|
||||||
</div>
|
|
||||||
{errors().author_id && <div class={formStyles.fieldError}>{errors().author_id}</div>}
|
{errors().author_id && <div class={formStyles.fieldError}>{errors().author_id}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -170,15 +166,13 @@ const InviteEditModal: Component<InviteEditModalProps> = (props) => {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={formData().shout_id}
|
value={formData().shout_id}
|
||||||
onInput={(e) => updateField('shout_id', parseInt(e.target.value) || 0)}
|
onInput={(e) => updateField('shout_id', Number.parseInt(e.target.value) || 0)}
|
||||||
class={`${formStyles.input} ${errors().shout_id ? formStyles.inputError : ''}`}
|
class={`${formStyles.input} ${errors().shout_id ? formStyles.inputError : ''}`}
|
||||||
placeholder="123"
|
placeholder="123"
|
||||||
required
|
required
|
||||||
disabled={!isCreating()} // При редактировании ID нельзя менять
|
disabled={!isCreating()} // При редактировании ID нельзя менять
|
||||||
/>
|
/>
|
||||||
<div class={formStyles.fieldHint}>
|
<div class={formStyles.fieldHint}>ID публикации, к которой приглашают на сотрудничество</div>
|
||||||
ID публикации, к которой приглашают на сотрудничество
|
|
||||||
</div>
|
|
||||||
{errors().shout_id && <div class={formStyles.fieldError}>{errors().shout_id}</div>}
|
{errors().shout_id && <div class={formStyles.fieldError}>{errors().shout_id}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -196,9 +190,7 @@ const InviteEditModal: Component<InviteEditModalProps> = (props) => {
|
||||||
<option value="ACCEPTED">Принято</option>
|
<option value="ACCEPTED">Принято</option>
|
||||||
<option value="REJECTED">Отклонено</option>
|
<option value="REJECTED">Отклонено</option>
|
||||||
</select>
|
</select>
|
||||||
<div class={formStyles.fieldHint}>
|
<div class={formStyles.fieldHint}>Текущий статус приглашения</div>
|
||||||
Текущий статус приглашения
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Информация о связанных объектах при редактировании */}
|
{/* Информация о связанных объектах при редактировании */}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import styles from '../styles/Table.module.css'
|
||||||
import Button from '../ui/Button'
|
import Button from '../ui/Button'
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
import Pagination from '../ui/Pagination'
|
import Pagination from '../ui/Pagination'
|
||||||
|
import { getAuthTokenFromCookie } from '../utils/auth'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Интерфейсы для приглашений
|
* Интерфейсы для приглашений
|
||||||
|
@ -74,16 +75,21 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
/**
|
/**
|
||||||
* Загружает список приглашений с учетом фильтров и пагинации
|
* Загружает список приглашений с учетом фильтров и пагинации
|
||||||
*/
|
*/
|
||||||
const loadInvites = async (page: number = 1) => {
|
const loadInvites = async (page = 1) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const limit = pagination().perPage
|
const limit = pagination().perPage
|
||||||
const offset = (page - 1) * limit
|
const offset = (page - 1) * limit
|
||||||
|
|
||||||
|
// Получаем токен авторизации из localStorage или cookie
|
||||||
|
const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie()
|
||||||
|
console.log(`[InvitesRoute] Загрузка приглашений, токен: ${authToken ? 'найден' : 'не найден'}`)
|
||||||
|
|
||||||
const response = await fetch('/graphql', {
|
const response = await fetch('/graphql', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: authToken ? `Bearer ${authToken}` : ''
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: ADMIN_GET_INVITES_QUERY,
|
query: ADMIN_GET_INVITES_QUERY,
|
||||||
|
@ -177,10 +183,15 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
const isCreating = !editModal().invite && createModal().show
|
const isCreating = !editModal().invite && createModal().show
|
||||||
const mutation = isCreating ? ADMIN_CREATE_INVITE_MUTATION : ADMIN_UPDATE_INVITE_MUTATION
|
const mutation = isCreating ? ADMIN_CREATE_INVITE_MUTATION : ADMIN_UPDATE_INVITE_MUTATION
|
||||||
|
|
||||||
|
// Получаем токен авторизации из localStorage или cookie
|
||||||
|
const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie()
|
||||||
|
console.log(`[InvitesRoute] Сохранение приглашения, токен: ${authToken ? 'найден' : 'не найден'}`)
|
||||||
|
|
||||||
const response = await fetch('/graphql', {
|
const response = await fetch('/graphql', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: authToken ? `Bearer ${authToken}` : ''
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: mutation,
|
query: mutation,
|
||||||
|
@ -215,10 +226,15 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
*/
|
*/
|
||||||
const deleteInvite = async (invite: Invite) => {
|
const deleteInvite = async (invite: Invite) => {
|
||||||
try {
|
try {
|
||||||
|
// Получаем токен авторизации из localStorage или cookie
|
||||||
|
const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie()
|
||||||
|
console.log(`[InvitesRoute] Удаление приглашения, токен: ${authToken ? 'найден' : 'не найден'}`)
|
||||||
|
|
||||||
const response = await fetch('/graphql', {
|
const response = await fetch('/graphql', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: authToken ? `Bearer ${authToken}` : ''
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: ADMIN_DELETE_INVITE_MUTATION,
|
query: ADMIN_DELETE_INVITE_MUTATION,
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import Any
|
||||||
|
|
||||||
from graphql import GraphQLResolveInfo
|
from graphql import GraphQLResolveInfo
|
||||||
from graphql.error import GraphQLError
|
from graphql.error import GraphQLError
|
||||||
from sqlalchemy import String, cast, or_
|
from sqlalchemy import String, cast, null, or_
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlalchemy.sql import func, select
|
from sqlalchemy.sql import func, select
|
||||||
|
|
||||||
|
@ -670,8 +670,8 @@ async def admin_restore_shout(_: None, info: GraphQLResolveInfo, shout_id: int)
|
||||||
return {"success": False, "error": "Публикация не была удалена"}
|
return {"success": False, "error": "Публикация не была удалена"}
|
||||||
|
|
||||||
# Сбрасываем время удаления
|
# Сбрасываем время удаления
|
||||||
shout.deleted_at = None
|
shout.deleted_at = null()
|
||||||
shout.deleted_by = None
|
shout.deleted_by = null()
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user