Files
core/panel/context/auth.tsx
Untone 752e2dcbdc
All checks were successful
Deploy on push / deploy (push) Successful in 2m46s
[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

196 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, createContext, createSignal, JSX, onMount, useContext } from 'solid-js'
import { AuthSuccess } from '~/graphql/generated/graphql'
import { query } from '../graphql'
import { ADMIN_LOGIN_MUTATION, ADMIN_LOGOUT_MUTATION } from '../graphql/mutations'
import {
AUTH_TOKEN_KEY,
CSRF_TOKEN_KEY,
checkAuthStatus,
clearAuthTokens,
getAuthTokenFromCookie,
getCsrfTokenFromCookie,
saveAuthToken
} from '../utils/auth'
/**
* Модуль авторизации
* @module auth
*/
/**
* Интерфейс для учетных данных
*/
export interface Credentials {
email: string
password: string
}
/**
* Интерфейс для результата авторизации
*/
export interface LoginResult {
success: boolean
token?: string
error?: string
}
// Экспортируем утилитарные функции для обратной совместимости
export {
AUTH_TOKEN_KEY,
CSRF_TOKEN_KEY,
getAuthTokenFromCookie,
getCsrfTokenFromCookie,
checkAuthStatus,
clearAuthTokens,
saveAuthToken
}
interface AuthContextType {
isAuthenticated: () => boolean
isReady: () => boolean
login: (username: string, password: string) => Promise<void>
logout: () => Promise<void>
}
const AuthContext = createContext<AuthContextType>({
isAuthenticated: () => false,
isReady: () => false,
login: async () => {},
logout: async () => {}
})
export const useAuth = () => useContext(AuthContext)
interface AuthProviderProps {
children: JSX.Element
}
export const AuthProvider: Component<AuthProviderProps> = (props) => {
console.log('[AuthProvider] Initializing...')
const [isAuthenticated, setIsAuthenticated] = createSignal(checkAuthStatus())
const [isReady, setIsReady] = createSignal(false)
console.log(
`[AuthProvider] Initial auth state: ${isAuthenticated() ? 'authenticated' : 'not authenticated'}`
)
// Инициализация авторизации при монтировании
onMount(async () => {
console.log('[AuthProvider] Performing auth initialization...')
// 🍪 Для httpOnly cookies проверяем авторизацию через GraphQL запрос
try {
console.log('[AuthProvider] Checking authentication via GraphQL...')
// Делаем тестовый запрос для проверки авторизации
const result = await query<{ me: { id: string } | null }>(`${location.origin}/graphql`, `
query CheckAuth {
me {
id
name
email
}
}
`)
if (result?.me?.id) {
console.log('[AuthProvider] User authenticated via httpOnly cookie:', result.me.id)
setIsAuthenticated(true)
} else {
console.log('[AuthProvider] No authenticated user found')
setIsAuthenticated(false)
}
} catch (error) {
console.log('[AuthProvider] Authentication check failed:', error)
setIsAuthenticated(false)
}
console.log('[AuthProvider] Auth initialization complete, ready for requests')
setIsReady(true)
})
const login = async (username: string, password: string) => {
console.log('[AuthProvider] Attempting login...')
try {
const result = await query<{
login: { success: boolean; token?: string }
}>(`${location.origin}/graphql`, ADMIN_LOGIN_MUTATION, {
email: username,
password
})
if (result?.login?.success) {
console.log('[AuthProvider] Login successful')
// Backend автоматически установил session_token cookie при успешном login
console.log('[AuthProvider] Token saved in httpOnly cookie by backend')
setIsAuthenticated(true)
// Убираем window.location.href - пусть роутер сам обрабатывает навигацию
} else {
console.error('[AuthProvider] Login failed')
throw new Error('Неверные учетные данные')
}
} catch (error) {
console.error('[AuthProvider] Login error:', error)
throw error
}
}
const logout = async () => {
console.log('[AuthProvider] Attempting logout...')
try {
// Сначала очищаем токены на клиенте
clearAuthTokens()
setIsAuthenticated(false)
// Затем делаем запрос на сервер
const result = await query<{ logout: { success: boolean; message?: string } }>(
`${location.origin}/graphql`,
ADMIN_LOGOUT_MUTATION
)
console.log('[AuthProvider] Logout response:', result)
if (result?.logout?.success) {
console.log('[AuthProvider] Logout successful:', result.logout.message)
window.location.href = '/login'
} else {
console.warn('[AuthProvider] Logout was not successful:', result?.logout?.message)
// Все равно редиректим на страницу входа
window.location.href = '/login'
}
} catch (error) {
console.error('[AuthProvider] Logout error:', error)
// При любой ошибке редиректим на страницу входа
window.location.href = '/login'
}
}
const value: AuthContextType = {
isAuthenticated,
isReady,
login,
logout
}
console.log('[AuthProvider] Rendering provider with context')
return <AuthContext.Provider value={value}>{props.children}</AuthContext.Provider>
}
// Export the logout function for direct use
export const logout = async () => {
console.log('[Auth] Executing standalone logout...')
try {
const result = await query<{ logout: AuthSuccess }>(`${location.origin}/graphql`, ADMIN_LOGOUT_MUTATION)
console.log('[Auth] Standalone logout result:', result)
if (result?.logout?.success) {
clearAuthTokens()
return true
}
return false
} catch (error) {
console.error('[Auth] Standalone logout error:', error)
// Даже при ошибке очищаем токены
clearAuthTokens()
throw error
}
}