/** * Copyright (c) 2024-2025 Тарабанов Александр Викторович * All rights reserved. * * This software is proprietary and confidential. * Unauthorized copying, modification, or distribution is prohibited. * * For licensing inquiries: info@hb3-accelerator.com * Website: https://hb3-accelerator.com * GitHub: https://github.com/HB3-ACCELERATOR */ import { ref, onMounted, onUnmounted, provide, inject } from 'vue'; import axios from '../api/axios'; // === SINGLETON STATE === const isAuthenticated = ref(false); const authType = ref(null); const userId = ref(null); const address = ref(null); const telegramId = ref(null); const email = ref(null); const processedGuestIds = ref([]); const identities = ref([]); const tokenBalances = ref([]); const userAccessLevel = ref({ level: 'guest', tokenCount: 0, hasAccess: false }); // Функция для обновления списка идентификаторов const updateIdentities = async () => { if (!isAuthenticated.value || !userId.value) return; // Проверяем, что identities ref существует if (!identities || typeof identities.value === 'undefined') { console.warn('Identities ref is not initialized'); return; } try { const response = await axios.get('/auth/identities'); if (response.data.success) { // Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные const filteredIdentities = response.data.identities .filter((identity) => identity.provider !== 'guest') .reduce((acc, identity) => { // Для каждого типа провайдера оставляем только один идентификатор const existingIdentity = acc.find((i) => i.provider === identity.provider); if (!existingIdentity) { acc.push(identity); } return acc; }, []); // Сравниваем новый отфильтрованный список с текущим значением const currentProviders = (identities.value || []).map(id => id?.provider || '').sort(); const newProviders = (filteredIdentities || []).map(id => id?.provider || '').sort(); const identitiesChanged = JSON.stringify(currentProviders) !== JSON.stringify(newProviders); // Обновляем реактивное значение с проверкой try { if (identities && identities.value !== undefined) { identities.value = filteredIdentities; console.log('User identities updated:', identities.value); } else { console.warn('Identities ref is not available or not initialized'); } } catch (error) { console.error('Error updating identities:', error); // Если произошла ошибка, пытаемся инициализировать identities if (identities && typeof identities.value === 'undefined') { identities.value = []; } } // Если список идентификаторов изменился, принудительно проверяем аутентификацию, // чтобы обновить authType и другие связанные данные (например, telegramId) if (identitiesChanged) { console.log('Identities changed, forcing auth check.'); await checkAuth(); // Вызываем checkAuth для обновления полного состояния } } } catch (error) { console.error('Error fetching user identities:', error); } }; // Периодическое обновление идентификаторов let identitiesInterval; const startIdentitiesPolling = () => { if (identitiesInterval) return; identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд }; const stopIdentitiesPolling = () => { if (identitiesInterval) { clearInterval(identitiesInterval); identitiesInterval = null; } }; const checkTokenBalances = async (address) => { try { const response = await axios.get(`/auth/check-tokens/${address}`); if (response.data.success) { tokenBalances.value = response.data.data; return response.data.data; } return null; } catch (error) { console.error('Error checking token balances:', error); return null; } }; const checkUserAccessLevel = async (address) => { try { const response = await axios.get(`/auth/access-level/${address}`); if (response.data.success) { userAccessLevel.value = response.data.data; return response.data.data; } return null; } catch (error) { console.error('Error checking user access level:', error); return null; } }; const updateAuth = async ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, email: newEmail, userAccessLevel: newUserAccessLevel, }) => { const wasAuthenticated = isAuthenticated.value; const previousUserId = userId.value; console.log('updateAuth called with:', { authenticated, newAuthType, newUserId, newAddress, newTelegramId, newEmail, newUserAccessLevel, }); // Убедимся, что переменные являются реактивными isAuthenticated.value = authenticated === true; authType.value = newAuthType || null; userId.value = newUserId || null; address.value = newAddress || null; telegramId.value = newTelegramId || null; email.value = newEmail || null; // Обновляем userAccessLevel только если он изменился if (newUserAccessLevel) { // Используем userAccessLevel из ответа сервера console.log('[updateAuth] Setting userAccessLevel from server:', JSON.stringify(newUserAccessLevel, null, 2)); userAccessLevel.value = newUserAccessLevel; } else if (authenticated && newAddress) { // Если userAccessLevel не передан, но пользователь аутентифицирован, запрашиваем его try { const accessLevel = await checkUserAccessLevel(newAddress); if (accessLevel && accessLevel.level !== userAccessLevel.value.level) { console.log('[updateAuth] Updating userAccessLevel from API:', accessLevel); userAccessLevel.value = accessLevel; } } catch (error) { console.error('Error updating userAccessLevel in updateAuth:', error); } } else if (!authenticated) { // Сбрасываем userAccessLevel для неавторизованных пользователей if (userAccessLevel.value.level !== 'guest') { console.log('[updateAuth] Resetting userAccessLevel to guest'); userAccessLevel.value = { level: 'guest', tokenCount: 0, hasAccess: false }; } } // Кэшируем данные аутентификации localStorage.setItem( 'authData', JSON.stringify({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, email: newEmail, }) ); // Если аутентификация через кошелек, проверяем баланс токенов и уровень доступа if (authenticated && newAuthType === 'wallet' && newAddress) { await checkTokenBalances(newAddress); await checkUserAccessLevel(newAddress); } // Обновляем идентификаторы при любом изменении аутентификации if (authenticated) { try { await updateIdentities(); startIdentitiesPolling(); } catch (error) { console.error('Error updating identities in updateAuth:', error); } } else { stopIdentitiesPolling(); try { if (identities && typeof identities.value !== 'undefined') { identities.value = []; } } catch (error) { console.error('Error clearing identities:', error); } } console.log('Auth updated:', { authenticated: isAuthenticated.value, userId: userId.value, address: address.value, telegramId: telegramId.value, email: email.value, }); // Уведомляем все компоненты об изменении состояния аутентификации // Только если состояние действительно изменилось if (wasAuthenticated !== isAuthenticated.value || previousUserId !== newUserId) { // Централизованная очистка данных при отключении if (!isAuthenticated.value && wasAuthenticated) { console.log('[useAuth] User logged out, clearing application data'); // Очищаем глобальные данные приложения window.dispatchEvent(new CustomEvent('clear-application-data')); } // Централизованное обновление данных при подключении if (isAuthenticated.value && !wasAuthenticated) { console.log('[useAuth] User logged in, refreshing application data'); window.dispatchEvent(new CustomEvent('refresh-application-data')); } window.dispatchEvent(new CustomEvent('auth-state-changed', { detail: { authenticated: isAuthenticated.value, authType: authType.value, userId: userId.value, address: address.value, userAccessLevel: userAccessLevel.value } })); } // Если пользователь только что аутентифицировался или сменил аккаунт, // пробуем связать сообщения if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) { console.log('Auth change detected, linking messages'); linkMessages(); } }; // Функция для связывания сообщений после успешной авторизации const linkMessages = async () => { try { if (isAuthenticated.value) { console.log('Linking messages after authentication'); // Проверка, есть ли гостевой ID для обработки const localGuestId = localStorage.getItem('guestId'); // Если гостевого ID нет или он уже был обработан, пропускаем запрос if (!localGuestId || processedGuestIds.value.includes(localGuestId)) { console.log('No new guest IDs to process or already processed'); return { success: true, message: 'No new guest IDs to process', processedIds: processedGuestIds.value, }; } // Создаем объект с идентификаторами для передачи на сервер const identifiersData = { userId: userId.value, guestId: localGuestId, }; // Добавляем все доступные идентификаторы if (address.value) identifiersData.address = address.value; if (email.value) identifiersData.email = email.value; if (telegramId.value) identifiersData.telegramId = telegramId.value; console.log('Sending link-guest-messages request with data:', identifiersData); /* Удаляем ненужный вызов try { // Отправляем запрос на связывание сообщений const response = await axios.post('/auth/link-guest-messages', identifiersData); if (response.data.success) { console.log('Messages linked successfully:', response.data); // Обновляем список обработанных guestIds из ответа сервера if (response.data.processedIds && Array.isArray(response.data.processedIds)) { processedGuestIds.value = [...response.data.processedIds]; console.log('Updated processed guest IDs from server:', processedGuestIds.value); } // В качестве запасного варианта также обрабатываем старый формат ответа else if (response.data.results && Array.isArray(response.data.results)) { const newProcessedIds = response.data.results .filter((result) => result.guestId) .map((result) => result.guestId); if (newProcessedIds.length > 0) { processedGuestIds.value = [ ...new Set([...processedGuestIds.value, ...newProcessedIds]), ]; console.log('Updated processed guest IDs from results:', processedGuestIds.value); } } // Очищаем гостевые сообщения из localStorage после успешного связывания localStorage.removeItem('guestMessages'); localStorage.removeItem('guestId'); return { success: true, processedIds: processedGuestIds.value, }; } } catch (error) { console.error('Error linking messages:', error); return { success: false, error: error.message, }; } */ // Предполагаем, что бэкенд автоматически связывает сообщения // Очищаем данные гостя локально console.log('Assuming backend handles message linking. Clearing local guest data.'); localStorage.removeItem('guestMessages'); localStorage.removeItem('guestId'); // Добавляем текущий guestId в обработанные, чтобы не пытаться отправить его снова if(localGuestId) { updateProcessedGuestIds([localGuestId]); } return { success: true, message: 'Local guest data cleared.' }; } return { success: false, message: 'Not authenticated' }; } catch (error) { console.error('Error in linkMessages:', error); return { success: false, error: error.message }; } }; const checkAuth = async () => { try { const response = await axios.get('/auth/check'); console.log('Auth check response:', JSON.stringify(response.data, null, 2)); const wasAuthenticated = isAuthenticated.value; const previousUserId = userId.value; const previousAuthType = authType.value; // Проверяем, изменилось ли состояние аутентификации const authChanged = ( wasAuthenticated !== response.data.authenticated || previousUserId !== response.data.userId || previousAuthType !== response.data.authType ); if (authChanged) { console.log('[checkAuth] Authentication state changed, updating...'); // Обновляем данные авторизации через updateAuth вместо прямого изменения await updateAuth({ authenticated: response.data.authenticated, authType: response.data.authType, userId: response.data.userId, address: response.data.address, telegramId: response.data.telegramId, email: response.data.email, userAccessLevel: response.data.userAccessLevel, // Добавляем userAccessLevel из ответа сервера }); } else { console.log('[checkAuth] No authentication changes, skipping update'); } // Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения if (response.data.authenticated) { // Сначала обновляем идентификаторы, чтобы иметь актуальные данные try { await updateIdentities(); } catch (error) { console.error('Error updating identities in checkAuth:', error); } // Если пользователь только что аутентифицировался или сменил аккаунт, // связываем гостевые сообщения с его аккаунтом if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) { // Немедленно связываем сообщения const linkResult = await linkMessages(); console.log('Link messages result on auth change:', linkResult); // Если пользователь только что аутентифицировался через Telegram, // обновляем историю чата без перезагрузки страницы if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') { console.log('Telegram auth detected, loading message history'); // Отправляем событие для загрузки истории чата window.dispatchEvent(new CustomEvent('load-chat-history')); } } // Обновляем отображение подключенного состояния в UI updateConnectionDisplay(true, response.data.authType, response.data); } else { // Обновляем отображение отключенного состояния updateConnectionDisplay(false); } return response.data; } catch (error) { console.error('Error checking auth:', error); // В случае ошибки сбрасываем состояние аутентификации updateConnectionDisplay(false); return { authenticated: false }; } }; const disconnect = async () => { try { // Удаляем все идентификаторы перед выходом await axios.post('/auth/logout'); // Обновляем состояние в памяти updateAuth({ authenticated: false, authType: null, userId: null, address: null, telegramId: null, email: null, }); // Обновляем отображение отключенного состояния updateConnectionDisplay(false); // Очищаем списки идентификаторов identities.value = []; processedGuestIds.value = []; // Очищаем localStorage полностью localStorage.removeItem('isAuthenticated'); localStorage.removeItem('userId'); localStorage.removeItem('address'); localStorage.removeItem('guestId'); localStorage.removeItem('guestMessages'); localStorage.removeItem('telegramId'); localStorage.removeItem('email'); // Удаляем класс подключенного кошелька document.body.classList.remove('wallet-connected'); console.log('User disconnected successfully and all identifiers cleared'); return { success: true }; } catch (error) { console.error('Error disconnecting:', error); return { success: false, error: error.message }; } }; // Обновляем список обработанных guestIds const updateProcessedGuestIds = (ids) => { if (Array.isArray(ids)) { processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])].slice(-20); } }; // Функция для обновления отображения подключения в UI const updateConnectionDisplay = (isConnected, authType, authData = {}) => { try { console.log('Updating connection display:', { isConnected, authType, authData }); if (isConnected) { document.body.classList.add('wallet-connected'); const authDisplayEl = document.getElementById('auth-display'); if (authDisplayEl) { let displayText = 'Подключено'; if (authType === 'wallet' && authData.address) { const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`; displayText = `Кошелек: ${shortAddress}`; } else if (authType === 'email' && authData.email) { displayText = `Email: ${authData.email}`; } else if (authType === 'telegram' && authData.telegramId) { displayText = `Telegram: ${authData.telegramUsername || authData.telegramId}`; } authDisplayEl.innerHTML = displayText; authDisplayEl.style.display = 'inline-block'; } // Скрываем кнопки авторизации и показываем кнопку выхода const authButtonsEl = document.getElementById('auth-buttons'); const logoutButtonEl = document.getElementById('logout-button'); if (authButtonsEl) authButtonsEl.style.display = 'none'; if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block'; } else { document.body.classList.remove('wallet-connected'); // Скрываем отображение аутентификации const authDisplayEl = document.getElementById('auth-display'); if (authDisplayEl) { authDisplayEl.style.display = 'none'; } // Показываем кнопки авторизации и скрываем кнопку выхода const authButtonsEl = document.getElementById('auth-buttons'); const logoutButtonEl = document.getElementById('logout-button'); if (authButtonsEl) authButtonsEl.style.display = 'flex'; if (logoutButtonEl) logoutButtonEl.style.display = 'none'; } } catch (error) { console.error('Error updating connection display:', error); } }; /** * Связывает новый идентификатор с текущим аккаунтом пользователя * @param {string} type - Тип идентификатора (wallet, email, telegram) * @param {string} value - Значение идентификатора * @returns {Promise} - Результат операции */ const linkIdentity = async (type, value) => { const response = await axios.post('/link', { type, value, }); return response.data; }; /** * Удаляет идентификатор пользователя * @param {string} provider - Тип идентификатора (wallet, email, telegram) * @param {string} providerId - Значение идентификатора * @returns {Promise} - Результат операции */ const deleteIdentity = async (provider, providerId) => { const response = await axios.delete(`/${provider}/${encodeURIComponent(providerId)}`); return response.data; }; // === SINGLETON API === const authApi = { isAuthenticated, authType, userId, address, telegramId, email, identities, processedGuestIds, tokenBalances, userAccessLevel, updateAuth, checkAuth, disconnect, linkMessages, updateIdentities, updateProcessedGuestIds, updateConnectionDisplay, linkIdentity, deleteIdentity, checkTokenBalances, checkUserAccessLevel, }; // === PROVIDE/INJECT HELPERS === const AUTH_KEY = Symbol('auth'); export function provideAuth() { provide(AUTH_KEY, authApi); } export function useAuthContext() { const ctx = inject(AUTH_KEY); if (!ctx) throw new Error('Auth context not provided!'); return ctx; } // === useAuth теперь просто возвращает singleton === export function useAuth() { onMounted(async () => { await checkAuth(); }); onUnmounted(() => { stopIdentitiesPolling(); }); return authApi; }