All checks were successful
Deploy on push / deploy (push) Successful in 2m46s
### 🍪 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
105 lines
4.4 KiB
TypeScript
105 lines
4.4 KiB
TypeScript
/**
|
||
* Утилиты для работы с токенами авторизации
|
||
* @module auth-utils
|
||
*/
|
||
|
||
// Экспортируем константы для использования в других модулях
|
||
export const AUTH_TOKEN_KEY = 'auth_token' // localStorage fallback
|
||
export const SESSION_COOKIE_NAME = 'session_token' // ✅ httpOnly cookie от backend
|
||
export const CSRF_TOKEN_KEY = 'csrf_token'
|
||
|
||
/**
|
||
* Получает токен авторизации из cookie
|
||
* @returns Токен или пустую строку, если токен не найден
|
||
*/
|
||
export function getAuthTokenFromCookie(): string {
|
||
console.log('[Auth] Checking auth token in cookies...')
|
||
const cookieItems = document.cookie.split(';')
|
||
for (const item of cookieItems) {
|
||
const [name, value] = item.trim().split('=')
|
||
if (name === AUTH_TOKEN_KEY) {
|
||
console.log('[Auth] Found auth token in cookies')
|
||
return value
|
||
}
|
||
}
|
||
console.log('[Auth] No auth token found in cookies')
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* Получает CSRF-токен из cookie
|
||
* @returns CSRF-токен или пустую строку, если токен не найден
|
||
*/
|
||
export function getCsrfTokenFromCookie(): string {
|
||
console.log('[Auth] Checking CSRF token in cookies...')
|
||
const cookieItems = document.cookie.split(';')
|
||
for (const item of cookieItems) {
|
||
const [name, value] = item.trim().split('=')
|
||
if (name === CSRF_TOKEN_KEY) {
|
||
console.log('[Auth] Found CSRF token in cookies')
|
||
return value
|
||
}
|
||
}
|
||
console.log('[Auth] No CSRF token found in cookies')
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* Очищает все токены авторизации
|
||
*/
|
||
export function clearAuthTokens(): void {
|
||
console.log('[Auth] Clearing all auth tokens...')
|
||
// Очищаем токен из localStorage
|
||
localStorage.removeItem(AUTH_TOKEN_KEY)
|
||
|
||
// Для удаления cookie устанавливаем ей истекшее время жизни
|
||
// biome-ignore lint/suspicious/noDocumentCookie: Требуется для кроссбраузерной совместимости
|
||
document.cookie = `${AUTH_TOKEN_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
|
||
// biome-ignore lint/suspicious/noDocumentCookie: Требуется для кроссбраузерной совместимости
|
||
document.cookie = `${CSRF_TOKEN_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`
|
||
console.log('[Auth] Auth tokens cleared')
|
||
}
|
||
|
||
/**
|
||
* Сохраняет токен авторизации
|
||
* @param token - Токен для сохранения
|
||
*/
|
||
export function saveAuthToken(token: string): void {
|
||
console.log('[Auth] Attempting to save auth token...')
|
||
if (!token) {
|
||
console.log('[Auth] No token provided, skipping save')
|
||
return
|
||
}
|
||
|
||
// Всегда сохраняем токен в localStorage для надежности
|
||
localStorage.setItem(AUTH_TOKEN_KEY, token)
|
||
console.log('[Auth] Token saved to localStorage')
|
||
}
|
||
|
||
/**
|
||
* Проверяет, авторизован ли пользователь через httpOnly cookie
|
||
* @returns Статус авторизации (всегда true для httpOnly - проверка на backend)
|
||
*/
|
||
export function checkAuthStatus(): boolean {
|
||
console.log('[Auth] Checking authentication status...')
|
||
|
||
// 🍪 Админка использует httpOnly cookies - токен недоступен JavaScript!
|
||
// Браузер автоматически отправляет session_token cookie с каждым запросом
|
||
// Окончательная проверка авторизации происходит на backend через GraphQL
|
||
|
||
// Проверяем localStorage только как fallback для старых сессий
|
||
const localToken = localStorage.getItem(AUTH_TOKEN_KEY)
|
||
const hasLocalToken = !!localToken && localToken.length > 10
|
||
|
||
if (hasLocalToken) {
|
||
console.log('[Auth] Found legacy token in localStorage - will be migrated to httpOnly cookie')
|
||
}
|
||
|
||
// ✅ Для httpOnly cookie всегда возвращаем true
|
||
// Реальная проверка авторизации произойдет при первом GraphQL запросе
|
||
// Если cookie недействителен, backend вернет ошибку авторизации
|
||
console.log('[Auth] Using httpOnly cookie authentication - status will be verified by backend')
|
||
|
||
return true // ✅ Полагаемся на httpOnly cookie + backend проверку
|
||
}
|