Files
core/auth/oauth.py

1157 lines
52 KiB
Python
Raw Normal View History

2025-05-29 12:37:39 +03:00
import time
from secrets import token_urlsafe
2025-08-17 16:33:54 +03:00
from typing import Any, Callable
2025-05-29 12:37:39 +03:00
2025-09-28 20:34:26 +03:00
import httpx
2025-05-30 14:08:29 +03:00
import orjson
2023-10-26 22:38:31 +02:00
from authlib.integrations.starlette_client import OAuth
2025-05-16 09:23:48 +03:00
from authlib.oauth2.rfc7636 import create_s256_code_challenge
from graphql import GraphQLResolveInfo
2025-06-02 21:50:58 +03:00
from sqlalchemy.orm import Session
from starlette.requests import Request
2025-05-29 12:37:39 +03:00
from starlette.responses import JSONResponse, RedirectResponse
2023-10-30 22:00:55 +01:00
2025-06-02 21:50:58 +03:00
from auth.tokens.storage import TokenStorage
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации - **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)` ### 🧪 Тестирование - **Исправление тестов** - адаптация к новой структуре моделей - **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py` - **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев - **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями - **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода ### 🔧 Рефакторинг - **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру - **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль - **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры - **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей - **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки ### 🔧 Авторизация с cookies - **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization - **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно - **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token` - **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession` - **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author` - **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами - **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession` - **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации ### 📝 Документация - **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа - Обновлена документация RBAC - Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00
from orm.author import Author
2025-07-31 18:55:59 +03:00
from orm.community import Community, CommunityAuthor, CommunityFollower
2025-06-28 13:56:05 +03:00
from settings import (
FRONTEND_URL,
OAUTH_CLIENTS,
)
[0.9.7] - 2025-08-18 ### 🔄 Изменения - **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации - **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)` ### 🧪 Тестирование - **Исправление тестов** - адаптация к новой структуре моделей - **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py` - **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев - **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями - **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода ### 🔧 Рефакторинг - **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру - **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль - **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры - **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей - **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки ### 🔧 Авторизация с cookies - **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization - **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно - **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token` - **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession` - **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author` - **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами - **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession` - **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации ### 📝 Документация - **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа - Обновлена документация RBAC - Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00
from storage.db import local_session
from storage.redis import redis
2025-07-03 00:20:10 +03:00
from utils.generate_slug import generate_unique_slug
2025-05-30 14:05:50 +03:00
from utils.logger import root_logger as logger
2025-06-02 21:50:58 +03:00
# Type для dependency injection сессии
SessionFactory = Callable[[], Session]
class SessionManager:
"""Менеджер сессий для dependency injection с поддержкой тестирования"""
def __init__(self) -> None:
self._factory: SessionFactory = local_session
def set_factory(self, factory: SessionFactory) -> None:
"""Устанавливает фабрику сессий для dependency injection"""
self._factory = factory
def get_session(self) -> Session:
"""Получает сессию БД через dependency injection"""
return self._factory()
# Глобальный менеджер сессий
session_manager = SessionManager()
def set_session_factory(factory: SessionFactory) -> None:
"""
Устанавливает фабрику сессий для dependency injection.
Используется в тестах для подмены реальной БД на тестовую.
"""
session_manager.set_factory(factory)
def get_session() -> Session:
"""
Получает сессию БД через dependency injection.
Возвращает сессию которую нужно явно закрывать после использования.
Внимание: не забывайте закрывать сессию после использования!
Рекомендуется использовать try/finally блок.
"""
return session_manager.get_session()
oauth = OAuth()
2025-05-30 14:05:50 +03:00
# OAuth state management через Redis (TTL 10 минут)
OAUTH_STATE_TTL = 600 # 10 минут
2025-06-02 21:50:58 +03:00
# Конфигурация провайдеров для регистрации
PROVIDER_CONFIGS = {
2025-05-16 09:23:48 +03:00
"google": {
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
"client_kwargs": {
"scope": "openid email profile",
},
2025-05-16 09:23:48 +03:00
},
"github": {
"access_token_url": "https://github.com/login/oauth/access_token",
"authorize_url": "https://github.com/login/oauth/authorize",
"api_base_url": "https://api.github.com/",
"client_kwargs": {
"scope": "read:user user:email",
},
2025-05-16 09:23:48 +03:00
},
"facebook": {
"access_token_url": "https://graph.facebook.com/v18.0/oauth/access_token",
"authorize_url": "https://www.facebook.com/v18.0/dialog/oauth",
2025-05-16 09:23:48 +03:00
"api_base_url": "https://graph.facebook.com/",
"scope": "email public_profile", # Явно указываем необходимые scope
2025-05-16 09:23:48 +03:00
},
"x": {
"access_token_url": "https://api.twitter.com/2/oauth2/token",
"authorize_url": "https://twitter.com/i/oauth2/authorize",
"api_base_url": "https://api.twitter.com/2/",
"client_kwargs": {
"scope": "tweet.read users.read", # Базовые scope для X API v2
},
},
"telegram": {
"access_token_url": "https://oauth.telegram.org/auth/request",
"authorize_url": "https://oauth.telegram.org/auth",
"api_base_url": "https://api.telegram.org/",
"client_kwargs": {
"scope": "read", # Базовый scope для Telegram
},
},
"vk": {
"access_token_url": "https://oauth.vk.com/access_token",
"authorize_url": "https://oauth.vk.com/authorize",
"api_base_url": "https://api.vk.com/method/",
"client_kwargs": {
"scope": "email", # Минимальный scope для получения email
},
},
"yandex": {
"access_token_url": "https://oauth.yandex.ru/token",
"authorize_url": "https://oauth.yandex.ru/authorize",
"api_base_url": "https://login.yandex.ru/info",
"client_kwargs": {
"scope": "login:email login:info login:avatar", # Scope для получения профиля
},
},
}
2025-06-02 21:50:58 +03:00
# Константы для генерации временного email
TEMP_EMAIL_SUFFIX = "@oauth.local"
def _generate_temp_email(provider: str, user_id: str) -> str:
"""Генерирует временный email для OAuth провайдеров без email"""
return f"{provider}_{user_id}@oauth.local"
def _register_oauth_provider(provider: str, client_config: dict) -> None:
"""Регистрирует OAuth провайдер в зависимости от его типа"""
try:
provider_config = PROVIDER_CONFIGS.get(provider, {})
if not provider_config:
logger.warning(f"Unknown OAuth provider: {provider}")
return
# 🔍 Отладочная информация
logger.info(
f"Registering OAuth provider {provider} with client_id: {client_config['id'][:8] if client_config['id'] else 'EMPTY'}..."
)
2025-06-02 21:50:58 +03:00
# Базовые параметры для всех провайдеров
register_params: dict[str, Any] = {
2025-06-02 21:50:58 +03:00
"name": provider,
"client_id": client_config["id"],
"client_secret": client_config["key"],
}
# Добавляем конфигурацию провайдера с явной типизацией
if isinstance(provider_config, dict):
register_params.update(provider_config)
# 🔒 Для Facebook добавляем дополнительные параметры безопасности
if provider == "facebook":
register_params.update(
{
"client_kwargs": {
"scope": "email public_profile",
"token_endpoint_auth_method": "client_secret_post",
}
}
)
2025-06-02 21:50:58 +03:00
oauth.register(**register_params)
logger.info(f"OAuth provider {provider} registered successfully")
# 🔍 Проверяем что клиент действительно создался
test_client = oauth.create_client(provider)
if test_client:
logger.info(f"OAuth client {provider} created successfully")
else:
logger.error(f"OAuth client {provider} failed to create after registration")
2025-06-02 21:50:58 +03:00
except Exception as e:
logger.error(f"Failed to register OAuth provider {provider}: {e}")
# 🔍 Диагностика OAuth конфигурации
logger.info(f"Available OAuth providers in config: {list(PROVIDER_CONFIGS.keys())}")
logger.info(f"Available OAuth clients: {list(OAUTH_CLIENTS.keys())}")
2025-06-02 21:50:58 +03:00
for provider in PROVIDER_CONFIGS:
if provider.upper() in OAUTH_CLIENTS:
client_config = OAUTH_CLIENTS[provider.upper()]
# 🔍 Проверяем что id и key не пустые
client_id = client_config.get("id", "").strip()
client_key = client_config.get("key", "").strip()
logger.info(
f"OAuth provider {provider}: id={'SET' if client_id else 'EMPTY'}, key={'SET' if client_key else 'EMPTY'}"
)
if client_id and client_key:
2025-06-02 21:50:58 +03:00
_register_oauth_provider(provider, client_config)
else:
logger.warning(f"OAuth provider {provider} skipped: id={bool(client_id)}, key={bool(client_key)}")
else:
logger.warning(f"OAuth provider {provider} not found in OAUTH_CLIENTS")
2025-06-02 21:50:58 +03:00
# Провайдеры со специальной обработкой данных
PROVIDER_HANDLERS = {
"google": lambda token, _: {
"id": token.get("userinfo", {}).get("sub"),
"email": token.get("userinfo", {}).get("email"),
"name": token.get("userinfo", {}).get("name"),
"picture": token.get("userinfo", {}).get("picture", "").replace("=s96", "=s600"),
},
"telegram": lambda token, _: {
"id": str(token.get("id", "")),
"email": None,
"phone": str(token.get("phone_number", "")),
"name": token.get("first_name", "") + " " + token.get("last_name", ""),
"picture": token.get("photo_url"),
},
"x": lambda _, profile_data: {
"id": profile_data.get("data", {}).get("id"),
"email": None,
"name": profile_data.get("data", {}).get("name") or profile_data.get("data", {}).get("username"),
"picture": profile_data.get("data", {}).get("profile_image_url", "").replace("_normal", "_400x400"),
},
}
async def _fetch_github_profile(client: Any, token: Any) -> dict:
"""Получает профиль из GitHub API"""
try:
2025-09-28 20:52:17 +03:00
# Извлекаем access_token из ответа
access_token = token.get("access_token") if isinstance(token, dict) else token
2025-09-28 20:52:17 +03:00
if not access_token:
logger.error("No access_token found in GitHub token response")
return {}
2025-09-28 20:52:17 +03:00
# Используем прямой HTTP запрос к GitHub API
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/vnd.github.v3+json",
"User-Agent": "Discours-OAuth-Client",
}
2025-09-28 20:52:17 +03:00
async with httpx.AsyncClient() as http_client:
# Получаем основной профиль
profile_response = await http_client.get("https://api.github.com/user", headers=headers)
2025-09-28 20:52:17 +03:00
if profile_response.status_code != 200:
logger.error(f"GitHub API error: {profile_response.status_code} - {profile_response.text}")
return {}
profile_data = profile_response.json()
# Получаем email адреса (требует scope user:email)
emails_response = await http_client.get("https://api.github.com/user/emails", headers=headers)
emails_data = emails_response.json() if emails_response.status_code == 200 else []
# Ищем основной email
primary_email = None
if isinstance(emails_data, list):
primary_email = next((email["email"] for email in emails_data if email.get("primary")), None)
return {
"id": str(profile_data.get("id", "")),
"email": primary_email or profile_data.get("email"),
"name": profile_data.get("name") or profile_data.get("login", ""),
"picture": profile_data.get("avatar_url"),
}
except Exception as e:
logger.error(f"Error fetching GitHub profile: {e}")
return {}
2025-06-02 21:50:58 +03:00
async def _fetch_facebook_profile(client: Any, token: Any) -> dict:
"""Получает профиль из Facebook API"""
try:
# Используем актуальную версию API v18.0+ и расширенные поля
profile = await client.get("me?fields=id,name,email,picture.width(600).height(600)", token=token)
profile_data = profile.json()
# Проверяем наличие ошибок в ответе Facebook
if "error" in profile_data:
logger.error(f"Facebook API error: {profile_data['error']}")
return {}
return {
"id": str(profile_data.get("id", "")),
"email": profile_data.get("email"), # Может быть None если не предоставлен
"name": profile_data.get("name", ""),
"picture": profile_data.get("picture", {}).get("data", {}).get("url"),
}
except Exception as e:
logger.error(f"Error fetching Facebook profile: {e}")
return {}
2025-06-02 21:50:58 +03:00
async def _fetch_x_profile(client: Any, token: Any) -> dict:
"""Получает профиль из X (Twitter) API"""
try:
# Используем правильный endpoint для X API v2
profile = await client.get("users/me?user.fields=id,name,username,profile_image_url", token=token)
profile_data = profile.json()
# Проверяем наличие ошибок в ответе X
if "errors" in profile_data:
logger.error(f"X API error: {profile_data['errors']}")
return {}
return PROVIDER_HANDLERS["x"](token, profile_data)
except Exception as e:
logger.error(f"Error fetching X profile: {e}")
return {}
2025-06-02 21:50:58 +03:00
async def _fetch_vk_profile(client: Any, token: Any) -> dict:
"""Получает профиль из VK API"""
try:
# Используем актуальную версию API v5.199+
profile = await client.get("users.get?fields=photo_400_orig,contacts&v=5.199", token=token)
profile_data = profile.json()
# Проверяем наличие ошибок в ответе VK
if "error" in profile_data:
logger.error(f"VK API error: {profile_data['error']}")
return {}
if profile_data.get("response"):
user_data = profile_data["response"][0]
return {
"id": str(user_data["id"]),
"email": user_data.get("contacts", {}).get("email"),
"name": f"{user_data.get('first_name', '')} {user_data.get('last_name', '')}".strip(),
"picture": user_data.get("photo_400_orig"),
}
return {}
except Exception as e:
logger.error(f"Error fetching VK profile: {e}")
return {}
2025-05-16 09:23:48 +03:00
2025-06-02 21:50:58 +03:00
async def _fetch_yandex_profile(client: Any, token: Any) -> dict:
"""Получает профиль из Yandex API"""
profile = await client.get("?format=json", token=token)
profile_data = profile.json()
return {
"id": profile_data.get("id"),
"email": profile_data.get("default_email"),
"name": profile_data.get("display_name") or profile_data.get("real_name"),
"picture": f"https://avatars.yandex.net/get-yapic/{profile_data.get('default_avatar_id')}/islands-200"
if profile_data.get("default_avatar_id")
else None,
}
2025-09-29 00:27:16 +03:00
async def _fetch_google_profile(client: Any, token: Any) -> dict:
"""Получает профиль из Google API"""
try:
# Извлекаем access_token из ответа
access_token = token.get("access_token") if isinstance(token, dict) else token
if not access_token:
logger.error("No access_token found in Google token response")
return {}
# Используем прямой HTTP запрос к Google API
headers = {
"Authorization": f"Bearer {access_token}",
"Accept": "application/json",
}
async with httpx.AsyncClient() as http_client:
# Получаем профиль пользователя
profile_response = await http_client.get("https://www.googleapis.com/oauth2/v2/userinfo", headers=headers)
if profile_response.status_code != 200:
logger.error(f"Google API error: {profile_response.status_code} - {profile_response.text}")
return {}
profile_data = profile_response.json()
return {
"id": str(profile_data.get("id", "")),
"email": profile_data.get("email"),
"name": profile_data.get("name", ""),
"picture": profile_data.get("picture", "").replace("=s96", "=s600"),
}
except Exception as e:
logger.error(f"Error fetching Google profile: {e}")
return {}
2025-06-02 21:50:58 +03:00
async def get_user_profile(provider: str, client: Any, token: Any) -> dict:
"""Получает профиль пользователя от провайдера OAuth"""
# Провайдеры требующие API вызовов
profile_fetchers = {
2025-09-29 00:27:16 +03:00
"google": _fetch_google_profile,
2025-06-02 21:50:58 +03:00
"github": _fetch_github_profile,
"facebook": _fetch_facebook_profile,
"x": _fetch_x_profile,
"vk": _fetch_vk_profile,
"yandex": _fetch_yandex_profile,
}
if provider in profile_fetchers:
return await profile_fetchers[provider](client, token)
2025-09-29 00:27:16 +03:00
# Простые провайдеры с обработкой через lambda (только для telegram теперь)
if provider in PROVIDER_HANDLERS:
return PROVIDER_HANDLERS[provider](token, None)
2025-06-02 21:50:58 +03:00
return {}
async def oauth_login(_: None, _info: GraphQLResolveInfo, provider: str, callback_data: dict[str, Any]) -> JSONResponse:
"""
Обработка OAuth авторизации
Args:
provider: Провайдер OAuth (google, github, etc.)
callback_data: Данные из callback-а
Returns:
dict: Результат авторизации с токеном или ошибкой
"""
2025-06-02 21:50:58 +03:00
if provider not in PROVIDER_CONFIGS:
2025-05-16 09:23:48 +03:00
return JSONResponse({"error": "Invalid provider"}, status_code=400)
client = oauth.create_client(provider)
2025-05-16 09:23:48 +03:00
if not client:
logger.error(f"OAuth client for {provider} not found. Available clients: {list(oauth._clients.keys())}")
2025-05-16 09:23:48 +03:00
return JSONResponse({"error": "Provider not configured"}, status_code=400)
2025-05-30 14:05:50 +03:00
# Получаем параметры из query string
state = callback_data.get("state")
redirect_uri = callback_data.get("redirect_uri", FRONTEND_URL)
2025-05-30 14:08:29 +03:00
2025-05-30 14:05:50 +03:00
if not state:
return JSONResponse({"error": "State parameter is required"}, status_code=400)
2025-05-16 09:23:48 +03:00
# Генерируем PKCE challenge
code_verifier = token_urlsafe(32)
code_challenge = create_s256_code_challenge(code_verifier)
2025-05-30 14:05:50 +03:00
# Сохраняем состояние OAuth в Redis
oauth_data = {
"code_verifier": code_verifier,
"provider": provider,
"redirect_uri": redirect_uri,
2025-05-30 14:08:29 +03:00
"created_at": int(time.time()),
2025-05-30 14:05:50 +03:00
}
await store_oauth_state(state, oauth_data)
2025-05-16 09:23:48 +03:00
# Callback должен идти на backend с принудительным HTTPS для продакшна
2025-09-24 13:35:49 +03:00
# Извлекаем только схему и хост из base_url (убираем путь!)
from urllib.parse import urlparse
parsed_url = urlparse(callback_data["base_url"])
scheme = "https" if parsed_url.netloc != "localhost:8000" else parsed_url.scheme
backend_base_url = f"{scheme}://{parsed_url.netloc}"
oauth_callback_uri = f"{backend_base_url}/oauth/{provider}/callback"
logger.info(f"🔗 GraphQL callback URI: '{oauth_callback_uri}'")
2025-05-16 09:23:48 +03:00
try:
return await client.authorize_redirect(
callback_data["request"],
2025-05-30 14:05:50 +03:00
oauth_callback_uri,
2025-05-16 09:23:48 +03:00
code_challenge=code_challenge,
code_challenge_method="S256",
2025-05-30 14:05:50 +03:00
state=state,
2025-05-16 09:23:48 +03:00
)
except Exception as e:
logger.error(f"OAuth redirect error for {provider}: {e!s}")
2025-05-16 09:23:48 +03:00
return JSONResponse({"error": str(e)}, status_code=500)
2025-06-02 21:50:58 +03:00
async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse:
2025-06-28 13:56:05 +03:00
"""
Обработчик OAuth callback.
Создает или обновляет пользователя и устанавливает сессионный токен.
"""
2025-05-16 09:23:48 +03:00
try:
2025-06-28 14:04:23 +03:00
provider = request.path_params.get("provider")
2025-05-16 09:23:48 +03:00
if not provider:
2025-06-28 14:04:23 +03:00
return JSONResponse({"error": "Provider not specified"}, status_code=400)
2025-05-16 09:23:48 +03:00
2025-06-28 14:04:23 +03:00
# Получаем OAuth клиента
2025-05-16 09:23:48 +03:00
client = oauth.create_client(provider)
if not client:
2025-06-28 14:04:23 +03:00
return JSONResponse({"error": "Invalid provider"}, status_code=400)
2025-05-16 09:23:48 +03:00
2025-06-28 14:04:23 +03:00
# Получаем токен
token = await client.authorize_access_token(request)
if not token:
return JSONResponse({"error": "Failed to get access token"}, status_code=400)
2025-05-16 09:23:48 +03:00
# Получаем профиль пользователя
profile = await get_user_profile(provider, client, token)
2025-06-28 14:04:23 +03:00
if not profile:
return JSONResponse({"error": "Failed to get user profile"}, status_code=400)
2025-06-28 14:04:23 +03:00
# Создаем или обновляем пользователя
2025-06-02 21:50:58 +03:00
author = await _create_or_update_user(provider, profile)
2025-06-28 14:04:23 +03:00
if not author:
return JSONResponse({"error": "Failed to create/update user"}, status_code=500)
# Создаем сессию
session_token = await TokenStorage.create_session(
str(author.id),
auth_data={
"provider": provider,
"profile": profile,
},
2025-06-30 22:43:32 +03:00
username=author.name
if isinstance(author.name, str)
else str(author.name)
if author.name is not None
else None,
2025-06-28 14:04:23 +03:00
device_info={
"user_agent": request.headers.get("user-agent"),
2025-09-23 18:54:56 +03:00
"ip": request.client.host if hasattr(request, "client") and request.client else None,
2025-06-28 14:04:23 +03:00
},
)
# Получаем state из Redis для редиректа
state = request.query_params.get("state")
state_data = await get_oauth_state(state) if state else None
redirect_uri = state_data.get("redirect_uri") if state_data else FRONTEND_URL
2025-06-30 22:43:32 +03:00
if not isinstance(redirect_uri, str) or not redirect_uri:
redirect_uri = FRONTEND_URL
2025-05-16 09:23:48 +03:00
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
# 🎯 Стандартный OAuth flow: токен в URL для фронтенда
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
2025-09-24 19:30:06 +03:00
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
parsed_url = urlparse(redirect_uri)
2025-09-24 19:30:06 +03:00
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
# 🌐 OAuth: токен в URL (стандартный подход)
logger.info("🌐 OAuth: using token in URL")
query_params = parse_qs(parsed_url.query)
query_params["access_token"] = [session_token]
if state:
query_params["state"] = [state]
new_query = urlencode(query_params, doseq=True)
final_redirect_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
new_query,
parsed_url.fragment,
[0.9.29] - 2025-09-26 ### 🚨 CRITICAL Security Fixes - **🔒 Open Redirect Protection**: Добавлена строгая валидация redirect_uri против whitelist доменов - **🔒 Rate Limiting**: Защита OAuth endpoints от брутфорса (10 попыток за 5 минут на IP) - **🔒 Logout Endpoint**: Критически важный endpoint для безопасного отзыва httpOnly cookies - **🔒 Provider Validation**: Усиленная валидация OAuth провайдеров с логированием атак - **🚨 GlitchTip Alerts**: Автоматические алерты безопасности в GlitchTip при критических событиях ### 🛡️ Security Modules - **auth/oauth_security.py**: Модуль безопасности OAuth с валидацией и rate limiting + GlitchTip алерты - **auth/logout.py**: Безопасный logout с поддержкой JSON API и browser redirect - **tests/test_oauth_security.py**: Комплексные тесты безопасности (11 тестов) - **tests/test_oauth_glitchtip_alerts.py**: Тесты интеграции с GlitchTip (8 тестов) ### 🔧 OAuth Improvements - **Minimal Flow**: Упрощен до минимума - только httpOnly cookie, нет JWT в URL - **Simple Logic**: Нет error параметра = успех, максимальная простота - **DRY Refactoring**: Устранено дублирование кода в logout и валидации ### 🎯 OAuth Endpoints - **Старт**: `v3.dscrs.site/oauth/{provider}` - с rate limiting и валидацией - **Callback**: `v3.dscrs.site/oauth/{provider}/callback` - безопасный redirect_uri - **Logout**: `v3.dscrs.site/auth/logout` - отзыв httpOnly cookies - **Финализация**: `testing.discours.io/oauth?redirect_url=...` - минимальная схема ### 📊 Security Test Coverage - ✅ Open redirect attack prevention - ✅ Rate limiting protection - ✅ Provider validation - ✅ Safe fallback mechanisms - ✅ Cookie security (httpOnly + Secure + SameSite) - ✅ GlitchTip integration (8 тестов алертов) ### 📝 Documentation - Создан `docs/oauth-minimal-flow.md` - полное описание минимального flow - Обновлена документация OAuth в `docs/auth/oauth.md` - Добавлены security best practices
2025-09-26 21:03:45 +03:00
)
2025-05-16 09:23:48 +03:00
)
2025-06-28 14:04:23 +03:00
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
# 🔗 Редиректим с токеном в URL
response = RedirectResponse(url=final_redirect_url, status_code=307)
logger.info(f"✅ OAuth: токен передан в URL для user_id={author.id}")
logger.info(f"🔗 Redirect URL: {final_redirect_url}")
2025-09-27 20:25:30 +03:00
2025-06-28 14:04:23 +03:00
logger.info(f"OAuth успешно завершен для {provider}, user_id={author.id}")
2025-05-16 09:23:48 +03:00
return response
except Exception as e:
2025-09-24 23:11:01 +03:00
logger.error(f"OAuth callback error for {provider}: {e!s}", exc_info=True)
logger.error(f"OAuth callback request URL: {request.url}")
logger.error(f"OAuth callback query params: {dict(request.query_params)}")
2025-05-30 14:05:50 +03:00
# В случае ошибки редиректим на фронтенд с ошибкой
fallback_redirect = request.query_params.get("redirect_uri", FRONTEND_URL)
2025-09-24 23:11:01 +03:00
return RedirectResponse(url=f"{fallback_redirect}?error=auth_failed&provider={provider}")
2025-05-30 14:05:50 +03:00
async def store_oauth_state(state: str, data: dict) -> None:
"""Сохраняет OAuth состояние в Redis с TTL"""
key = f"oauth_state:{state}"
await redis.execute("SETEX", key, OAUTH_STATE_TTL, orjson.dumps(data))
2025-05-30 14:08:29 +03:00
2025-08-17 16:33:54 +03:00
async def get_oauth_state(state: str) -> dict | None:
2025-05-30 14:05:50 +03:00
"""Получает и удаляет OAuth состояние из Redis (one-time use)"""
key = f"oauth_state:{state}"
data = await redis.execute("GET", key)
if data:
await redis.execute("DEL", key) # Одноразовое использование
return orjson.loads(data)
return None
# HTTP handlers для тестирования
async def oauth_login_http(request: Request) -> JSONResponse | RedirectResponse:
"""HTTP handler для OAuth login"""
try:
2025-09-29 08:15:15 +03:00
# 🚫 Блокируем запросы от ботов (GPTBot, crawlers)
user_agent = request.headers.get("user-agent", "").lower()
if (
any(bot in user_agent for bot in ["gptbot", "crawler", "spider", "bot"])
or "x-openai-host-hash" in request.headers
):
logger.warning(f"🤖 Blocked OAuth request from bot: {user_agent}")
return JSONResponse({"error": "OAuth not available for bots"}, status_code=403)
provider = request.path_params.get("provider")
2025-09-24 13:35:49 +03:00
logger.info(
f"🔍 OAuth login request: provider='{provider}', url='{request.url}', path_params={request.path_params}, query_params={dict(request.query_params)}"
)
2025-06-02 21:50:58 +03:00
if not provider or provider not in PROVIDER_CONFIGS:
2025-09-24 13:35:49 +03:00
logger.error(f"❌ Invalid provider: '{provider}', available: {list(PROVIDER_CONFIGS.keys())}")
return JSONResponse({"error": "Invalid provider"}, status_code=400)
client = oauth.create_client(provider)
if not client:
logger.error(f"OAuth client for {provider} not found. Available clients: {list(oauth._clients.keys())}")
return JSONResponse({"error": "Provider not configured"}, status_code=400)
# Генерируем PKCE challenge
code_verifier = token_urlsafe(32)
code_challenge = create_s256_code_challenge(code_verifier)
state = token_urlsafe(32)
2025-09-29 16:33:49 +03:00
# 🎯 Получаем redirect_uri из query параметра (фронтенд должен передавать явно)
explicit_redirect_uri = request.query_params.get("redirect_uri")
if explicit_redirect_uri:
# Декодируем если URL-encoded
from urllib.parse import unquote
if "%3A" in explicit_redirect_uri or "%2F" in explicit_redirect_uri:
explicit_redirect_uri = unquote(explicit_redirect_uri)
# Если это /oauth, меняем на /settings
if "/oauth" in explicit_redirect_uri:
from urllib.parse import urlparse, urlunparse
parsed = urlparse(explicit_redirect_uri)
explicit_redirect_uri = urlunparse(
(parsed.scheme, parsed.netloc, "/settings", parsed.params, "", parsed.fragment)
)
logger.info(f"🔧 Changed /oauth redirect to /settings: {explicit_redirect_uri}")
final_redirect_uri = explicit_redirect_uri
else:
# Fallback на настройки профиля
final_redirect_uri = FRONTEND_URL.rstrip("/") + "/settings"
logger.info(f"🎯 Final redirect URI: '{final_redirect_uri}'")
# 🔑 Создаем state с redirect URL и случайным значением для безопасности
import base64
import json
state_data = {
"redirect_uri": final_redirect_uri,
"random": token_urlsafe(16), # Для CSRF protection
"timestamp": int(time.time()),
}
# Кодируем state в base64 для передачи в URL
state_json = json.dumps(state_data)
state = base64.urlsafe_b64encode(state_json.encode()).decode().rstrip("=")
logger.info(f"🔑 Created state with redirect_uri: {final_redirect_uri}")
2025-09-24 13:35:49 +03:00
oauth_data = {
"code_verifier": code_verifier,
"provider": provider,
2025-09-24 08:18:44 +03:00
"redirect_uri": final_redirect_uri,
2025-09-29 16:33:49 +03:00
"state_data": state_data, # Сохраняем для callback
"created_at": int(time.time()),
}
await store_oauth_state(state, oauth_data)
2025-09-24 13:35:49 +03:00
# Получаем БАЗОВЫЙ backend URL (только схема + хост, без пути!)
scheme = "https" if request.url.netloc != "localhost:8000" else request.url.scheme
2025-09-24 13:35:49 +03:00
backend_base_url = f"{scheme}://{request.url.netloc}"
callback_uri = f"{backend_base_url}/oauth/{provider}/callback"
logger.info(f"🔗 Backend base URL: '{backend_base_url}'")
2025-09-24 23:11:01 +03:00
logger.info(f"🔗 Callback URI for {provider}: '{callback_uri}'")
2025-09-23 20:49:25 +03:00
# 🔍 Создаем redirect URL вручную (обходим использование request.session в authlib)
2025-09-24 23:11:01 +03:00
# VK, Facebook не поддерживают PKCE, используем code_challenge только для поддерживающих провайдеров
if provider in ["vk", "yandex", "telegram", "facebook"]:
2025-09-23 21:22:47 +03:00
# Провайдеры без PKCE поддержки
2025-09-24 23:11:01 +03:00
logger.info(f"🔧 Creating authorization URL without PKCE for {provider}")
2025-09-23 21:22:47 +03:00
authorization_url = await client.create_authorization_url(
callback_uri,
state=state,
)
else:
2025-09-24 23:11:01 +03:00
# Провайдеры с PKCE поддержкой (Google, GitHub, X)
logger.info(f"🔧 Creating authorization URL with PKCE for {provider}")
2025-09-23 21:22:47 +03:00
authorization_url = await client.create_authorization_url(
callback_uri,
code_challenge=code_challenge,
code_challenge_method="S256",
state=state,
)
2025-09-24 23:11:01 +03:00
logger.info(f"🚀 {provider.title()} authorization URL: '{authorization_url['url']}'")
2025-09-23 20:49:25 +03:00
return RedirectResponse(url=authorization_url["url"], status_code=302)
except Exception as e:
logger.error(f"OAuth login error: {e}")
return JSONResponse({"error": "OAuth login failed"}, status_code=500)
async def oauth_callback_http(request: Request) -> JSONResponse | RedirectResponse:
"""HTTP handler для OAuth callback"""
2025-09-29 13:59:49 +03:00
logger.info("🔄 OAuth callback started")
try:
2025-09-29 08:15:15 +03:00
# 🚫 Блокируем запросы от ботов (GPTBot, crawlers)
user_agent = request.headers.get("user-agent", "").lower()
if (
any(bot in user_agent for bot in ["gptbot", "crawler", "spider", "bot"])
or "x-openai-host-hash" in request.headers
):
logger.warning(f"🤖 Blocked OAuth request from bot: {user_agent}")
return JSONResponse({"error": "OAuth not available for bots"}, status_code=403)
# 🔍 Диагностика входящего callback запроса
logger.info("🔄 OAuth callback received:")
logger.info(f" - URL: {request.url}")
logger.info(f" - Method: {request.method}")
logger.info(f" - Headers: {dict(request.headers)}")
logger.info(f" - Query params: {dict(request.query_params)}")
logger.info(f" - Path params: {request.path_params}")
2025-09-23 18:54:56 +03:00
# 🔍 Получаем состояние OAuth только из Redis (убираем зависимость от request.session)
state = request.query_params.get("state")
2025-09-23 18:54:56 +03:00
if not state:
logger.error("❌ Missing OAuth state parameter")
2025-09-23 18:54:56 +03:00
return JSONResponse({"error": "Missing OAuth state parameter"}, status_code=400)
oauth_data = await get_oauth_state(state)
if not oauth_data:
logger.warning(f"🚨 OAuth state {state} not found or expired")
[0.9.29] - 2025-09-26 ### 🚨 CRITICAL Security Fixes - **🔒 Open Redirect Protection**: Добавлена строгая валидация redirect_uri против whitelist доменов - **🔒 Rate Limiting**: Защита OAuth endpoints от брутфорса (10 попыток за 5 минут на IP) - **🔒 Logout Endpoint**: Критически важный endpoint для безопасного отзыва httpOnly cookies - **🔒 Provider Validation**: Усиленная валидация OAuth провайдеров с логированием атак - **🚨 GlitchTip Alerts**: Автоматические алерты безопасности в GlitchTip при критических событиях ### 🛡️ Security Modules - **auth/oauth_security.py**: Модуль безопасности OAuth с валидацией и rate limiting + GlitchTip алерты - **auth/logout.py**: Безопасный logout с поддержкой JSON API и browser redirect - **tests/test_oauth_security.py**: Комплексные тесты безопасности (11 тестов) - **tests/test_oauth_glitchtip_alerts.py**: Тесты интеграции с GlitchTip (8 тестов) ### 🔧 OAuth Improvements - **Minimal Flow**: Упрощен до минимума - только httpOnly cookie, нет JWT в URL - **Simple Logic**: Нет error параметра = успех, максимальная простота - **DRY Refactoring**: Устранено дублирование кода в logout и валидации ### 🎯 OAuth Endpoints - **Старт**: `v3.dscrs.site/oauth/{provider}` - с rate limiting и валидацией - **Callback**: `v3.dscrs.site/oauth/{provider}/callback` - безопасный redirect_uri - **Logout**: `v3.dscrs.site/auth/logout` - отзыв httpOnly cookies - **Финализация**: `testing.discours.io/oauth?redirect_url=...` - минимальная схема ### 📊 Security Test Coverage - ✅ Open redirect attack prevention - ✅ Rate limiting protection - ✅ Provider validation - ✅ Safe fallback mechanisms - ✅ Cookie security (httpOnly + Secure + SameSite) - ✅ GlitchTip integration (8 тестов алертов) ### 📝 Documentation - Создан `docs/oauth-minimal-flow.md` - полное описание минимального flow - Обновлена документация OAuth в `docs/auth/oauth.md` - Добавлены security best practices
2025-09-26 21:03:45 +03:00
# Для testing.discours.io редиректим с ошибкой
error_redirect = "https://testing.discours.io/oauth?error=oauth_state_expired"
return RedirectResponse(url=error_redirect, status_code=302)
2025-09-23 18:54:56 +03:00
provider = oauth_data.get("provider")
if not provider:
return JSONResponse({"error": "No provider in OAuth state"}, status_code=400)
2025-09-23 21:34:48 +03:00
# Дополнительная проверка провайдера из path параметров (для старого формата)
provider_from_path = request.path_params.get("provider")
if provider_from_path and provider_from_path != provider:
return JSONResponse({"error": "Provider mismatch"}, status_code=400)
# Используем существующую логику
client = oauth.create_client(provider)
2025-09-23 18:54:56 +03:00
if not client:
logger.warning(f"🚨 OAuth provider {provider} not configured - returning graceful error")
2025-09-27 12:31:53 +03:00
# Проверяем конфигурацию провайдера
from settings import OAUTH_CLIENTS
provider_config = OAUTH_CLIENTS.get(provider.upper(), {})
logger.error(
f"🚨 OAuth config for {provider}: client_id={'***' if provider_config.get('id') else 'MISSING'}, client_secret={'***' if provider_config.get('key') else 'MISSING'}"
)
# Graceful fallback: редиректим на фронтенд с информативной ошибкой
redirect_uri = oauth_data.get("redirect_uri", FRONTEND_URL)
error_url = f"{redirect_uri}?error=provider_not_configured&provider={provider}&message=OAuth+provider+credentials+missing"
return RedirectResponse(url=error_url, status_code=302)
2025-09-23 18:54:56 +03:00
2025-09-23 20:49:25 +03:00
# Получаем authorization code из query параметров
code = request.query_params.get("code")
if not code:
return JSONResponse({"error": "Missing authorization code"}, status_code=400)
2025-09-23 21:22:47 +03:00
# 🔍 Обмениваем code на токен - с PKCE или без в зависимости от провайдера
logger.info("🔄 Step 1: Exchanging authorization code for access token...")
2025-09-27 12:31:53 +03:00
logger.info(f"🔧 Authorization response URL: {request.url}")
logger.info(f"🔧 Code parameter: {code[:20]}..." if code and len(code) > 20 else f"🔧 Code parameter: {code}")
2025-09-28 20:04:52 +03:00
# Получаем БАЗОВЫЙ backend URL (только схема + хост, без пути!)
scheme = "https" if request.url.netloc != "localhost:8000" else request.url.scheme
backend_base_url = f"{scheme}://{request.url.netloc}"
# Получаем callback URI (тот же, что использовался при авторизации)
callback_uri = f"{backend_base_url}/oauth/{provider}/callback"
try:
if provider in ["vk", "yandex", "telegram", "facebook"]:
# Провайдеры без PKCE поддержки (Facebook может иметь проблемы с PKCE)
logger.info(f"🔧 Using OAuth without PKCE for {provider}")
2025-09-28 20:04:52 +03:00
logger.info(f"🔧 Callback URI: {callback_uri}")
2025-09-28 20:45:08 +03:00
# Получаем token endpoint для провайдера
token_endpoints = {
"vk": "https://oauth.vk.com/access_token",
"yandex": "https://oauth.yandex.ru/token",
"telegram": "https://oauth.telegram.org/auth/token",
"facebook": "https://graph.facebook.com/v18.0/oauth/access_token",
}
2025-09-28 20:52:17 +03:00
2025-09-28 20:45:08 +03:00
token_endpoint = token_endpoints.get(provider)
if not token_endpoint:
logger.error(f"❌ Unknown token endpoint for provider: {provider}")
return JSONResponse({"error": f"Unknown provider: {provider}"}, status_code=400)
2025-09-28 20:34:26 +03:00
2025-09-28 20:45:08 +03:00
# Используем внутренний HTTP клиент для прямого запроса к token endpoint
2025-09-28 20:34:26 +03:00
token_data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": callback_uri,
"client_id": client.client_id,
}
# Для некоторых провайдеров может потребоваться client_secret
if hasattr(client, "client_secret") and client.client_secret:
token_data["client_secret"] = client.client_secret
async with httpx.AsyncClient() as http_client:
2025-09-30 19:20:41 +03:00
token_response = await http_client.post(
2025-09-28 20:45:08 +03:00
token_endpoint, data=token_data, headers={"Accept": "application/json"}
2025-09-28 20:34:26 +03:00
)
2025-09-30 19:20:41 +03:00
if token_response.status_code != 200:
error_msg = f"Token request failed: {token_response.status_code} - {token_response.text}"
2025-09-28 20:34:26 +03:00
logger.error(f"{error_msg}")
raise ValueError(error_msg)
2025-09-30 19:20:41 +03:00
token = token_response.json()
else:
# Провайдеры с PKCE поддержкой
code_verifier = oauth_data.get("code_verifier")
if not code_verifier:
logger.error(f"❌ Missing code verifier for {provider}")
return JSONResponse({"error": "Missing code verifier in OAuth state"}, status_code=400)
logger.info(f"🔧 Using OAuth with PKCE for {provider}")
2025-09-27 12:31:53 +03:00
logger.info(f"🔧 Code verifier length: {len(code_verifier) if code_verifier else 0}")
2025-09-28 20:04:52 +03:00
logger.info(f"🔧 Callback URI: {callback_uri}")
2025-09-28 20:45:08 +03:00
# Получаем token endpoint для провайдера
token_endpoints = {
"google": "https://oauth2.googleapis.com/token",
"github": "https://github.com/login/oauth/access_token",
}
2025-09-28 20:52:17 +03:00
2025-09-28 20:45:08 +03:00
token_endpoint = token_endpoints.get(provider)
if not token_endpoint:
logger.error(f"❌ Unknown token endpoint for provider: {provider}")
return JSONResponse({"error": f"Unknown provider: {provider}"}, status_code=400)
2025-09-28 20:34:26 +03:00
2025-09-28 20:45:08 +03:00
# Используем внутренний HTTP клиент для прямого запроса к token endpoint
2025-09-28 20:34:26 +03:00
token_data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": callback_uri,
"client_id": client.client_id,
"code_verifier": code_verifier,
}
2025-09-28 20:53:42 +03:00
# Google требует client_secret даже при использовании PKCE
if hasattr(client, "client_secret") and client.client_secret:
token_data["client_secret"] = client.client_secret
2025-09-28 20:34:26 +03:00
async with httpx.AsyncClient() as http_client:
0.9.29] - 2025-10-08 ### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS - **🚀 +175% Recall**: Интегрирован ColBERT через pylate с НАТИВНЫМ MUVERA multi-vector retrieval - **🎯 TRUE MaxSim**: Настоящий token-level MaxSim scoring, а не упрощенный max pooling - **🗜️ Native Multi-Vector FDE**: Каждый токен encode_fde отдельно → список FDE векторов - **🚀 FAISS Acceleration**: Двухэтапный поиск O(log N) для масштабирования >10K документов - **🎯 Dual Architecture**: Поддержка BiEncoder (быстрый) и ColBERT (качественный) через `SEARCH_MODEL_TYPE` - **⚡ Faster Indexing**: ColBERT индексация ~12s vs BiEncoder ~26s на бенчмарке - **📊 Better Results**: Recall@10 улучшен с 0.16 до 0.44 (+175%) ### 🛠️ Technical Changes - **requirements.txt**: Добавлены `pylate>=1.0.0` и `faiss-cpu>=1.7.4` - **services/search.py**: - Добавлен `MuveraPylateWrapper` с **native MUVERA multi-vector** retrieval - 🎯 **TRUE MaxSim**: token-level scoring через списки FDE векторов - 🚀 **FAISS prefilter**: двухэтапный поиск (грубый → точный) - Обновлен `SearchService` для динамического выбора модели - Каждый токен → отдельный FDE вектор (не max pooling!) - **settings.py**: - `SEARCH_MODEL_TYPE` - выбор модели (default: "colbert") - `SEARCH_USE_FAISS` - включить FAISS (default: true) - `SEARCH_FAISS_CANDIDATES` - количество кандидатов (default: 1000) ### 📚 Documentation - **docs/search-system.md**: Полностью обновлена документация - Сравнение BiEncoder vs ColBERT с бенчмарками - 🚀 **Секция про FAISS**: когда включать, архитектура, производительность - Руководство по выбору модели для разных сценариев - 🎯 **Детальное описание native MUVERA multi-vector**: каждый токен → FDE - TRUE MaxSim scoring алгоритм с примерами кода - Двухэтапный поиск: FAISS prefilter → MaxSim rerank - 🤖 Предупреждение о проблеме дистилляционных моделей (pylate#142) ### ⚙️ Configuration ```bash # Включить ColBERT (рекомендуется для production) SEARCH_MODEL_TYPE=colbert # 🚀 FAISS acceleration (обязательно для >10K документов) SEARCH_USE_FAISS=true # default: true SEARCH_FAISS_CANDIDATES=1000 # default: 1000 # Fallback к BiEncoder (быстрее, но -62% recall) SEARCH_MODEL_TYPE=biencoder ``` ### 🎯 Impact - ✅ **Качество поиска**: +175% recall на бенчмарке NanoFiQA2018 - ✅ **TRUE ColBERT**: Native multi-vector без упрощений (max pooling) - ✅ **MUVERA правильно**: Используется по назначению для multi-vector retrieval - ✅ **Масштабируемость**: FAISS prefilter → O(log N) вместо O(N) - ✅ **Готовность к росту**: Архитектура выдержит >50K документов - ✅ **Индексация**: Быстрее на ~54% (12s vs 26s) - ⚠️ **Latency**: С FAISS остается приемлемой даже на больших индексах - ✅ **Backward Compatible**: BiEncoder + отключение FAISS через env ### 🔗 References - GitHub PR: https://github.com/sionic-ai/muvera-py/pull/1 - pylate issue: https://github.com/lightonai/pylate/issues/142 - Model: `answerdotai/answerai-colbert-small-v1`
2025-10-09 01:15:19 +03:00
token_response = await http_client.post(
2025-09-28 20:45:08 +03:00
token_endpoint, data=token_data, headers={"Accept": "application/json"}
2025-09-28 20:34:26 +03:00
)
0.9.29] - 2025-10-08 ### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS - **🚀 +175% Recall**: Интегрирован ColBERT через pylate с НАТИВНЫМ MUVERA multi-vector retrieval - **🎯 TRUE MaxSim**: Настоящий token-level MaxSim scoring, а не упрощенный max pooling - **🗜️ Native Multi-Vector FDE**: Каждый токен encode_fde отдельно → список FDE векторов - **🚀 FAISS Acceleration**: Двухэтапный поиск O(log N) для масштабирования >10K документов - **🎯 Dual Architecture**: Поддержка BiEncoder (быстрый) и ColBERT (качественный) через `SEARCH_MODEL_TYPE` - **⚡ Faster Indexing**: ColBERT индексация ~12s vs BiEncoder ~26s на бенчмарке - **📊 Better Results**: Recall@10 улучшен с 0.16 до 0.44 (+175%) ### 🛠️ Technical Changes - **requirements.txt**: Добавлены `pylate>=1.0.0` и `faiss-cpu>=1.7.4` - **services/search.py**: - Добавлен `MuveraPylateWrapper` с **native MUVERA multi-vector** retrieval - 🎯 **TRUE MaxSim**: token-level scoring через списки FDE векторов - 🚀 **FAISS prefilter**: двухэтапный поиск (грубый → точный) - Обновлен `SearchService` для динамического выбора модели - Каждый токен → отдельный FDE вектор (не max pooling!) - **settings.py**: - `SEARCH_MODEL_TYPE` - выбор модели (default: "colbert") - `SEARCH_USE_FAISS` - включить FAISS (default: true) - `SEARCH_FAISS_CANDIDATES` - количество кандидатов (default: 1000) ### 📚 Documentation - **docs/search-system.md**: Полностью обновлена документация - Сравнение BiEncoder vs ColBERT с бенчмарками - 🚀 **Секция про FAISS**: когда включать, архитектура, производительность - Руководство по выбору модели для разных сценариев - 🎯 **Детальное описание native MUVERA multi-vector**: каждый токен → FDE - TRUE MaxSim scoring алгоритм с примерами кода - Двухэтапный поиск: FAISS prefilter → MaxSim rerank - 🤖 Предупреждение о проблеме дистилляционных моделей (pylate#142) ### ⚙️ Configuration ```bash # Включить ColBERT (рекомендуется для production) SEARCH_MODEL_TYPE=colbert # 🚀 FAISS acceleration (обязательно для >10K документов) SEARCH_USE_FAISS=true # default: true SEARCH_FAISS_CANDIDATES=1000 # default: 1000 # Fallback к BiEncoder (быстрее, но -62% recall) SEARCH_MODEL_TYPE=biencoder ``` ### 🎯 Impact - ✅ **Качество поиска**: +175% recall на бенчмарке NanoFiQA2018 - ✅ **TRUE ColBERT**: Native multi-vector без упрощений (max pooling) - ✅ **MUVERA правильно**: Используется по назначению для multi-vector retrieval - ✅ **Масштабируемость**: FAISS prefilter → O(log N) вместо O(N) - ✅ **Готовность к росту**: Архитектура выдержит >50K документов - ✅ **Индексация**: Быстрее на ~54% (12s vs 26s) - ⚠️ **Latency**: С FAISS остается приемлемой даже на больших индексах - ✅ **Backward Compatible**: BiEncoder + отключение FAISS через env ### 🔗 References - GitHub PR: https://github.com/sionic-ai/muvera-py/pull/1 - pylate issue: https://github.com/lightonai/pylate/issues/142 - Model: `answerdotai/answerai-colbert-small-v1`
2025-10-09 01:15:19 +03:00
if token_response.status_code != 200:
error_msg = f"Token request failed: {token_response.status_code} - {token_response.text}"
2025-09-28 20:34:26 +03:00
logger.error(f"{error_msg}")
raise ValueError(error_msg)
0.9.29] - 2025-10-08 ### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS - **🚀 +175% Recall**: Интегрирован ColBERT через pylate с НАТИВНЫМ MUVERA multi-vector retrieval - **🎯 TRUE MaxSim**: Настоящий token-level MaxSim scoring, а не упрощенный max pooling - **🗜️ Native Multi-Vector FDE**: Каждый токен encode_fde отдельно → список FDE векторов - **🚀 FAISS Acceleration**: Двухэтапный поиск O(log N) для масштабирования >10K документов - **🎯 Dual Architecture**: Поддержка BiEncoder (быстрый) и ColBERT (качественный) через `SEARCH_MODEL_TYPE` - **⚡ Faster Indexing**: ColBERT индексация ~12s vs BiEncoder ~26s на бенчмарке - **📊 Better Results**: Recall@10 улучшен с 0.16 до 0.44 (+175%) ### 🛠️ Technical Changes - **requirements.txt**: Добавлены `pylate>=1.0.0` и `faiss-cpu>=1.7.4` - **services/search.py**: - Добавлен `MuveraPylateWrapper` с **native MUVERA multi-vector** retrieval - 🎯 **TRUE MaxSim**: token-level scoring через списки FDE векторов - 🚀 **FAISS prefilter**: двухэтапный поиск (грубый → точный) - Обновлен `SearchService` для динамического выбора модели - Каждый токен → отдельный FDE вектор (не max pooling!) - **settings.py**: - `SEARCH_MODEL_TYPE` - выбор модели (default: "colbert") - `SEARCH_USE_FAISS` - включить FAISS (default: true) - `SEARCH_FAISS_CANDIDATES` - количество кандидатов (default: 1000) ### 📚 Documentation - **docs/search-system.md**: Полностью обновлена документация - Сравнение BiEncoder vs ColBERT с бенчмарками - 🚀 **Секция про FAISS**: когда включать, архитектура, производительность - Руководство по выбору модели для разных сценариев - 🎯 **Детальное описание native MUVERA multi-vector**: каждый токен → FDE - TRUE MaxSim scoring алгоритм с примерами кода - Двухэтапный поиск: FAISS prefilter → MaxSim rerank - 🤖 Предупреждение о проблеме дистилляционных моделей (pylate#142) ### ⚙️ Configuration ```bash # Включить ColBERT (рекомендуется для production) SEARCH_MODEL_TYPE=colbert # 🚀 FAISS acceleration (обязательно для >10K документов) SEARCH_USE_FAISS=true # default: true SEARCH_FAISS_CANDIDATES=1000 # default: 1000 # Fallback к BiEncoder (быстрее, но -62% recall) SEARCH_MODEL_TYPE=biencoder ``` ### 🎯 Impact - ✅ **Качество поиска**: +175% recall на бенчмарке NanoFiQA2018 - ✅ **TRUE ColBERT**: Native multi-vector без упрощений (max pooling) - ✅ **MUVERA правильно**: Используется по назначению для multi-vector retrieval - ✅ **Масштабируемость**: FAISS prefilter → O(log N) вместо O(N) - ✅ **Готовность к росту**: Архитектура выдержит >50K документов - ✅ **Индексация**: Быстрее на ~54% (12s vs 26s) - ⚠️ **Latency**: С FAISS остается приемлемой даже на больших индексах - ✅ **Backward Compatible**: BiEncoder + отключение FAISS через env ### 🔗 References - GitHub PR: https://github.com/sionic-ai/muvera-py/pull/1 - pylate issue: https://github.com/lightonai/pylate/issues/142 - Model: `answerdotai/answerai-colbert-small-v1`
2025-10-09 01:15:19 +03:00
token = token_response.json()
except Exception as e:
logger.error(f"❌ Failed to fetch access token for {provider}: {e}", exc_info=True)
2025-09-27 12:31:53 +03:00
logger.error(f"❌ Request URL: {request.url}")
logger.error(f"❌ OAuth data: {oauth_data}")
raise # Re-raise для обработки в основном except блоке
2025-09-23 18:54:56 +03:00
if not token:
2025-09-24 23:11:01 +03:00
logger.error(f"❌ Failed to get access token for {provider}")
2025-09-23 18:54:56 +03:00
return JSONResponse({"error": "Failed to get access token"}, status_code=400)
2025-09-24 23:11:01 +03:00
logger.info(f"✅ Got access token for {provider}: {bool(token)}")
# 🔄 Step 2: Getting user profile
logger.info(f"🔄 Step 2: Getting user profile from {provider}...")
try:
profile = await get_user_profile(provider, client, token)
except Exception as e:
logger.error(f"❌ Exception while getting user profile for {provider}: {e}", exc_info=True)
raise # Re-raise для обработки в основном except блоке
if not profile:
logger.error(f"❌ Failed to get user profile for {provider} - empty profile returned")
return JSONResponse({"error": "Failed to get user profile"}, status_code=400)
[0.9.25] - 2025-01-25 ### Added - 🔍 **OAuth Detailed Logging**: Добавлено пошаговое логирование OAuth callback для диагностики ошибок `auth_failed` - 🧪 **OAuth Diagnostic Tools**: Создан `oauth_debug.py` для анализа OAuth callback параметров и диагностики проблем - 📊 **OAuth Test Helper**: Добавлен `oauth_test_helper.py` для создания тестовых состояний OAuth в Redis - 🔧 **OAuth Provider Detection**: Автоматическое определение OAuth провайдера по формату authorization code ### Fixed - 🚨 **OAuth Callback Error Handling**: Улучшена обработка исключений в OAuth callback с детальным логированием каждого шага - 🔍 **OAuth Exception Tracking**: Добавлено логирование исключений на каждом этапе: token exchange, profile fetch, user creation, session creation - 📋 **OAuth Error Diagnosis**: Реализована система диагностики для выявления точной причины `error=auth_failed` редиректов ### Changed - 🔧 **OAuth Callback Flow**: Разделен OAuth callback на логические шаги с индивидуальным error handling - 📝 **OAuth Error Messages**: Улучшены сообщения об ошибках для более точной диагностики проблем
2025-09-25 08:48:36 +03:00
logger.info(
f"✅ Got user profile for {provider}: id={profile.get('id')}, email={profile.get('email')}, name={profile.get('name')}"
)
2025-09-24 23:11:01 +03:00
# 🔄 Step 3: Creating or updating user
logger.info(f"🔄 Step 3: Creating or updating user for {provider}...")
try:
author = await _create_or_update_user(provider, profile)
2025-09-29 13:59:49 +03:00
logger.info("✅ Step 3 completed: User created/updated successfully")
except Exception as e:
logger.error(f"❌ Exception while creating/updating user for {provider}: {e}", exc_info=True)
raise # Re-raise для обработки в основном except блоке
2025-09-23 18:54:56 +03:00
if not author:
logger.error(f"❌ Failed to create/update user for {provider} - no author returned")
2025-09-23 18:54:56 +03:00
return JSONResponse({"error": "Failed to create/update user"}, status_code=500)
logger.info(f"✅ User created/updated for {provider}: user_id={author.id}, email={author.email}")
# 🔄 Step 4: Creating session token
logger.info(f"🔄 Step 4: Creating session token for user {author.id}...")
try:
session_token = await TokenStorage.create_session(
str(author.id),
auth_data={
"provider": provider,
"profile": profile,
},
username=author.name
if isinstance(author.name, str)
else str(author.name)
if author.name is not None
else None,
device_info={
"user_agent": request.headers.get("user-agent"),
"ip": request.client.host if hasattr(request, "client") and request.client else None,
},
)
2025-09-29 13:59:49 +03:00
logger.info("✅ Step 4 completed: Session token created successfully")
except Exception as e:
logger.error(f"❌ Exception while creating session token for {provider}: {e}", exc_info=True)
raise # Re-raise для обработки в основном except блоке
2025-09-27 13:08:57 +03:00
if not session_token:
logger.error(f"❌ Session token is empty for {provider}")
raise ValueError("Session token creation failed")
logger.info(f"✅ Session token created for {provider}: token_length={len(session_token)}")
2025-09-27 13:51:15 +03:00
logger.info(
f"🔧 Session token preview: {session_token[:20]}..."
if len(session_token) > 20
else f"🔧 Session token: {session_token}"
)
2025-09-29 16:33:49 +03:00
# 🔑 Получаем redirect_uri из state данных (новый подход)
state_data = oauth_data.get("state_data", {})
redirect_uri = state_data.get("redirect_uri") or oauth_data.get("redirect_uri", FRONTEND_URL)
2025-09-23 18:54:56 +03:00
if not isinstance(redirect_uri, str) or not redirect_uri:
2025-09-29 16:33:49 +03:00
redirect_uri = FRONTEND_URL.rstrip("/") + "/settings"
logger.info(f"🔑 Using redirect_uri from state: {redirect_uri}")
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
# 🎯 Стандартный OAuth flow: токен в URL для фронтенда
2025-09-29 08:53:39 +03:00
from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse
# 🔧 Декодируем redirect_uri если он URL-encoded
if "%3A" in redirect_uri or "%2F" in redirect_uri:
redirect_uri = unquote(redirect_uri)
logger.info(f"🔧 Decoded redirect_uri: {redirect_uri}")
2025-09-24 19:30:06 +03:00
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
parsed_url = urlparse(redirect_uri)
[0.9.29] - 2025-09-26 ### 🚨 CRITICAL Security Fixes - **🔒 Open Redirect Protection**: Добавлена строгая валидация redirect_uri против whitelist доменов - **🔒 Rate Limiting**: Защита OAuth endpoints от брутфорса (10 попыток за 5 минут на IP) - **🔒 Logout Endpoint**: Критически важный endpoint для безопасного отзыва httpOnly cookies - **🔒 Provider Validation**: Усиленная валидация OAuth провайдеров с логированием атак - **🚨 GlitchTip Alerts**: Автоматические алерты безопасности в GlitchTip при критических событиях ### 🛡️ Security Modules - **auth/oauth_security.py**: Модуль безопасности OAuth с валидацией и rate limiting + GlitchTip алерты - **auth/logout.py**: Безопасный logout с поддержкой JSON API и browser redirect - **tests/test_oauth_security.py**: Комплексные тесты безопасности (11 тестов) - **tests/test_oauth_glitchtip_alerts.py**: Тесты интеграции с GlitchTip (8 тестов) ### 🔧 OAuth Improvements - **Minimal Flow**: Упрощен до минимума - только httpOnly cookie, нет JWT в URL - **Simple Logic**: Нет error параметра = успех, максимальная простота - **DRY Refactoring**: Устранено дублирование кода в logout и валидации ### 🎯 OAuth Endpoints - **Старт**: `v3.dscrs.site/oauth/{provider}` - с rate limiting и валидацией - **Callback**: `v3.dscrs.site/oauth/{provider}/callback` - безопасный redirect_uri - **Logout**: `v3.dscrs.site/auth/logout` - отзыв httpOnly cookies - **Финализация**: `testing.discours.io/oauth?redirect_url=...` - минимальная схема ### 📊 Security Test Coverage - ✅ Open redirect attack prevention - ✅ Rate limiting protection - ✅ Provider validation - ✅ Safe fallback mechanisms - ✅ Cookie security (httpOnly + Secure + SameSite) - ✅ GlitchTip integration (8 тестов алертов) ### 📝 Documentation - Создан `docs/oauth-minimal-flow.md` - полное описание минимального flow - Обновлена документация OAuth в `docs/auth/oauth.md` - Добавлены security best practices
2025-09-26 21:03:45 +03:00
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
# 🌐 OAuth: токен в URL (стандартный подход)
logger.info("🌐 OAuth: using token in URL")
query_params = parse_qs(parsed_url.query)
query_params["access_token"] = [session_token]
if state:
[0.9.29] - 2025-09-26 ### 🚨 CRITICAL Security Fixes - **🔒 Open Redirect Protection**: Добавлена строгая валидация redirect_uri против whitelist доменов - **🔒 Rate Limiting**: Защита OAuth endpoints от брутфорса (10 попыток за 5 минут на IP) - **🔒 Logout Endpoint**: Критически важный endpoint для безопасного отзыва httpOnly cookies - **🔒 Provider Validation**: Усиленная валидация OAuth провайдеров с логированием атак - **🚨 GlitchTip Alerts**: Автоматические алерты безопасности в GlitchTip при критических событиях ### 🛡️ Security Modules - **auth/oauth_security.py**: Модуль безопасности OAuth с валидацией и rate limiting + GlitchTip алерты - **auth/logout.py**: Безопасный logout с поддержкой JSON API и browser redirect - **tests/test_oauth_security.py**: Комплексные тесты безопасности (11 тестов) - **tests/test_oauth_glitchtip_alerts.py**: Тесты интеграции с GlitchTip (8 тестов) ### 🔧 OAuth Improvements - **Minimal Flow**: Упрощен до минимума - только httpOnly cookie, нет JWT в URL - **Simple Logic**: Нет error параметра = успех, максимальная простота - **DRY Refactoring**: Устранено дублирование кода в logout и валидации ### 🎯 OAuth Endpoints - **Старт**: `v3.dscrs.site/oauth/{provider}` - с rate limiting и валидацией - **Callback**: `v3.dscrs.site/oauth/{provider}/callback` - безопасный redirect_uri - **Logout**: `v3.dscrs.site/auth/logout` - отзыв httpOnly cookies - **Финализация**: `testing.discours.io/oauth?redirect_url=...` - минимальная схема ### 📊 Security Test Coverage - ✅ Open redirect attack prevention - ✅ Rate limiting protection - ✅ Provider validation - ✅ Safe fallback mechanisms - ✅ Cookie security (httpOnly + Secure + SameSite) - ✅ GlitchTip integration (8 тестов алертов) ### 📝 Documentation - Создан `docs/oauth-minimal-flow.md` - полное описание минимального flow - Обновлена документация OAuth в `docs/auth/oauth.md` - Добавлены security best practices
2025-09-26 21:03:45 +03:00
query_params["state"] = [state]
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
new_query = urlencode(query_params, doseq=True)
final_redirect_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
new_query,
parsed_url.fragment,
[0.9.29] - 2025-09-26 ### 🚨 CRITICAL Security Fixes - **🔒 Open Redirect Protection**: Добавлена строгая валидация redirect_uri против whitelist доменов - **🔒 Rate Limiting**: Защита OAuth endpoints от брутфорса (10 попыток за 5 минут на IP) - **🔒 Logout Endpoint**: Критически важный endpoint для безопасного отзыва httpOnly cookies - **🔒 Provider Validation**: Усиленная валидация OAuth провайдеров с логированием атак - **🚨 GlitchTip Alerts**: Автоматические алерты безопасности в GlitchTip при критических событиях ### 🛡️ Security Modules - **auth/oauth_security.py**: Модуль безопасности OAuth с валидацией и rate limiting + GlitchTip алерты - **auth/logout.py**: Безопасный logout с поддержкой JSON API и browser redirect - **tests/test_oauth_security.py**: Комплексные тесты безопасности (11 тестов) - **tests/test_oauth_glitchtip_alerts.py**: Тесты интеграции с GlitchTip (8 тестов) ### 🔧 OAuth Improvements - **Minimal Flow**: Упрощен до минимума - только httpOnly cookie, нет JWT в URL - **Simple Logic**: Нет error параметра = успех, максимальная простота - **DRY Refactoring**: Устранено дублирование кода в logout и валидации ### 🎯 OAuth Endpoints - **Старт**: `v3.dscrs.site/oauth/{provider}` - с rate limiting и валидацией - **Callback**: `v3.dscrs.site/oauth/{provider}/callback` - безопасный redirect_uri - **Logout**: `v3.dscrs.site/auth/logout` - отзыв httpOnly cookies - **Финализация**: `testing.discours.io/oauth?redirect_url=...` - минимальная схема ### 📊 Security Test Coverage - ✅ Open redirect attack prevention - ✅ Rate limiting protection - ✅ Provider validation - ✅ Safe fallback mechanisms - ✅ Cookie security (httpOnly + Secure + SameSite) - ✅ GlitchTip integration (8 тестов алертов) ### 📝 Documentation - Создан `docs/oauth-minimal-flow.md` - полное описание минимального flow - Обновлена документация OAuth в `docs/auth/oauth.md` - Добавлены security best practices
2025-09-26 21:03:45 +03:00
)
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
)
2025-09-24 19:30:06 +03:00
logger.info(f"🔗 OAuth redirect URL: {final_redirect_url}")
# 🔍 Дополнительная диагностика для отладки
logger.info("🎯 OAuth callback redirect details:")
logger.info(f" - Original redirect_uri: {oauth_data.get('redirect_uri')}")
logger.info(f" - Final redirect_uri: {redirect_uri}")
logger.info(f" - Session token length: {len(session_token)}")
logger.info(f" - State: {state}")
logger.info(f" - Provider: {provider}")
logger.info(f" - User ID: {author.id}")
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
# 🔗 Редиректим с токеном в URL
2025-09-29 13:59:49 +03:00
logger.info("🔄 Step 5: Creating redirect response...")
0.9.29] - 2025-10-08 ### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS - **🚀 +175% Recall**: Интегрирован ColBERT через pylate с НАТИВНЫМ MUVERA multi-vector retrieval - **🎯 TRUE MaxSim**: Настоящий token-level MaxSim scoring, а не упрощенный max pooling - **🗜️ Native Multi-Vector FDE**: Каждый токен encode_fde отдельно → список FDE векторов - **🚀 FAISS Acceleration**: Двухэтапный поиск O(log N) для масштабирования >10K документов - **🎯 Dual Architecture**: Поддержка BiEncoder (быстрый) и ColBERT (качественный) через `SEARCH_MODEL_TYPE` - **⚡ Faster Indexing**: ColBERT индексация ~12s vs BiEncoder ~26s на бенчмарке - **📊 Better Results**: Recall@10 улучшен с 0.16 до 0.44 (+175%) ### 🛠️ Technical Changes - **requirements.txt**: Добавлены `pylate>=1.0.0` и `faiss-cpu>=1.7.4` - **services/search.py**: - Добавлен `MuveraPylateWrapper` с **native MUVERA multi-vector** retrieval - 🎯 **TRUE MaxSim**: token-level scoring через списки FDE векторов - 🚀 **FAISS prefilter**: двухэтапный поиск (грубый → точный) - Обновлен `SearchService` для динамического выбора модели - Каждый токен → отдельный FDE вектор (не max pooling!) - **settings.py**: - `SEARCH_MODEL_TYPE` - выбор модели (default: "colbert") - `SEARCH_USE_FAISS` - включить FAISS (default: true) - `SEARCH_FAISS_CANDIDATES` - количество кандидатов (default: 1000) ### 📚 Documentation - **docs/search-system.md**: Полностью обновлена документация - Сравнение BiEncoder vs ColBERT с бенчмарками - 🚀 **Секция про FAISS**: когда включать, архитектура, производительность - Руководство по выбору модели для разных сценариев - 🎯 **Детальное описание native MUVERA multi-vector**: каждый токен → FDE - TRUE MaxSim scoring алгоритм с примерами кода - Двухэтапный поиск: FAISS prefilter → MaxSim rerank - 🤖 Предупреждение о проблеме дистилляционных моделей (pylate#142) ### ⚙️ Configuration ```bash # Включить ColBERT (рекомендуется для production) SEARCH_MODEL_TYPE=colbert # 🚀 FAISS acceleration (обязательно для >10K документов) SEARCH_USE_FAISS=true # default: true SEARCH_FAISS_CANDIDATES=1000 # default: 1000 # Fallback к BiEncoder (быстрее, но -62% recall) SEARCH_MODEL_TYPE=biencoder ``` ### 🎯 Impact - ✅ **Качество поиска**: +175% recall на бенчмарке NanoFiQA2018 - ✅ **TRUE ColBERT**: Native multi-vector без упрощений (max pooling) - ✅ **MUVERA правильно**: Используется по назначению для multi-vector retrieval - ✅ **Масштабируемость**: FAISS prefilter → O(log N) вместо O(N) - ✅ **Готовность к росту**: Архитектура выдержит >50K документов - ✅ **Индексация**: Быстрее на ~54% (12s vs 26s) - ⚠️ **Latency**: С FAISS остается приемлемой даже на больших индексах - ✅ **Backward Compatible**: BiEncoder + отключение FAISS через env ### 🔗 References - GitHub PR: https://github.com/sionic-ai/muvera-py/pull/1 - pylate issue: https://github.com/lightonai/pylate/issues/142 - Model: `answerdotai/answerai-colbert-small-v1`
2025-10-09 01:15:19 +03:00
redirect_response = RedirectResponse(url=final_redirect_url, status_code=307)
2025-09-23 18:54:56 +03:00
[0.9.28] - 2025-09-28 ### 🍪 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
2025-09-28 13:06:03 +03:00
logger.info(f"✅ OAuth: токен передан в URL для user_id={author.id}")
logger.info(f"🔗 Final redirect URL: {final_redirect_url}")
2025-09-27 20:25:30 +03:00
2025-09-29 12:51:04 +03:00
# 🔍 Дополнительная диагностика редиректа
logger.info("🔍 RedirectResponse details:")
logger.info(" - Status code: 307")
logger.info(f" - Location header: {final_redirect_url}")
logger.info(f" - URL length: {len(final_redirect_url)}")
logger.info(f" - Contains token: {'access_token=' in final_redirect_url}")
2025-09-29 13:59:49 +03:00
logger.info("✅ Step 5 completed: Redirect response created successfully")
2025-09-27 13:08:57 +03:00
logger.info(f"✅ OAuth успешно завершен для {provider}, user_id={author.id}")
2025-09-29 13:59:49 +03:00
logger.info("🔄 Returning redirect response to client...")
0.9.29] - 2025-10-08 ### 🎯 Search Quality Upgrade: ColBERT + Native MUVERA + FAISS - **🚀 +175% Recall**: Интегрирован ColBERT через pylate с НАТИВНЫМ MUVERA multi-vector retrieval - **🎯 TRUE MaxSim**: Настоящий token-level MaxSim scoring, а не упрощенный max pooling - **🗜️ Native Multi-Vector FDE**: Каждый токен encode_fde отдельно → список FDE векторов - **🚀 FAISS Acceleration**: Двухэтапный поиск O(log N) для масштабирования >10K документов - **🎯 Dual Architecture**: Поддержка BiEncoder (быстрый) и ColBERT (качественный) через `SEARCH_MODEL_TYPE` - **⚡ Faster Indexing**: ColBERT индексация ~12s vs BiEncoder ~26s на бенчмарке - **📊 Better Results**: Recall@10 улучшен с 0.16 до 0.44 (+175%) ### 🛠️ Technical Changes - **requirements.txt**: Добавлены `pylate>=1.0.0` и `faiss-cpu>=1.7.4` - **services/search.py**: - Добавлен `MuveraPylateWrapper` с **native MUVERA multi-vector** retrieval - 🎯 **TRUE MaxSim**: token-level scoring через списки FDE векторов - 🚀 **FAISS prefilter**: двухэтапный поиск (грубый → точный) - Обновлен `SearchService` для динамического выбора модели - Каждый токен → отдельный FDE вектор (не max pooling!) - **settings.py**: - `SEARCH_MODEL_TYPE` - выбор модели (default: "colbert") - `SEARCH_USE_FAISS` - включить FAISS (default: true) - `SEARCH_FAISS_CANDIDATES` - количество кандидатов (default: 1000) ### 📚 Documentation - **docs/search-system.md**: Полностью обновлена документация - Сравнение BiEncoder vs ColBERT с бенчмарками - 🚀 **Секция про FAISS**: когда включать, архитектура, производительность - Руководство по выбору модели для разных сценариев - 🎯 **Детальное описание native MUVERA multi-vector**: каждый токен → FDE - TRUE MaxSim scoring алгоритм с примерами кода - Двухэтапный поиск: FAISS prefilter → MaxSim rerank - 🤖 Предупреждение о проблеме дистилляционных моделей (pylate#142) ### ⚙️ Configuration ```bash # Включить ColBERT (рекомендуется для production) SEARCH_MODEL_TYPE=colbert # 🚀 FAISS acceleration (обязательно для >10K документов) SEARCH_USE_FAISS=true # default: true SEARCH_FAISS_CANDIDATES=1000 # default: 1000 # Fallback к BiEncoder (быстрее, но -62% recall) SEARCH_MODEL_TYPE=biencoder ``` ### 🎯 Impact - ✅ **Качество поиска**: +175% recall на бенчмарке NanoFiQA2018 - ✅ **TRUE ColBERT**: Native multi-vector без упрощений (max pooling) - ✅ **MUVERA правильно**: Используется по назначению для multi-vector retrieval - ✅ **Масштабируемость**: FAISS prefilter → O(log N) вместо O(N) - ✅ **Готовность к росту**: Архитектура выдержит >50K документов - ✅ **Индексация**: Быстрее на ~54% (12s vs 26s) - ⚠️ **Latency**: С FAISS остается приемлемой даже на больших индексах - ✅ **Backward Compatible**: BiEncoder + отключение FAISS через env ### 🔗 References - GitHub PR: https://github.com/sionic-ai/muvera-py/pull/1 - pylate issue: https://github.com/lightonai/pylate/issues/142 - Model: `answerdotai/answerai-colbert-small-v1`
2025-10-09 01:15:19 +03:00
return redirect_response
except Exception as e:
2025-09-24 23:11:01 +03:00
logger.error(f"OAuth callback error for {provider}: {e!s}", exc_info=True)
logger.error(f"OAuth callback request URL: {request.url}")
logger.error(f"OAuth callback query params: {dict(request.query_params)}")
2025-09-27 12:31:53 +03:00
2025-09-23 18:54:56 +03:00
# В случае ошибки редиректим на фронтенд с ошибкой
2025-09-27 12:31:53 +03:00
# Используем сохраненный redirect_uri из OAuth state или fallback
try:
state = request.query_params.get("state")
oauth_data = await get_oauth_state(state) if state else None
fallback_redirect = oauth_data.get("redirect_uri") if oauth_data else FRONTEND_URL
except Exception:
fallback_redirect = FRONTEND_URL
# Обеспечиваем что fallback_redirect это строка
if not isinstance(fallback_redirect, str):
fallback_redirect = FRONTEND_URL
2025-09-29 16:33:49 +03:00
# Для testing.discours.io используем страницу профиля для ошибок
2025-09-27 12:31:53 +03:00
if "testing.discours.io" in fallback_redirect:
from urllib.parse import quote
2025-09-29 16:33:49 +03:00
error_url = f"https://testing.discours.io/settings?error=auth_failed&provider={provider}&redirect_url={quote(fallback_redirect)}"
2025-09-27 12:31:53 +03:00
else:
error_url = f"{fallback_redirect}?error=auth_failed&provider={provider}"
logger.error(f"🚨 Redirecting to error URL: {error_url}")
return RedirectResponse(url=error_url)
2025-06-02 21:50:58 +03:00
async def _create_or_update_user(provider: str, profile: dict) -> Author:
"""
Создает или обновляет пользователя на основе OAuth профиля.
Возвращает объект Author.
"""
# Для некоторых провайдеров (X, Telegram) email может отсутствовать
email = profile.get("email")
if not email:
# Генерируем временный email на основе провайдера и ID
email = _generate_temp_email(provider, profile.get("id", "unknown"))
logger.info(f"Generated temporary email for {provider} user: {email}")
# Создаем или обновляем пользователя
session = get_session()
try:
# Сначала ищем пользователя по OAuth
author = Author.find_by_oauth(provider, profile["id"], session)
if author:
# Пользователь найден по OAuth - обновляем данные
author.set_oauth_account(provider, profile["id"], email=profile.get("email"))
_update_author_profile(author, profile)
else:
# Ищем пользователя по email если есть настоящий email
author = None
if email and not email.endswith(TEMP_EMAIL_SUFFIX):
2025-07-31 18:55:59 +03:00
author = session.query(Author).where(Author.email == email).first()
2025-06-02 21:50:58 +03:00
if author:
# Пользователь найден по email - добавляем OAuth данные
author.set_oauth_account(provider, profile["id"], email=profile.get("email"))
_update_author_profile(author, profile)
else:
# Создаем нового пользователя
author = _create_new_oauth_user(provider, profile, email, session)
session.commit()
return author
finally:
session.close()
def _update_author_profile(author: Author, profile: dict) -> None:
"""Обновляет профиль автора данными из OAuth"""
if profile.get("name") and not author.name:
author.name = profile["name"] # type: ignore[assignment]
if profile.get("picture") and not author.pic:
author.pic = profile["picture"] # type: ignore[assignment]
author.updated_at = int(time.time()) # type: ignore[assignment]
author.last_seen = int(time.time()) # type: ignore[assignment]
def _create_new_oauth_user(provider: str, profile: dict, email: str, session: Any) -> Author:
"""Создает нового пользователя из OAuth профиля"""
slug = generate_unique_slug(profile["name"] or f"{provider}_{profile.get('id', 'user')}")
author = Author(
email=email,
name=profile["name"] or f"{provider.title()} User",
slug=slug,
pic=profile.get("picture"),
email_verified=bool(profile.get("email")),
created_at=int(time.time()),
updated_at=int(time.time()),
last_seen=int(time.time()),
)
session.add(author)
session.flush() # Получаем ID автора
# Добавляем OAuth данные для нового пользователя
author.set_oauth_account(provider, profile["id"], email=profile.get("email"))
2025-07-02 22:30:21 +03:00
# Добавляем пользователя в основное сообщество с дефолтными ролями
target_community_id = 1 # Основное сообщество
# Получаем сообщество для назначения дефолтных ролей
2025-07-31 18:55:59 +03:00
community = session.query(Community).where(Community.id == target_community_id).first()
2025-07-02 22:30:21 +03:00
if community:
2025-07-25 01:04:15 +03:00
default_roles = community.get_default_roles()
2025-07-02 22:30:21 +03:00
2025-07-31 18:55:59 +03:00
# Проверяем, не существует ли уже запись CommunityAuthor
existing_ca = (
session.query(CommunityAuthor).filter_by(community_id=target_community_id, author_id=author.id).first()
)
if not existing_ca:
# Создаем CommunityAuthor с дефолтными ролями
community_author = CommunityAuthor(
community_id=target_community_id, author_id=author.id, roles=",".join(default_roles)
)
session.add(community_author)
logger.info(f"Создана запись CommunityAuthor для OAuth пользователя {author.id} с ролями: {default_roles}")
# Проверяем, не существует ли уже запись подписчика
existing_follower = (
session.query(CommunityFollower).filter_by(community=target_community_id, follower=int(author.id)).first()
2025-07-02 22:30:21 +03:00
)
2025-07-31 18:55:59 +03:00
if not existing_follower:
# Добавляем пользователя в подписчики сообщества
follower = CommunityFollower(community=target_community_id, follower=int(author.id))
session.add(follower)
logger.info(f"OAuth пользователь {author.id} добавлен в подписчики сообщества {target_community_id}")
2025-07-02 22:30:21 +03:00
2025-06-02 21:50:58 +03:00
return author