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 logout: () => Promise } const AuthContext = createContext({ isAuthenticated: () => false, isReady: () => false, login: async () => {}, logout: async () => {} }) export const useAuth = () => useContext(AuthContext) interface AuthProviderProps { children: JSX.Element } export const AuthProvider: Component = (props) => { console.log('[AuthProvider] Initializing...') // Начинаем с false чтобы избежать мерцания, реальная проверка будет в onMount const [isAuthenticated, setIsAuthenticated] = createSignal(false) const [isReady, setIsReady] = createSignal(false) // Флаг для предотвращения повторных инициализаций let isInitializing = false console.log('[AuthProvider] Initial auth state: not authenticated (will check via GraphQL)') // Инициализация авторизации при монтировании onMount(async () => { // Защита от повторных вызовов if (isInitializing) { console.log('[AuthProvider] Already initializing, skipping...') return } isInitializing = true console.log('[AuthProvider] Performing auth initialization...') // 🍪 Для httpOnly cookies проверяем авторизацию через GraphQL запрос try { console.log('[AuthProvider] Checking authentication via GraphQL...') // Добавляем таймаут для запроса (5 секунд для лучшего UX) const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Auth check timeout')), 5000) ) const authPromise = query<{ me: { id: string } | null }>( `${location.origin}/graphql`, ` query CheckAuth { me { id name email } } ` ) // Делаем тестовый запрос для проверки авторизации с таймаутом const result = (await Promise.race([authPromise, timeoutPromise])) as { me: { id: string; name: string; email: string } | null } 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) } finally { // Всегда устанавливаем ready в true, даже при ошибке console.log('[AuthProvider] Auth initialization complete, ready for requests') setIsReady(true) isInitializing = false } }) 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...') // Предотвращаем повторные инициализации во время logout isInitializing = true 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' } finally { isInitializing = false } } const value: AuthContextType = { isAuthenticated, isReady, login, logout } console.log('[AuthProvider] Rendering provider with context') return {props.children} } // 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 } }