This commit is contained in:
@@ -2,9 +2,6 @@
|
|||||||
Единый middleware для обработки авторизации в GraphQL запросах
|
Единый middleware для обработки авторизации в GraphQL запросах
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import secrets
|
|
||||||
import time
|
import time
|
||||||
from collections.abc import Awaitable, MutableMapping
|
from collections.abc import Awaitable, MutableMapping
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any, Callable, Optional
|
||||||
@@ -37,10 +34,6 @@ from utils.logger import root_logger as logger
|
|||||||
|
|
||||||
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
|
ADMIN_EMAILS = ADMIN_EMAILS_LIST.split(",")
|
||||||
|
|
||||||
# Добавляем константу для CSRF токена
|
|
||||||
CSRF_TOKEN_KEY = os.environ.get("CSRF_TOKEN_KEY", "csrf_token")
|
|
||||||
CSRF_HEADER_NAME = os.environ.get("CSRF_HEADER_NAME", "X-CSRF-Token")
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatedUser:
|
class AuthenticatedUser:
|
||||||
"""Аутентифицированный пользователь"""
|
"""Аутентифицированный пользователь"""
|
||||||
@@ -355,54 +348,6 @@ class AuthMiddleware:
|
|||||||
logger.error(f"[AuthMiddleware] Ошибка в GraphQL resolve: {e!s}")
|
logger.error(f"[AuthMiddleware] Ошибка в GraphQL resolve: {e!s}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def generate_csrf_token(self, user_id: Optional[str] = None) -> str:
|
|
||||||
"""
|
|
||||||
Генерирует криптографически стойкий CSRF токен
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_id: Необязательный идентификатор пользователя для привязки токена
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: CSRF токен
|
|
||||||
"""
|
|
||||||
# Используем secrets для генерации случайных байт
|
|
||||||
random_bytes = secrets.token_bytes(32)
|
|
||||||
|
|
||||||
# Добавляем user_id для привязки токена к сессии, если передан
|
|
||||||
salt = user_id.encode() if user_id else b""
|
|
||||||
|
|
||||||
# Создаем хеш с солью
|
|
||||||
csrf_token = hashlib.sha256(random_bytes + salt).hexdigest()
|
|
||||||
|
|
||||||
return csrf_token
|
|
||||||
|
|
||||||
async def validate_csrf_token(self, request: Request) -> bool:
|
|
||||||
"""
|
|
||||||
Проверяет CSRF токен в запросе
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: Объект запроса
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: Результат проверки CSRF токена
|
|
||||||
"""
|
|
||||||
# Получаем токен из заголовка
|
|
||||||
request_csrf_token = request.headers.get(CSRF_HEADER_NAME)
|
|
||||||
|
|
||||||
# Получаем токен из куки
|
|
||||||
cookie_csrf_token = request.cookies.get(CSRF_TOKEN_KEY)
|
|
||||||
|
|
||||||
# Проверяем наличие и совпадение токенов
|
|
||||||
if not request_csrf_token or not cookie_csrf_token:
|
|
||||||
logger.warning("[CSRF] Токен отсутствует")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if request_csrf_token != cookie_csrf_token:
|
|
||||||
logger.warning("[CSRF] Токены не совпадают")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def process_result(self, request: Request, result: Any) -> Response:
|
async def process_result(self, request: Request, result: Any) -> Response:
|
||||||
"""
|
"""
|
||||||
Обрабатывает результат GraphQL запроса, поддерживая установку cookie
|
Обрабатывает результат GraphQL запроса, поддерживая установку cookie
|
||||||
@@ -476,23 +421,6 @@ class AuthMiddleware:
|
|||||||
samesite=SESSION_COOKIE_SAMESITE,
|
samesite=SESSION_COOKIE_SAMESITE,
|
||||||
)
|
)
|
||||||
logger.debug(f"[graphql_handler] Удалена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
logger.debug(f"[graphql_handler] Удалена cookie {SESSION_COOKIE_NAME} для операции {op_name}")
|
||||||
|
|
||||||
# Если это аутентификация, генерируем и устанавливаем CSRF токен
|
|
||||||
if op_name in ["login", "refreshtoken"]:
|
|
||||||
# Генерируем CSRF токен
|
|
||||||
csrf_token = self.generate_csrf_token()
|
|
||||||
|
|
||||||
# Устанавливаем токен в куки
|
|
||||||
response.set_cookie(
|
|
||||||
key=CSRF_TOKEN_KEY,
|
|
||||||
value=csrf_token,
|
|
||||||
httponly=False, # Должен быть доступен JavaScript
|
|
||||||
secure=SESSION_COOKIE_SECURE,
|
|
||||||
samesite=SESSION_COOKIE_SAMESITE,
|
|
||||||
max_age=SESSION_COOKIE_MAX_AGE,
|
|
||||||
)
|
|
||||||
logger.debug("[CSRF] Установлен новый CSRF токен")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[process_result] Ошибка при обработке POST запроса: {e!s}")
|
logger.error(f"[process_result] Ошибка при обработке POST запроса: {e!s}")
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, createContext, createSignal, JSX, useContext } from 'solid-js'
|
import { Component, createContext, createSignal, JSX, onMount, useContext } from 'solid-js'
|
||||||
import { query } from '../graphql'
|
import { query } from '../graphql'
|
||||||
import { ADMIN_LOGIN_MUTATION, ADMIN_LOGOUT_MUTATION } from '../graphql/mutations'
|
import { ADMIN_LOGIN_MUTATION, ADMIN_LOGOUT_MUTATION } from '../graphql/mutations'
|
||||||
import {
|
import {
|
||||||
@@ -45,12 +45,14 @@ export {
|
|||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
isAuthenticated: () => boolean
|
isAuthenticated: () => boolean
|
||||||
|
isReady: () => boolean
|
||||||
login: (username: string, password: string) => Promise<void>
|
login: (username: string, password: string) => Promise<void>
|
||||||
logout: () => Promise<void>
|
logout: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType>({
|
const AuthContext = createContext<AuthContextType>({
|
||||||
isAuthenticated: () => false,
|
isAuthenticated: () => false,
|
||||||
|
isReady: () => false,
|
||||||
login: async () => {},
|
login: async () => {},
|
||||||
logout: async () => {}
|
logout: async () => {}
|
||||||
})
|
})
|
||||||
@@ -64,10 +66,27 @@ interface AuthProviderProps {
|
|||||||
export const AuthProvider: Component<AuthProviderProps> = (props) => {
|
export const AuthProvider: Component<AuthProviderProps> = (props) => {
|
||||||
console.log('[AuthProvider] Initializing...')
|
console.log('[AuthProvider] Initializing...')
|
||||||
const [isAuthenticated, setIsAuthenticated] = createSignal(checkAuthStatus())
|
const [isAuthenticated, setIsAuthenticated] = createSignal(checkAuthStatus())
|
||||||
|
const [isReady, setIsReady] = createSignal(false)
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[AuthProvider] Initial auth state: ${isAuthenticated() ? 'authenticated' : 'not authenticated'}`
|
`[AuthProvider] Initial auth state: ${isAuthenticated() ? 'authenticated' : 'not authenticated'}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Инициализация авторизации при монтировании
|
||||||
|
onMount(async () => {
|
||||||
|
console.log('[AuthProvider] Performing auth initialization...')
|
||||||
|
|
||||||
|
// Небольшая задержка для завершения других инициализаций
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
// Проверяем текущее состояние авторизации
|
||||||
|
const authStatus = checkAuthStatus()
|
||||||
|
setIsAuthenticated(authStatus)
|
||||||
|
|
||||||
|
console.log('[AuthProvider] Auth initialization complete, ready for requests')
|
||||||
|
setIsReady(true)
|
||||||
|
})
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = async (username: string, password: string) => {
|
||||||
console.log('[AuthProvider] Attempting login...')
|
console.log('[AuthProvider] Attempting login...')
|
||||||
try {
|
try {
|
||||||
@@ -127,6 +146,7 @@ export const AuthProvider: Component<AuthProviderProps> = (props) => {
|
|||||||
|
|
||||||
const value: AuthContextType = {
|
const value: AuthContextType = {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
isReady,
|
||||||
login,
|
login,
|
||||||
logout
|
logout
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
GET_COMMUNITIES_QUERY,
|
GET_COMMUNITIES_QUERY,
|
||||||
GET_TOPICS_QUERY
|
GET_TOPICS_QUERY
|
||||||
} from '../graphql/queries'
|
} from '../graphql/queries'
|
||||||
|
import { useAuth } from './auth'
|
||||||
|
|
||||||
export interface Community {
|
export interface Community {
|
||||||
id: number
|
id: number
|
||||||
@@ -92,6 +93,7 @@ const DataContext = createContext<DataContextType>({
|
|||||||
const COMMUNITY_STORAGE_KEY = 'admin-selected-community'
|
const COMMUNITY_STORAGE_KEY = 'admin-selected-community'
|
||||||
|
|
||||||
export function DataProvider(props: { children: JSX.Element }) {
|
export function DataProvider(props: { children: JSX.Element }) {
|
||||||
|
const auth = useAuth()
|
||||||
const [communities, setCommunities] = createSignal<Community[]>([])
|
const [communities, setCommunities] = createSignal<Community[]>([])
|
||||||
const [topics, setTopics] = createSignal<Topic[]>([])
|
const [topics, setTopics] = createSignal<Topic[]>([])
|
||||||
const [allTopics, setAllTopics] = createSignal<Topic[]>([])
|
const [allTopics, setAllTopics] = createSignal<Topic[]>([])
|
||||||
@@ -140,11 +142,16 @@ export function DataProvider(props: { children: JSX.Element }) {
|
|||||||
// Эффект для загрузки ролей при изменении сообщества
|
// Эффект для загрузки ролей при изменении сообщества
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const community = selectedCommunity()
|
const community = selectedCommunity()
|
||||||
if (community !== null) {
|
const isReady = auth.isReady()
|
||||||
console.log('[DataProvider] Загрузка ролей для сообщества:', community)
|
const isAuthenticated = auth.isAuthenticated()
|
||||||
|
|
||||||
|
if (community !== null && isReady && isAuthenticated) {
|
||||||
|
console.log('[DataProvider] Auth ready, загрузка ролей для сообщества:', community)
|
||||||
loadRoles(community).catch((err) => {
|
loadRoles(community).catch((err) => {
|
||||||
console.warn('Не удалось загрузить роли для сообщества:', err)
|
console.warn('Не удалось загрузить роли для сообщества:', err)
|
||||||
})
|
})
|
||||||
|
} else if (!isReady) {
|
||||||
|
console.log('[DataProvider] Ожидание готовности авторизации перед загрузкой ролей')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -324,6 +331,26 @@ export function DataProvider(props: { children: JSX.Element }) {
|
|||||||
// biome-ignore lint/suspicious/noExplicitAny: grahphql
|
// biome-ignore lint/suspicious/noExplicitAny: grahphql
|
||||||
queryGraphQL: async (queryStr: string, variables?: Record<string, any>) => {
|
queryGraphQL: async (queryStr: string, variables?: Record<string, any>) => {
|
||||||
try {
|
try {
|
||||||
|
// Ждем готовности авторизации перед выполнением запроса
|
||||||
|
const maxWaitTime = 5000 // 5 секунд максимум
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
while (!auth.isReady() && Date.now() - startTime < maxWaitTime) {
|
||||||
|
console.log('[DataProvider] Ожидание готовности авторизации для GraphQL запроса...')
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!auth.isReady()) {
|
||||||
|
console.warn('[DataProvider] Таймаут ожидания готовности авторизации')
|
||||||
|
throw new Error('Auth not ready')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!auth.isAuthenticated()) {
|
||||||
|
console.warn('[DataProvider] Пользователь не авторизован')
|
||||||
|
throw new Error('User not authenticated')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DataProvider] Выполнение GraphQL запроса после готовности авторизации')
|
||||||
return await query(`${location.origin}/graphql`, queryStr, variables)
|
return await query(`${location.origin}/graphql`, queryStr, variables)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка выполнения GraphQL запроса:', error)
|
console.error('Ошибка выполнения GraphQL запроса:', error)
|
||||||
|
@@ -3,27 +3,18 @@
|
|||||||
* @module api
|
* @module api
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AUTH_TOKEN_KEY, clearAuthTokens, getAuthTokenFromCookie } from '../utils/auth'
|
import {
|
||||||
|
AUTH_TOKEN_KEY,
|
||||||
|
clearAuthTokens,
|
||||||
|
getAuthTokenFromCookie,
|
||||||
|
getCsrfTokenFromCookie
|
||||||
|
} from '../utils/auth'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Тип для произвольных данных GraphQL
|
* Тип для произвольных данных GraphQL
|
||||||
*/
|
*/
|
||||||
type GraphQLData = Record<string, unknown>
|
type GraphQLData = Record<string, unknown>
|
||||||
|
|
||||||
const CSRF_TOKEN_KEY = 'csrf_token'
|
|
||||||
const CSRF_HEADER_NAME = 'X-CSRF-Token'
|
|
||||||
|
|
||||||
function getCsrfTokenFromCookie(): string {
|
|
||||||
const cookieItems = document.cookie.split(';')
|
|
||||||
for (const item of cookieItems) {
|
|
||||||
const [name, value] = item.trim().split('=')
|
|
||||||
if (name === CSRF_TOKEN_KEY) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Возвращает заголовки для GraphQL запроса с учетом авторизации и CSRF
|
* Возвращает заголовки для GraphQL запроса с учетом авторизации и CSRF
|
||||||
* @returns Объект с заголовками
|
* @returns Объект с заголовками
|
||||||
@@ -52,21 +43,15 @@ function getRequestHeaders(): Record<string, string> {
|
|||||||
// Добавляем CSRF-токен, если он есть
|
// Добавляем CSRF-токен, если он есть
|
||||||
const csrfToken = getCsrfTokenFromCookie()
|
const csrfToken = getCsrfTokenFromCookie()
|
||||||
if (csrfToken) {
|
if (csrfToken) {
|
||||||
headers[CSRF_HEADER_NAME] = csrfToken
|
headers['X-CSRF-Token'] = csrfToken
|
||||||
console.debug('Добавлен CSRF-токен в запрос')
|
console.debug('Добавлен CSRF-токен в запрос')
|
||||||
}
|
}
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GraphQLError extends Error {
|
|
||||||
extensions?: {
|
|
||||||
code?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Выполняет GraphQL запрос
|
* Выполняет GraphQL запрос с retry логикой для 503 ошибок
|
||||||
* @param endpoint - URL эндпоинта GraphQL
|
* @param endpoint - URL эндпоинта GraphQL
|
||||||
* @param query - GraphQL запрос
|
* @param query - GraphQL запрос
|
||||||
* @param variables - Переменные запроса
|
* @param variables - Переменные запроса
|
||||||
@@ -77,8 +62,12 @@ export async function query<T = unknown>(
|
|||||||
query: string,
|
query: string,
|
||||||
variables?: Record<string, unknown>
|
variables?: Record<string, unknown>
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
const maxRetries = 3
|
||||||
|
const retryDelay = 500 // 500ms базовая задержка
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
console.log(`[GraphQL] Making request to ${endpoint}`)
|
console.log(`[GraphQL] Making request to ${endpoint} (attempt ${attempt}/${maxRetries})`)
|
||||||
console.log(`[GraphQL] Query: ${query.substring(0, 100)}...`)
|
console.log(`[GraphQL] Query: ${query.substring(0, 100)}...`)
|
||||||
|
|
||||||
// Используем существующую функцию для получения всех необходимых заголовков
|
// Используем существующую функцию для получения всех необходимых заголовков
|
||||||
@@ -99,7 +88,15 @@ export async function query<T = unknown>(
|
|||||||
|
|
||||||
console.log(`[GraphQL] Response status: ${response.status}`)
|
console.log(`[GraphQL] Response status: ${response.status}`)
|
||||||
|
|
||||||
// Обработка HTTP-ошибок авторизации
|
// Если получили 503 и это не последняя попытка, повторяем запрос
|
||||||
|
if (response.status === 503 && attempt < maxRetries) {
|
||||||
|
const delay = retryDelay * attempt // Экспоненциальная задержка
|
||||||
|
console.log(`[GraphQL] Got 503 error, retrying after ${delay}ms...`)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
console.log('[GraphQL] Unauthorized response, clearing auth tokens')
|
console.log('[GraphQL] Unauthorized response, clearing auth tokens')
|
||||||
clearAuthTokens()
|
clearAuthTokens()
|
||||||
@@ -107,53 +104,51 @@ export async function query<T = unknown>(
|
|||||||
if (!window.location.pathname.includes('/login')) {
|
if (!window.location.pathname.includes('/login')) {
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
}
|
}
|
||||||
throw new Error('Unauthorized')
|
}
|
||||||
|
const errorText = await response.text()
|
||||||
|
throw new Error(`HTTP error: ${response.status} ${errorText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json()
|
const result = await response.json()
|
||||||
console.log('[GraphQL] Response received:', result)
|
console.log('[GraphQL] Response received:', result)
|
||||||
|
|
||||||
// Обработка CSRF-ошибок
|
|
||||||
if (result.errors) {
|
if (result.errors) {
|
||||||
const csrfError = result.errors.find((error: GraphQLError) =>
|
// Проверяем ошибки авторизации
|
||||||
['CSRF_TOKEN_MISSING', 'CSRF_TOKEN_INVALID'].includes(error.extensions?.code ?? '')
|
const hasUnauthorized = result.errors.some(
|
||||||
|
(error: { message?: string }) =>
|
||||||
|
error.message?.toLowerCase().includes('unauthorized') ||
|
||||||
|
error.message?.toLowerCase().includes('please login')
|
||||||
)
|
)
|
||||||
|
|
||||||
if (csrfError) {
|
if (hasUnauthorized) {
|
||||||
console.error('[GraphQL] CSRF Error:', csrfError)
|
console.log('[GraphQL] Unauthorized error in response, clearing auth tokens')
|
||||||
|
|
||||||
// Принудительное обновление страницы для получения нового токена
|
|
||||||
window.location.reload()
|
|
||||||
|
|
||||||
throw new Error(`CSRF Error: ${csrfError.message}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка других GraphQL-ошибок
|
|
||||||
const unauthorizedError = result.errors.find(
|
|
||||||
(error: GraphQLError) =>
|
|
||||||
error.message.toLowerCase().includes('unauthorized') ||
|
|
||||||
error.message.toLowerCase().includes('please login')
|
|
||||||
)
|
|
||||||
|
|
||||||
if (unauthorizedError) {
|
|
||||||
console.log('[GraphQL] Unauthorized response, clearing auth tokens')
|
|
||||||
clearAuthTokens()
|
clearAuthTokens()
|
||||||
|
|
||||||
// Перенаправляем на страницу входа только если мы не на ней
|
// Перенаправляем на страницу входа только если мы не на ней
|
||||||
if (!window.location.pathname.includes('/login')) {
|
if (!window.location.pathname.includes('/login')) {
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
}
|
}
|
||||||
throw new Error('Unauthorized')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(result.errors.map((e: GraphQLError) => e.message).join('; '))
|
// Handle GraphQL errors
|
||||||
|
const errorMessage = result.errors.map((e: { message?: string }) => e.message).join(', ')
|
||||||
|
throw new Error(`GraphQL error: ${errorMessage}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data as T
|
return result.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Если это последняя попытка или ошибка не 503, пробрасываем ошибку
|
||||||
|
if (attempt === maxRetries || !(error instanceof Error) || !error.message.includes('503')) {
|
||||||
console.error('[GraphQL] Query error:', error)
|
console.error('[GraphQL] Query error:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Для других ошибок на промежуточных попытках просто логируем
|
||||||
|
console.warn(`[GraphQL] Attempt ${attempt} failed, retrying...`, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Этот код никогда не должен выполниться, но добавляем для TypeScript
|
||||||
|
throw new Error('Max retries exceeded')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -9,11 +9,23 @@ import AdminPage from '../routes/admin'
|
|||||||
export const ProtectedRoute = () => {
|
export const ProtectedRoute = () => {
|
||||||
console.log('[ProtectedRoute] Checking authentication...')
|
console.log('[ProtectedRoute] Checking authentication...')
|
||||||
const auth = useAuth()
|
const auth = useAuth()
|
||||||
|
const isReady = auth.isReady()
|
||||||
const authenticated = auth.isAuthenticated()
|
const authenticated = auth.isAuthenticated()
|
||||||
console.log(
|
|
||||||
`[ProtectedRoute] Authentication state: ${authenticated ? 'authenticated' : 'not authenticated'}`
|
|
||||||
)
|
|
||||||
|
|
||||||
|
console.log(`[ProtectedRoute] Auth state: ready=${isReady}, authenticated=${authenticated}`)
|
||||||
|
|
||||||
|
// Если авторизация еще не готова, показываем загрузку
|
||||||
|
if (!isReady) {
|
||||||
|
console.log('[ProtectedRoute] Auth not ready, showing loading...')
|
||||||
|
return (
|
||||||
|
<div class="loading-screen">
|
||||||
|
<div class="loading-spinner" />
|
||||||
|
<div>Инициализация авторизации...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если авторизация готова, но пользователь не аутентифицирован
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
console.log('[ProtectedRoute] Not authenticated, redirecting to login...')
|
console.log('[ProtectedRoute] Not authenticated, redirecting to login...')
|
||||||
// Используем window.location.href для редиректа
|
// Используем window.location.href для редиректа
|
||||||
@@ -21,11 +33,12 @@ export const ProtectedRoute = () => {
|
|||||||
return (
|
return (
|
||||||
<div class="loading-screen">
|
<div class="loading-screen">
|
||||||
<div class="loading-spinner" />
|
<div class="loading-spinner" />
|
||||||
<div>Проверка авторизации...</div>
|
<div>Перенаправление на страницу входа...</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[ProtectedRoute] Auth ready and authenticated, rendering admin panel...')
|
||||||
return (
|
return (
|
||||||
<DataProvider>
|
<DataProvider>
|
||||||
<TableSortProvider>
|
<TableSortProvider>
|
||||||
|
@@ -6,14 +6,9 @@ from ariadne import (
|
|||||||
ObjectType,
|
ObjectType,
|
||||||
QueryType,
|
QueryType,
|
||||||
SchemaBindable,
|
SchemaBindable,
|
||||||
graphql,
|
|
||||||
load_schema_from_path,
|
load_schema_from_path,
|
||||||
make_executable_schema,
|
|
||||||
)
|
)
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import JSONResponse, Response
|
|
||||||
|
|
||||||
from auth.middleware import CSRF_HEADER_NAME, CSRF_TOKEN_KEY
|
|
||||||
from services.db import create_table_if_not_exists, local_session
|
from services.db import create_table_if_not_exists, local_session
|
||||||
|
|
||||||
# Создаем основные типы
|
# Создаем основные типы
|
||||||
@@ -83,79 +78,3 @@ def create_all_tables() -> None:
|
|||||||
table_name = getattr(model, "__tablename__", str(model))
|
table_name = getattr(model, "__tablename__", str(model))
|
||||||
logger.error(f"Error creating table {table_name}: {e}")
|
logger.error(f"Error creating table {table_name}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def graphql_handler(request: Request) -> Response:
|
|
||||||
"""
|
|
||||||
Обработчик GraphQL запросов с проверкой CSRF токена
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Проверяем CSRF токен для всех мутаций
|
|
||||||
data = await request.json()
|
|
||||||
op_name = data.get("operationName", "").lower()
|
|
||||||
|
|
||||||
# Проверяем CSRF только для мутаций
|
|
||||||
if op_name and (op_name.endswith("mutation") or op_name in ["login", "refreshtoken"]):
|
|
||||||
# Получаем токен из заголовка
|
|
||||||
request_csrf_token = request.headers.get(CSRF_HEADER_NAME)
|
|
||||||
|
|
||||||
# Получаем токен из куки
|
|
||||||
cookie_csrf_token = request.cookies.get(CSRF_TOKEN_KEY)
|
|
||||||
|
|
||||||
# Строгая проверка токена
|
|
||||||
if not request_csrf_token or not cookie_csrf_token:
|
|
||||||
# Возвращаем ошибку как часть GraphQL-ответа
|
|
||||||
return JSONResponse(
|
|
||||||
{
|
|
||||||
"data": None,
|
|
||||||
"errors": [{"message": "CSRF токен отсутствует", "extensions": {"code": "CSRF_TOKEN_MISSING"}}],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if request_csrf_token != cookie_csrf_token:
|
|
||||||
# Возвращаем ошибку как часть GraphQL-ответа
|
|
||||||
return JSONResponse(
|
|
||||||
{
|
|
||||||
"data": None,
|
|
||||||
"errors": [
|
|
||||||
{"message": "Недопустимый CSRF токен", "extensions": {"code": "CSRF_TOKEN_INVALID"}}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Существующая логика обработки GraphQL запроса
|
|
||||||
schema = get_schema()
|
|
||||||
result = await graphql(
|
|
||||||
schema,
|
|
||||||
data.get("query"),
|
|
||||||
variable_values=data.get("variables"),
|
|
||||||
operation_name=data.get("operationName"),
|
|
||||||
context_value={"request": request},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Обработка ошибок GraphQL
|
|
||||||
if result.errors:
|
|
||||||
return JSONResponse(
|
|
||||||
{
|
|
||||||
"data": result.data,
|
|
||||||
"errors": [{"message": str(error), "locations": error.locations} for error in result.errors],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse({"data": result.data})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"GraphQL handler error: {e}")
|
|
||||||
return JSONResponse(
|
|
||||||
{
|
|
||||||
"data": None,
|
|
||||||
"errors": [{"message": "Внутренняя ошибка сервера", "extensions": {"code": "INTERNAL_SERVER_ERROR"}}],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_schema():
|
|
||||||
"""
|
|
||||||
Создает и возвращает GraphQL схему
|
|
||||||
"""
|
|
||||||
return make_executable_schema(type_defs, resolvers)
|
|
||||||
|
Reference in New Issue
Block a user