225 lines
7.1 KiB
TypeScript
225 lines
7.1 KiB
TypeScript
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...')
|
||
// Начинаем с 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 <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
|
||
}
|
||
}
|