### 🍪 CRITICAL Cross-Origin Auth - **🔧 SESSION_COOKIE_DOMAIN**: Добавлена поддержка поддоменов `.discours.io` для cross-origin cookies - **🌐 Cross-Origin SSE**: Исправлена работа Server-Sent Events с httpOnly cookies между поддоменами - **🔐 Unified Auth**: Унифицированы настройки cookies для OAuth, login, refresh, logout операций - **📝 MyPy Compliance**: Исправлена типизация `SESSION_COOKIE_SAMESITE` с использованием `cast()` ### 🛠️ Technical Changes - **settings.py**: Добавлен `SESSION_COOKIE_DOMAIN` с типобезопасной настройкой SameSite - **auth/oauth.py**: Обновлены все `set_cookie` вызовы с `domain` параметром - **auth/middleware.py**: Добавлена поддержка `SESSION_COOKIE_DOMAIN` в logout операциях - **resolvers/auth.py**: Унифицированы cookie настройки в login/refresh/logout resolvers - **auth/__init__.py**: Обновлены cookie операции с domain поддержкой ### 📚 Documentation - **docs/auth/sse-httponly-integration.md**: Новая документация по SSE + httpOnly cookies интеграции - **docs/auth/architecture.md**: Обновлены диаграммы для unified httpOnly cookie архитектуры ### 🎯 Impact - ✅ **GraphQL API** (`v3.discours.io`) теперь работает с httpOnly cookies cross-origin - ✅ **SSE сервер** (`connect.discours.io`) работает с теми же cookies - ✅ **Безопасность**: httpOnly cookies защищают от XSS атак - ✅ **UX**: Автоматическая аутентификация без управления токенами в JavaScript
This commit is contained in:
@@ -91,8 +91,18 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A
|
||||
|
||||
result = await auth_service.login(email, password, request)
|
||||
|
||||
# Устанавливаем httpOnly cookie если есть токен
|
||||
if result.get("success") and result.get("token"):
|
||||
# 🎯 Проверяем откуда пришел запрос - админка или основной сайт
|
||||
request = info.context.get("request")
|
||||
is_admin_request = False
|
||||
|
||||
if request:
|
||||
# Проверяем путь запроса или Referer header
|
||||
referer = request.headers.get("referer", "")
|
||||
origin = request.headers.get("origin", "")
|
||||
is_admin_request = "/panel" in referer or "/panel" in origin or "admin" in referer
|
||||
|
||||
# Устанавливаем httpOnly cookie только для админки
|
||||
if result.get("success") and result.get("token") and is_admin_request:
|
||||
try:
|
||||
response = info.context.get("response")
|
||||
if not response:
|
||||
@@ -109,21 +119,27 @@ async def login(_: None, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, A
|
||||
else "none",
|
||||
max_age=SESSION_COOKIE_MAX_AGE,
|
||||
path="/",
|
||||
domain=SESSION_COOKIE_DOMAIN, # ✅ КРИТИЧНО для поддоменов
|
||||
domain=SESSION_COOKIE_DOMAIN,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"✅ Email/Password: httpOnly cookie установлен для пользователя {result.get('author', {}).get('id')}"
|
||||
f"✅ Admin login: httpOnly cookie установлен для пользователя {result.get('author', {}).get('id')}"
|
||||
)
|
||||
|
||||
# 💋 НЕ возвращаем токен клиенту - он в httpOnly cookie
|
||||
# Для админки НЕ возвращаем токен клиенту - он в httpOnly cookie
|
||||
result_without_token = result.copy()
|
||||
result_without_token["token"] = None # Скрываем токен от JavaScript
|
||||
result_without_token["token"] = None
|
||||
return result_without_token
|
||||
|
||||
except Exception as cookie_error:
|
||||
logger.warning(f"Не удалось установить cookie: {cookie_error}")
|
||||
|
||||
# Для основного сайта возвращаем токен как обычно (Bearer в localStorage)
|
||||
if not is_admin_request:
|
||||
logger.info(
|
||||
f"✅ Main site login: токен возвращен для localStorage пользователя {result.get('author', {}).get('id')}"
|
||||
)
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.warning(f"Ошибка входа: {e}")
|
||||
|
||||
Reference in New Issue
Block a user