ваше сообщение коммита

This commit is contained in:
2025-04-21 16:52:32 +03:00
parent ab05988017
commit 4648aab7d5
20 changed files with 4206 additions and 4068 deletions

View File

@@ -1,7 +1,8 @@
import globals from 'globals'; import globals from 'globals';
import * as vueParser from 'vue-eslint-parser';
import vuePlugin from 'eslint-plugin-vue'; import vuePlugin from 'eslint-plugin-vue';
import prettierPlugin from 'eslint-plugin-prettier'; import prettierPlugin from 'eslint-plugin-prettier';
import prettierConfig from '@vue/eslint-config-prettier'; import eslintConfigPrettier from 'eslint-config-prettier';
export default [ export default [
{ {
@@ -35,29 +36,37 @@ export default [
...globals.browser, ...globals.browser,
...globals.es2021, ...globals.es2021,
}, },
parser: vuePlugin.parser, parser: vueParser,
parserOptions: { parserOptions: {
ecmaFeatures: { sourceType: 'module',
jsx: true, ecmaVersion: 2022,
},
}, },
}, },
plugins: { plugins: {
vue: vuePlugin, vue: vuePlugin,
prettier: prettierPlugin, prettier: prettierPlugin,
}, },
processor: vuePlugin.processors['.vue'],
rules: { rules: {
...prettierConfig.rules, ...vuePlugin.configs.base.rules,
...vuePlugin.configs['vue3-essential'].rules,
...vuePlugin.configs['vue3-strongly-recommended'].rules,
...vuePlugin.configs['vue3-recommended'].rules,
...eslintConfigPrettier.rules,
'prettier/prettier': 'warn',
'vue/comment-directive': 'off',
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
'vue/no-unused-vars': 'warn', 'vue/no-unused-vars': 'warn',
'vue/html-self-closing': ['warn', { 'vue/no-v-html': 'off',
html: { 'vue/html-self-closing': [
void: 'always', 'warn',
normal: 'always', {
component: 'always' html: {
} void: 'always',
}], normal: 'always',
component: 'always',
},
},
],
'vue/component-name-in-template-casing': ['warn', 'PascalCase'], 'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
}, },
}, },

View File

@@ -1,137 +1,28 @@
<template> <template>
<div id="app"> <div id="app">
<router-view /> <div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner" />
</div>
<RouterView />
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref, provide, computed } from 'vue'; import { ref, watch } from 'vue';
import { useRouter } from 'vue-router'; import { RouterView } from 'vue-router';
import axios from 'axios'; import { useAuth } from './composables/useAuth';
import './assets/styles/home.css';
console.log('App.vue: Version with auth check loaded'); // Состояние загрузки
const isLoading = ref(false);
const router = useRouter(); // Использование composable для аутентификации
const { isAuthenticated } = useAuth();
// Создаем реактивное состояние с помощью ref watch(isAuthenticated, (newValue, oldValue) => {
const authState = ref({ if (newValue !== oldValue) {
isAuthenticated: false, console.log('Состояние аутентификации изменилось:', newValue);
userRole: null,
address: null
});
// Предоставляем состояние аутентификации всем компонентам
const auth = {
// Используем computed для реактивности
isAuthenticated: computed(() => authState.value.isAuthenticated),
userRole: computed(() => authState.value.userRole),
address: computed(() => authState.value.address),
async checkAuth() {
try {
const response = await axios.get('/api/auth/check');
console.log('Auth check response:', response.data);
authState.value = {
isAuthenticated: response.data.authenticated,
userRole: response.data.role,
address: response.data.address
};
console.log('Auth state updated:', authState.value);
} catch (error) {
console.error('Auth check failed:', error);
} }
}, });
async disconnect() {
try {
await axios.post('/api/auth/logout');
authState.value = {
isAuthenticated: false,
userRole: null,
address: null
};
} catch (error) {
console.error('Logout failed:', error);
}
}
};
provide('auth', auth);
onMounted(async () => {
await auth.checkAuth();
});
</script> </script>
<style>
body {
margin: 0;
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
background-color: #f5f5f5;
}
#app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
flex: 1;
padding: 1rem;
}
button {
cursor: pointer;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 4px;
font-weight: 500;
border: none;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@@ -15,15 +15,15 @@ const api = axios.create({
baseURL: getBaseUrl(), baseURL: getBaseUrl(),
withCredentials: true, withCredentials: true,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} },
}); });
// Перехватчик запросов // Перехватчик запросов
api.interceptors.request.use( api.interceptors.request.use(
(config) => { (config) => {
config.withCredentials = true; // Важно для каждого запроса config.withCredentials = true; // Важно для каждого запроса
return config; return config;
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)
@@ -50,11 +50,11 @@ const sendGuestMessageToServer = async (messageText) => {
try { try {
await axios.post('/api/chat/guest-message', { await axios.post('/api/chat/guest-message', {
message: messageText, message: messageText,
language: userLanguage.value // language: userLanguage.value, // TODO: Реализовать получение языка пользователя
}); });
} catch (error) { } catch (error) {
console.error('Ошибка при отправке гостевого сообщения на сервер:', error); console.error('Ошибка при отправке гостевого сообщения на сервер:', error);
} }
}; };
export default api; export default api;

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<div class="conversation-list"> <div class="conversation-list">
<div class="list-header"> <div class="list-header">
<h3>Диалоги</h3> <h3>Диалоги</h3>
<button @click="createNewConversation" class="new-conversation-btn"> <button class="new-conversation-btn" @click="createNewConversation">
<span>+</span> Новый диалог <span>+</span> Новый диалог
</button> </button>
</div> </div>
@@ -40,205 +40,110 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed, defineEmits, watch, inject } from 'vue'; import { ref, onMounted, computed, defineEmits, watch, inject } from 'vue';
import axios from 'axios'; import axios from 'axios';
const emit = defineEmits(['select-conversation']); const emit = defineEmits(['select-conversation']);
const auth = inject('auth'); const auth = inject('auth');
const isAuthenticated = computed(() => auth.isAuthenticated.value); const isAuthenticated = computed(() => auth.isAuthenticated.value);
const conversations = ref([]); const conversations = ref([]);
const loading = ref(true); const loading = ref(true);
const selectedConversationId = ref(null); const selectedConversationId = ref(null);
// Следим за изменением статуса аутентификации // Следим за изменением статуса аутентификации
watch(() => isAuthenticated.value, (authenticated) => { watch(
if (!authenticated) { () => isAuthenticated.value,
conversations.value = []; // Очищаем список бесед при отключении (authenticated) => {
selectedConversationId.value = null; if (!authenticated) {
} conversations.value = []; // Очищаем список бесед при отключении
}); selectedConversationId.value = null;
}
// Загрузка списка диалогов
const fetchConversations = async () => {
try {
loading.value = true;
const response = await axios.get('/api/messages/conversations');
conversations.value = response.data;
// Если есть диалоги и не выбран ни один, выбираем первый
if (conversations.value.length > 0 && !selectedConversationId.value) {
selectConversation(conversations.value[0].conversation_id);
} }
} catch (error) { );
console.error('Error fetching conversations:', error);
} finally {
loading.value = false;
}
};
// Выбор диалога // Загрузка списка диалогов
const selectConversation = (conversationId) => { const fetchConversations = async () => {
selectedConversationId.value = conversationId; try {
emit('select-conversation', conversationId); loading.value = true;
}; const response = await axios.get('/api/messages/conversations');
conversations.value = response.data;
// Создание нового диалога // Если есть диалоги и не выбран ни один, выбираем первый
const createNewConversation = async () => { if (conversations.value.length > 0 && !selectedConversationId.value) {
try { selectConversation(conversations.value[0].conversation_id);
const response = await axios.post('/api/messages/conversations', { }
title: 'Новый диалог', } catch (error) {
}); console.error('Error fetching conversations:', error);
} finally {
loading.value = false;
}
};
// Добавляем новый диалог в список // Выбор диалога
const newConversation = { const selectConversation = (conversationId) => {
conversation_id: response.data.id, selectedConversationId.value = conversationId;
title: response.data.title, emit('select-conversation', conversationId);
username: authStore.username, };
address: authStore.address,
message_count: 0,
last_activity: response.data.created_at,
created_at: response.data.created_at,
};
conversations.value.unshift(newConversation); // Создание нового диалога
const createNewConversation = async () => {
try {
const response = await axios.post('/api/messages/conversations', {
title: 'Новый диалог',
});
// Выбираем новый диалог // Добавляем новый диалог в список
selectConversation(newConversation.conversation_id); const newConversation = {
} catch (error) { conversation_id: response.data.id,
console.error('Error creating conversation:', error); title: response.data.title,
} username: authStore.username,
}; address: authStore.address,
message_count: 0,
last_activity: response.data.created_at,
created_at: response.data.created_at,
};
// Форматирование времени conversations.value.unshift(newConversation);
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp); // Выбираем новый диалог
const now = new Date(); selectConversation(newConversation.conversation_id);
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); } catch (error) {
console.error('Error creating conversation:', error);
}
};
if (diffDays === 0) { // Форматирование времени
// Сегодня - показываем только время const formatTime = (timestamp) => {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (!timestamp) return '';
} else if (diffDays === 1) {
// Вчера
return 'Вчера';
} else if (diffDays < 7) {
// В течение недели - показываем день недели
const days = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
return days[date.getDay()];
} else {
// Более недели назад - показываем дату
return date.toLocaleDateString();
}
};
// Загрузка диалогов при монтировании компонента const date = new Date(timestamp);
onMounted(() => { const now = new Date();
fetchConversations(); const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
});
// Экспорт методов для использования в родительском компоненте if (diffDays === 0) {
defineExpose({ // Сегодня - показываем только время
fetchConversations, return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}); } else if (diffDays === 1) {
// Вчера
return 'Вчера';
} else if (diffDays < 7) {
// В течение недели - показываем день недели
const days = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
return days[date.getDay()];
} else {
// Более недели назад - показываем дату
return date.toLocaleDateString();
}
};
// Загрузка диалогов при монтировании компонента
onMounted(() => {
fetchConversations();
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchConversations,
});
</script> </script>
<style scoped>
.conversation-list {
display: flex;
flex-direction: column;
width: 300px;
border-right: 1px solid #e0e0e0;
background-color: #f9f9f9;
height: 100%;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.list-header h3 {
margin: 0;
font-size: 1.2rem;
}
.new-conversation-btn {
display: flex;
align-items: center;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 0.75rem;
cursor: pointer;
font-size: 0.9rem;
}
.new-conversation-btn span {
font-size: 1.2rem;
margin-right: 0.25rem;
}
.loading,
.empty-list {
padding: 2rem;
text-align: center;
color: #666;
}
.empty-list p {
margin: 0.5rem 0;
}
.conversations {
flex: 1;
overflow-y: auto;
}
.conversation-item {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
transition: background-color 0.2s;
}
.conversation-item:hover {
background-color: #f0f0f0;
}
.conversation-item.active {
background-color: #e8f5e9;
color: #4caf50;
}
.conversation-title {
font-weight: 500;
margin-bottom: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #888;
}
.time {
font-size: 0.8rem;
}
.connect-wallet-prompt {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -1,14 +1,14 @@
<template> <template>
<div class="message-input"> <div class="message-input">
<textarea <textarea
ref="textareaRef"
v-model="message" v-model="message"
placeholder="Введите сообщение..." placeholder="Введите сообщение..."
@keydown.enter.prevent="handleEnter"
ref="textareaRef"
:disabled="sending" :disabled="sending"
></textarea> @keydown.enter.prevent="handleEnter"
/>
<button @click="sendMessage" class="send-button" :disabled="!message.trim() || sending"> <button class="send-button" :disabled="!message.trim() || sending" @click="sendMessage">
<span v-if="sending">Отправка...</span> <span v-if="sending">Отправка...</span>
<span v-else>Отправить</span> <span v-else>Отправить</span>
</button> </button>
@@ -16,199 +16,149 @@
</template> </template>
<script setup> <script setup>
import { ref, defineEmits, nextTick } from 'vue'; import { ref, defineEmits, nextTick } from 'vue';
import axios from 'axios'; import axios from 'axios';
const props = defineProps({ const props = defineProps({
conversationId: { conversationId: {
type: [Number, String], type: [Number, String],
required: true, required: true,
}, },
}); });
const emit = defineEmits(['message-sent']); const emit = defineEmits(['message-sent']);
const message = ref(''); const message = ref('');
const sending = ref(false); const sending = ref(false);
const textareaRef = ref(null); const textareaRef = ref(null);
// Обработка нажатия Enter // Обработка нажатия Enter
const handleEnter = (event) => { const handleEnter = (event) => {
// Если нажат Shift+Enter, добавляем перенос строки // Если нажат Shift+Enter, добавляем перенос строки
if (event.shiftKey) { if (event.shiftKey) {
return; return;
} }
// Иначе отправляем сообщение // Иначе отправляем сообщение
sendMessage(); sendMessage();
};
// Отправка сообщения
const sendMessage = async () => {
const messageText = message.value.trim();
if (!messageText) return;
const userMessage = {
id: Date.now(),
content: messageText,
role: auth.isAuthenticated ? 'user' : 'guest',
timestamp: new Date().toISOString()
}; };
messages.value.push(userMessage); // Отправка сообщения
const sendMessage = async () => {
const messageText = message.value.trim();
if (!messageText) return;
try { const userMessage = {
// Логируем параметры запроса id: Date.now(),
console.log('Sending message to Ollama:', { content: messageText,
message: messageText, role: auth.isAuthenticated ? 'user' : 'guest',
language: userLanguage.value timestamp: new Date().toISOString(),
}); };
const response = await axios.post('/api/chat/message', { messages.value.push(userMessage);
message: messageText,
language: userLanguage.value
});
// Логируем ответ от Ollama try {
console.log('Response from Ollama:', response.data); // Логируем параметры запроса
console.log('Sending message to Ollama:', {
message: messageText,
language: userLanguage.value,
});
// Обработка ответа const response = await axios.post('/api/chat/message', {
messages.value.push({ message: messageText,
id: Date.now() + 1, language: userLanguage.value,
content: response.data.message, });
role: 'assistant',
timestamp: new Date().toISOString() // Логируем ответ от Ollama
}); console.log('Response from Ollama:', response.data);
// Обработка ответа
messages.value.push({
id: Date.now() + 1,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString(),
});
// Очищаем поле ввода
message.value = '';
// Фокусируемся на поле ввода
nextTick(() => {
textareaRef.value.focus();
});
// Уведомляем родительский компонент о новых сообщениях
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
} catch (error) {
console.error('Ошибка при отправке сообщения:', error);
} finally {
sending.value = false;
}
};
// Сброс поля ввода
const resetInput = () => {
message.value = '';
};
// Экспорт методов для использования в родительском компоненте
defineExpose({
resetInput,
focus: () => textareaRef.value?.focus(),
});
const sendGuestMessage = async (messageText) => {
if (!messageText.trim()) return;
const userMessage = {
id: Date.now(),
content: messageText,
role: 'user',
timestamp: new Date().toISOString(),
isGuest: true,
};
// Добавляем сообщение пользователя в локальную историю
messages.value.push(userMessage);
// Сохраняем сообщение в массиве гостевых сообщений
guestMessages.value.push(userMessage);
// Сохраняем гостевые сообщения в localStorage
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Очищаем поле ввода // Очищаем поле ввода
message.value = ''; newMessage.value = '';
// Фокусируемся на поле ввода // Прокрутка вниз
nextTick(() => { await nextTick();
textareaRef.value.focus(); if (messagesContainer.value) {
}); messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
// Уведомляем родительский компонент о новых сообщениях // Устанавливаем состояние загрузки
emit('message-sent', [response.data.userMessage, response.data.aiMessage]); isLoading.value = true;
} catch (error) {
console.error('Ошибка при отправке сообщения:', error);
} finally {
sending.value = false;
}
};
// Сброс поля ввода // Вместо отправки запроса к Ollama, отправляем сообщение с кнопками для аутентификации
const resetInput = () => { const authMessage = {
message.value = ''; id: Date.now() + 1,
}; content: 'Чтобы продолжить, пожалуйста, аутентифицируйтесь.',
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true,
showAuthOptions: true, // Указываем, что нужно показать кнопки аутентификации
};
// Экспорт методов для использования в родительском компоненте messages.value.push(authMessage);
defineExpose({ guestMessages.value.push(authMessage);
resetInput, localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
focus: () => textareaRef.value?.focus(),
});
const sendGuestMessage = async (messageText) => { // Прокрутка вниз
if (!messageText.trim()) return; await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
const userMessage = { isLoading.value = false;
id: Date.now(),
content: messageText,
role: 'user',
timestamp: new Date().toISOString(),
isGuest: true
}; };
// Добавляем сообщение пользователя в локальную историю
messages.value.push(userMessage);
// Сохраняем сообщение в массиве гостевых сообщений
guestMessages.value.push(userMessage);
// Сохраняем гостевые сообщения в localStorage
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Очищаем поле ввода
newMessage.value = '';
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
// Устанавливаем состояние загрузки
isLoading.value = true;
// Вместо отправки запроса к Ollama, отправляем сообщение с кнопками для аутентификации
const authMessage = {
id: Date.now() + 1,
content: 'Чтобы продолжить, пожалуйста, аутентифицируйтесь.',
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true,
showAuthOptions: true // Указываем, что нужно показать кнопки аутентификации
};
messages.value.push(authMessage);
guestMessages.value.push(authMessage);
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
isLoading.value = false;
};
</script> </script>
<style scoped>
.message-input {
display: flex;
padding: 1rem;
border-top: 1px solid #e0e0e0;
background-color: #fff;
}
textarea {
flex: 1;
min-height: 40px;
max-height: 120px;
padding: 0.75rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
resize: none;
font-family: inherit;
font-size: 0.9rem;
line-height: 1.4;
}
textarea:focus {
outline: none;
border-color: #4caf50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.send-button {
margin-left: 0.5rem;
padding: 0 1rem;
height: 40px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.send-button:hover:not(:disabled) {
background-color: #43a047;
}
.send-button:disabled {
background-color: #9e9e9e;
cursor: not-allowed;
}
</style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div v-if="isAuthenticated"> <div v-if="isAuthenticated">
<div class="message-thread" ref="threadContainer"> <div ref="threadContainer" class="message-thread">
<div v-if="loading" class="loading">Загрузка сообщений...</div> <div v-if="loading" class="loading">Загрузка сообщений...</div>
<div v-else-if="messages.length === 0" class="empty-thread"> <div v-else-if="messages.length === 0" class="empty-thread">
@@ -8,7 +8,11 @@
</div> </div>
<div v-else class="messages"> <div v-else class="messages">
<div v-for="message in messages" :key="message.id" :class="['message', message.sender_type]"> <div
v-for="message in messages"
:key="message.id"
:class="['message', message.sender_type]"
>
<div class="message-content">{{ message.content }}</div> <div class="message-content">{{ message.content }}</div>
<div class="message-meta"> <div class="message-meta">
<span class="time">{{ formatTime(message.created_at) }}</span> <span class="time">{{ formatTime(message.created_at) }}</span>
@@ -26,188 +30,110 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue'; import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
import axios from 'axios'; import axios from 'axios';
const props = defineProps({ const props = defineProps({
conversationId: { conversationId: {
type: [Number, String], type: [Number, String],
required: true, required: true,
}, },
});
const messages = ref([]);
const loading = ref(true);
const threadContainer = ref(null);
const isAuthenticated = ref(false);
// Загрузка сообщений диалога
const fetchMessages = async () => {
try {
loading.value = true;
const response = await axios.get(
`/api/messages/conversations/${props.conversationId}/messages`
);
messages.value = response.data;
// Прокрутка к последнему сообщению
await nextTick();
scrollToBottom();
} catch (error) {
console.error('Error fetching messages:', error);
} finally {
loading.value = false;
}
};
// Добавление новых сообщений
const addMessages = (newMessages) => {
if (Array.isArray(newMessages)) {
messages.value = [...messages.value, ...newMessages];
} else {
messages.value.push(newMessages);
}
// Прокрутка к последнему сообщению
nextTick(() => {
scrollToBottom();
}); });
};
// Прокрутка к последнему сообщению const messages = ref([]);
const scrollToBottom = () => { const loading = ref(true);
if (threadContainer.value) { const threadContainer = ref(null);
threadContainer.value.scrollTop = threadContainer.value.scrollHeight; const isAuthenticated = ref(false);
}
};
// Форматирование времени // Загрузка сообщений диалога
const formatTime = (timestamp) => { const fetchMessages = async () => {
if (!timestamp) return ''; try {
loading.value = true;
const response = await axios.get(
`/api/messages/conversations/${props.conversationId}/messages`
);
messages.value = response.data;
const date = new Date(timestamp); // Прокрутка к последнему сообщению
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); await nextTick();
}; scrollToBottom();
} catch (error) {
// Получение названия канала console.error('Error fetching messages:', error);
const channelName = (channel) => { } finally {
const channels = { loading.value = false;
web: 'Веб', }
telegram: 'Telegram',
email: 'Email',
}; };
return channels[channel] || channel; // Добавление новых сообщений
}; const addMessages = (newMessages) => {
if (Array.isArray(newMessages)) {
messages.value = [...messages.value, ...newMessages];
} else {
messages.value.push(newMessages);
}
// Наблюдение за изменением ID диалога // Прокрутка к последнему сообщению
watch( nextTick(() => {
() => props.conversationId, scrollToBottom();
(newId, oldId) => { });
if (newId && newId !== oldId) { };
// Прокрутка к последнему сообщению
const scrollToBottom = () => {
if (threadContainer.value) {
threadContainer.value.scrollTop = threadContainer.value.scrollHeight;
}
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
// Получение названия канала
const channelName = (channel) => {
const channels = {
web: 'Веб',
telegram: 'Telegram',
email: 'Email',
};
return channels[channel] || channel;
};
// Наблюдение за изменением ID диалога
watch(
() => props.conversationId,
(newId, oldId) => {
if (newId && newId !== oldId) {
fetchMessages();
}
}
);
// Следим за изменением статуса аутентификации
watch(
() => isAuthenticated.value,
(authenticated) => {
if (!authenticated) {
messages.value = []; // Очищаем сообщения при отключении
}
}
);
// Загрузка сообщений при монтировании компонента
onMounted(() => {
if (props.conversationId) {
fetchMessages(); fetchMessages();
} }
} });
);
// Следим за изменением статуса аутентификации // Экспорт методов для использования в родительском компоненте
watch(() => isAuthenticated.value, (authenticated) => { defineExpose({
if (!authenticated) { fetchMessages,
messages.value = []; // Очищаем сообщения при отключении addMessages,
} });
});
// Загрузка сообщений при монтировании компонента
onMounted(() => {
if (props.conversationId) {
fetchMessages();
}
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchMessages,
addMessages,
});
</script> </script>
<style scoped>
.message-thread {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
}
.loading,
.empty-thread {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #666;
}
.messages {
display: flex;
flex-direction: column;
gap: 1rem;
}
.message {
max-width: 80%;
padding: 0.75rem 1rem;
border-radius: 8px;
position: relative;
}
.message.user {
align-self: flex-end;
background-color: #e3f2fd;
border: 1px solid #bbdefb;
}
.message.ai {
align-self: flex-start;
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
}
.message.admin {
align-self: flex-start;
background-color: #fff3e0;
border: 1px dashed #ffb74d;
}
.message-content {
white-space: pre-wrap;
word-break: break-word;
}
.message-meta {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.25rem;
font-size: 0.7rem;
color: #999;
}
.time {
font-size: 0.7rem;
}
.channel {
font-size: 0.7rem;
padding: 0.1rem 0.3rem;
border-radius: 3px;
background-color: #f0f0f0;
}
.connect-wallet-prompt {
text-align: center;
padding: 2rem;
color: #666;
}
</style>

View File

@@ -1,173 +1,90 @@
<template> <template>
<div class="email-connection"> <div class="email-connection">
<div v-if="!showVerification" class="email-form"> <div v-if="!showVerification" class="email-form">
<input <input v-model="email" type="email" placeholder="Введите email" class="email-input" />
v-model="email" <button :disabled="isLoading || !isValidEmail" class="email-btn" @click="requestCode">
type="email"
placeholder="Введите email"
class="email-input"
/>
<button
@click="requestCode"
:disabled="isLoading || !isValidEmail"
class="email-btn"
>
{{ isLoading ? 'Отправка...' : 'Получить код' }} {{ isLoading ? 'Отправка...' : 'Получить код' }}
</button> </button>
</div> </div>
<div v-else class="verification-form"> <div v-else class="verification-form">
<p class="verification-info">Код отправлен на {{ email }}</p> <p class="verification-info">Код отправлен на {{ email }}</p>
<input <input v-model="code" type="text" placeholder="Введите код" class="code-input" />
v-model="code" <button :disabled="isLoading || !code" class="verify-btn" @click="verifyCode">
type="text"
placeholder="Введите код"
class="code-input"
/>
<button
@click="verifyCode"
:disabled="isLoading || !code"
class="verify-btn"
>
{{ isLoading ? 'Проверка...' : 'Подтвердить' }} {{ isLoading ? 'Проверка...' : 'Подтвердить' }}
</button> </button>
<button <button class="reset-btn" @click="resetForm">Изменить email</button>
@click="resetForm"
class="reset-btn"
>
Изменить email
</button>
</div> </div>
<div v-if="error" class="error">{{ error }}</div> <div v-if="error" class="error">{{ error }}</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import axios from '@/api/axios'; import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth'; import { useAuth } from '@/composables/useAuth';
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const { linkIdentity } = useAuth(); const { linkIdentity } = useAuth();
const email = ref(''); const email = ref('');
const code = ref(''); const code = ref('');
const error = ref(''); const error = ref('');
const isLoading = ref(false); const isLoading = ref(false);
const showVerification = ref(false); const showVerification = ref(false);
const isValidEmail = computed(() => { const isValidEmail = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
}); });
const requestCode = async () => { const requestCode = async () => {
try { try {
isLoading.value = true; isLoading.value = true;
error.value = ''; error.value = '';
const response = await axios.post('/api/auth/email/request-verification', { const response = await axios.post('/api/auth/email/request-verification', {
email: email.value email: email.value,
}); });
if (response.data.success) { if (response.data.success) {
showVerification.value = true; showVerification.value = true;
} else { } else {
error.value = response.data.error || 'Ошибка отправки кода'; error.value = response.data.error || 'Ошибка отправки кода';
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка отправки кода';
} finally {
isLoading.value = false;
} }
} catch (err) { };
error.value = err.response?.data?.error || 'Ошибка отправки кода';
} finally {
isLoading.value = false;
}
};
const verifyCode = async () => { const verifyCode = async () => {
try { try {
isLoading.value = true; isLoading.value = true;
error.value = ''; error.value = '';
const response = await axios.post('/api/auth/email/verify', { const response = await axios.post('/api/auth/email/verify', {
email: email.value, email: email.value,
code: code.value code: code.value,
}); });
if (response.data.success) { if (response.data.success) {
// Связываем email с текущим пользователем // Связываем email с текущим пользователем
await linkIdentity('email', email.value); await linkIdentity('email', email.value);
emit('close'); emit('close');
} else { } else {
error.value = response.data.error || 'Неверный код'; error.value = response.data.error || 'Неверный код';
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка проверки кода';
} finally {
isLoading.value = false;
} }
} catch (err) { };
error.value = err.response?.data?.error || 'Ошибка проверки кода';
} finally {
isLoading.value = false;
}
};
const resetForm = () => { const resetForm = () => {
email.value = ''; email.value = '';
code.value = ''; code.value = '';
error.value = ''; error.value = '';
showVerification.value = false; showVerification.value = false;
}; };
</script> </script>
<style scoped>
.email-connection {
padding: 20px;
max-width: 400px;
}
.email-form,
.verification-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.email-input,
.code-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.email-btn,
.verify-btn,
.reset-btn {
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.email-btn,
.verify-btn {
background-color: #48bb78;
color: white;
}
.reset-btn {
background-color: #e2e8f0;
color: #4a5568;
}
.verification-info {
color: #4a5568;
font-size: 14px;
}
.error {
color: #e53e3e;
margin-top: 5px;
font-size: 14px;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="telegram-connect"> <div class="telegram-connect">
<div v-if="!showQR" class="intro"> <div v-if="!showQR" class="intro">
<p>Подключите свой аккаунт Telegram для быстрой авторизации</p> <p>Подключите свой аккаунт Telegram для быстрой авторизации</p>
<button @click="startConnection" class="connect-button" :disabled="loading"> <button class="connect-button" :disabled="loading" @click="startConnection">
<span class="telegram-icon">📱</span> <span class="telegram-icon">📱</span>
{{ loading ? 'Загрузка...' : 'Подключить Telegram' }} {{ loading ? 'Загрузка...' : 'Подключить Telegram' }}
</button> </button>
@@ -10,14 +10,11 @@
<div v-else class="qr-section"> <div v-else class="qr-section">
<p>Отсканируйте QR-код в приложении Telegram</p> <p>Отсканируйте QR-код в приложении Telegram</p>
<div class="qr-container" v-html="qrCode"></div> <!-- eslint-disable-next-line vue/no-v-html -->
<div class="qr-container" v-html="qrCode" />
<p class="or-divider">или</p> <p class="or-divider">или</p>
<a :href="botLink" target="_blank" class="bot-link"> <a :href="botLink" target="_blank" class="bot-link"> Открыть бота в Telegram </a>
Открыть бота в Telegram <button class="reset-button" @click="resetConnection">Отмена</button>
</a>
<button @click="resetConnection" class="reset-button">
Отмена
</button>
</div> </div>
<div v-if="error" class="error">{{ error }}</div> <div v-if="error" class="error">{{ error }}</div>
@@ -25,183 +22,87 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import axios from '@/api/axios'; import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth'; import { useAuth } from '@/composables/useAuth';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const { linkIdentity } = useAuth(); const { linkIdentity } = useAuth();
const loading = ref(false); const loading = ref(false);
const error = ref(''); const error = ref('');
const showQR = ref(false); const showQR = ref(false);
const qrCode = ref(''); const qrCode = ref('');
const botLink = ref(''); const botLink = ref('');
const pollInterval = ref(null); const pollInterval = ref(null);
const connectionToken = ref(''); const connectionToken = ref('');
const startConnection = async () => { const startConnection = async () => {
try { try {
loading.value = true; loading.value = true;
error.value = '';
const response = await axios.post('/api/auth/telegram/start-connection');
if (response.data.success) {
connectionToken.value = response.data.token;
botLink.value = `https://t.me/${response.data.botUsername}?start=${connectionToken.value}`;
// Генерируем QR-код
const qr = await QRCode.toDataURL(botLink.value);
qrCode.value = `<img src="${qr}" alt="Telegram QR Code" />`;
showQR.value = true;
startPolling();
} else {
error.value = response.data.error || 'Не удалось начать процесс подключения';
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка при подключении Telegram';
} finally {
loading.value = false;
}
};
const checkConnection = async () => {
try {
const response = await axios.post('/api/auth/telegram/check-connection', {
token: connectionToken.value,
});
if (response.data.success && response.data.telegramId) {
// Связываем Telegram с текущим пользователем
await linkIdentity('telegram', response.data.telegramId);
stopPolling();
emit('close');
}
} catch (error) {
console.error('Error checking connection:', error);
}
};
const startPolling = () => {
pollInterval.value = setInterval(checkConnection, 2000);
};
const stopPolling = () => {
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
};
const resetConnection = () => {
stopPolling();
showQR.value = false;
error.value = ''; error.value = '';
qrCode.value = '';
const response = await axios.post('/api/auth/telegram/start-connection'); botLink.value = '';
connectionToken.value = '';
if (response.data.success) { };
connectionToken.value = response.data.token;
botLink.value = `https://t.me/${response.data.botUsername}?start=${connectionToken.value}`;
// Генерируем QR-код
const qr = await QRCode.toDataURL(botLink.value);
qrCode.value = `<img src="${qr}" alt="Telegram QR Code" />`;
showQR.value = true;
startPolling();
} else {
error.value = response.data.error || 'Не удалось начать процесс подключения';
}
} catch (err) {
error.value = err.response?.data?.error || 'Ошибка при подключении Telegram';
} finally {
loading.value = false;
}
};
const checkConnection = async () => { onUnmounted(() => {
try { stopPolling();
const response = await axios.post('/api/auth/telegram/check-connection', { });
token: connectionToken.value
});
if (response.data.success && response.data.telegramId) {
// Связываем Telegram с текущим пользователем
await linkIdentity('telegram', response.data.telegramId);
stopPolling();
emit('close');
}
} catch (error) {
console.error('Error checking connection:', error);
}
};
const startPolling = () => {
pollInterval.value = setInterval(checkConnection, 2000);
};
const stopPolling = () => {
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
};
const resetConnection = () => {
stopPolling();
showQR.value = false;
error.value = '';
qrCode.value = '';
botLink.value = '';
connectionToken.value = '';
};
onUnmounted(() => {
stopPolling();
});
</script> </script>
<style scoped>
.telegram-connect {
padding: 20px;
max-width: 400px;
}
.intro,
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
text-align: center;
}
.connect-button {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #0088cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
.connect-button:hover:not(:disabled) {
background-color: #0077b5;
}
.telegram-icon {
margin-right: 10px;
font-size: 18px;
}
.qr-container {
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.qr-container img {
max-width: 200px;
height: auto;
}
.or-divider {
color: #666;
margin: 10px 0;
}
.bot-link {
color: #0088cc;
text-decoration: none;
padding: 8px 16px;
border: 1px solid #0088cc;
border-radius: 4px;
transition: all 0.2s;
}
.bot-link:hover {
background-color: #0088cc;
color: white;
}
.reset-button {
padding: 8px 16px;
background-color: #e2e8f0;
color: #4a5568;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.reset-button:hover {
background-color: #cbd5e0;
}
.error {
color: #e53e3e;
margin-top: 10px;
text-align: center;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,11 +2,7 @@
<div class="wallet-connection"> <div class="wallet-connection">
<div v-if="!isConnected" class="connect-section"> <div v-if="!isConnected" class="connect-section">
<p>Подключите свой кошелек для доступа к расширенным функциям</p> <p>Подключите свой кошелек для доступа к расширенным функциям</p>
<button <button :disabled="isLoading" class="wallet-btn" @click="connectWallet">
@click="connectWallet"
:disabled="isLoading"
class="wallet-btn"
>
<span class="wallet-icon">💳</span> <span class="wallet-icon">💳</span>
{{ isLoading ? 'Подключение...' : 'Подключить кошелек' }} {{ isLoading ? 'Подключение...' : 'Подключить кошелек' }}
</button> </button>
@@ -20,106 +16,48 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useAuth } from '@/composables/useAuth'; import { useAuth } from '@/composables/useAuth';
import { connectWithWallet } from '@/services/wallet'; import { connectWithWallet } from '@/services/wallet';
const emit = defineEmits(['close']); const emit = defineEmits(['close']);
const { linkIdentity } = useAuth(); const { linkIdentity } = useAuth();
const isLoading = ref(false); const isLoading = ref(false);
const error = ref(''); const error = ref('');
const address = ref(''); const address = ref('');
const isConnected = computed(() => !!address.value); const isConnected = computed(() => !!address.value);
const formatAddress = (addr) => { const formatAddress = (addr) => {
if (!addr) return ''; if (!addr) return '';
return `${addr.slice(0, 6)}...${addr.slice(-4)}`; return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
}; };
const connectWallet = async () => { const connectWallet = async () => {
if (isLoading.value) return; if (isLoading.value) return;
try { try {
isLoading.value = true; isLoading.value = true;
error.value = ''; error.value = '';
// Подключаем кошелек // Подключаем кошелек
const result = await connectWithWallet(); const result = await connectWithWallet();
if (result.success) { if (result.success) {
address.value = result.address; address.value = result.address;
// Связываем кошелек с текущим пользователем // Связываем кошелек с текущим пользователем
await linkIdentity('wallet', result.address); await linkIdentity('wallet', result.address);
emit('close'); emit('close');
} else { } else {
error.value = result.error || 'Не удалось подключить кошелек'; error.value = result.error || 'Не удалось подключить кошелек';
}
} catch (err) {
console.error('Error connecting wallet:', err);
error.value = err.message || 'Произошла ошибка при подключении кошелька';
} finally {
isLoading.value = false;
} }
} catch (err) { };
console.error('Error connecting wallet:', err);
error.value = err.message || 'Произошла ошибка при подключении кошелька';
} finally {
isLoading.value = false;
}
};
</script> </script>
<style scoped>
.wallet-connection {
padding: 20px;
max-width: 400px;
}
.connect-section,
.status-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
text-align: center;
}
.wallet-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #4a5568;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s;
}
.wallet-btn:hover:not(:disabled) {
background-color: #2d3748;
}
.wallet-icon {
margin-right: 10px;
font-size: 18px;
}
.address {
font-family: monospace;
background-color: #f7fafc;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
}
.error {
color: #e53e3e;
margin-top: 10px;
text-align: center;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -2,8 +2,4 @@ import TelegramConnect from './TelegramConnect.vue';
import WalletConnection from './WalletConnection.vue'; import WalletConnection from './WalletConnection.vue';
import EmailConnect from './EmailConnect.vue'; import EmailConnect from './EmailConnect.vue';
export { export { TelegramConnect, WalletConnection, EmailConnect };
TelegramConnect,
WalletConnection,
EmailConnect
};

View File

@@ -12,26 +12,26 @@ export function useAuth() {
const processedGuestIds = ref([]); const processedGuestIds = ref([]);
const identities = ref([]); const identities = ref([]);
const tokenBalances = ref([]); const tokenBalances = ref([]);
// Функция для обновления списка идентификаторов // Функция для обновления списка идентификаторов
const updateIdentities = async () => { const updateIdentities = async () => {
if (!isAuthenticated.value || !userId.value) return; if (!isAuthenticated.value || !userId.value) return;
try { try {
const response = await axios.get('/api/auth/identities'); const response = await axios.get('/api/auth/identities');
if (response.data.success) { if (response.data.success) {
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные // Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
const filteredIdentities = response.data.identities const filteredIdentities = response.data.identities
.filter(identity => identity.provider !== 'guest') .filter((identity) => identity.provider !== 'guest')
.reduce((acc, identity) => { .reduce((acc, identity) => {
// Для каждого типа провайдера оставляем только один идентификатор // Для каждого типа провайдера оставляем только один идентификатор
const existingIdentity = acc.find(i => i.provider === identity.provider); const existingIdentity = acc.find((i) => i.provider === identity.provider);
if (!existingIdentity) { if (!existingIdentity) {
acc.push(identity); acc.push(identity);
} }
return acc; return acc;
}, []); }, []);
identities.value = filteredIdentities; identities.value = filteredIdentities;
console.log('User identities updated:', identities.value); console.log('User identities updated:', identities.value);
} }
@@ -39,7 +39,7 @@ export function useAuth() {
console.error('Error fetching user identities:', error); console.error('Error fetching user identities:', error);
} }
}; };
// Периодическое обновление идентификаторов // Периодическое обновление идентификаторов
let identitiesInterval; let identitiesInterval;
@@ -54,7 +54,7 @@ export function useAuth() {
identitiesInterval = null; identitiesInterval = null;
} }
}; };
const checkTokenBalances = async (address) => { const checkTokenBalances = async (address) => {
try { try {
const response = await axios.get(`/api/auth/check-tokens/${address}`); const response = await axios.get(`/api/auth/check-tokens/${address}`);
@@ -68,21 +68,29 @@ export function useAuth() {
return null; return null;
} }
}; };
const updateAuth = async ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin, email: newEmail }) => { const updateAuth = async ({
authenticated,
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
}) => {
const wasAuthenticated = isAuthenticated.value; const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value; const previousUserId = userId.value;
console.log('updateAuth called with:', { console.log('updateAuth called with:', {
authenticated, authenticated,
newAuthType, newAuthType,
newUserId, newUserId,
newAddress, newAddress,
newTelegramId, newTelegramId,
newIsAdmin, newIsAdmin,
newEmail newEmail,
}); });
// Убедимся, что переменные являются реактивными // Убедимся, что переменные являются реактивными
isAuthenticated.value = authenticated === true; isAuthenticated.value = authenticated === true;
authType.value = newAuthType || null; authType.value = newAuthType || null;
@@ -91,23 +99,26 @@ export function useAuth() {
telegramId.value = newTelegramId || null; telegramId.value = newTelegramId || null;
isAdmin.value = newIsAdmin === true; isAdmin.value = newIsAdmin === true;
email.value = newEmail || null; email.value = newEmail || null;
// Кэшируем данные аутентификации // Кэшируем данные аутентификации
localStorage.setItem('authData', JSON.stringify({ localStorage.setItem(
authenticated, 'authData',
authType: newAuthType, JSON.stringify({
userId: newUserId, authenticated,
address: newAddress, authType: newAuthType,
telegramId: newTelegramId, userId: newUserId,
isAdmin: newIsAdmin, address: newAddress,
email: newEmail telegramId: newTelegramId,
})); isAdmin: newIsAdmin,
email: newEmail,
})
);
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса // Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) { if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
await checkTokenBalances(newAddress); await checkTokenBalances(newAddress);
} }
// Обновляем идентификаторы при любом изменении аутентификации // Обновляем идентификаторы при любом изменении аутентификации
if (authenticated) { if (authenticated) {
await updateIdentities(); await updateIdentities();
@@ -116,63 +127,63 @@ export function useAuth() {
stopIdentitiesPolling(); stopIdentitiesPolling();
identities.value = []; identities.value = [];
} }
console.log('Auth updated:', { console.log('Auth updated:', {
authenticated: isAuthenticated.value, authenticated: isAuthenticated.value,
userId: userId.value, userId: userId.value,
address: address.value, address: address.value,
telegramId: telegramId.value, telegramId: telegramId.value,
email: email.value, email: email.value,
isAdmin: isAdmin.value isAdmin: isAdmin.value,
}); });
// Если пользователь только что аутентифицировался или сменил аккаунт, // Если пользователь только что аутентифицировался или сменил аккаунт,
// пробуем связать сообщения // пробуем связать сообщения
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) { if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
console.log('Auth change detected, linking messages'); console.log('Auth change detected, linking messages');
linkMessages(); linkMessages();
} }
}; };
// Функция для связывания сообщений после успешной авторизации // Функция для связывания сообщений после успешной авторизации
const linkMessages = async () => { const linkMessages = async () => {
try { try {
if (isAuthenticated.value) { if (isAuthenticated.value) {
console.log('Linking messages after authentication'); console.log('Linking messages after authentication');
// Проверка, есть ли гостевой ID для обработки // Проверка, есть ли гостевой ID для обработки
const localGuestId = localStorage.getItem('guestId'); const localGuestId = localStorage.getItem('guestId');
// Если гостевого ID нет или он уже был обработан, пропускаем запрос // Если гостевого ID нет или он уже был обработан, пропускаем запрос
if (!localGuestId || processedGuestIds.value.includes(localGuestId)) { if (!localGuestId || processedGuestIds.value.includes(localGuestId)) {
console.log('No new guest IDs to process or already processed'); console.log('No new guest IDs to process or already processed');
return { return {
success: true, success: true,
message: 'No new guest IDs to process', message: 'No new guest IDs to process',
processedIds: processedGuestIds.value processedIds: processedGuestIds.value,
}; };
} }
// Создаем объект с идентификаторами для передачи на сервер // Создаем объект с идентификаторами для передачи на сервер
const identifiersData = { const identifiersData = {
userId: userId.value, userId: userId.value,
guestId: localGuestId guestId: localGuestId,
}; };
// Добавляем все доступные идентификаторы // Добавляем все доступные идентификаторы
if (address.value) identifiersData.address = address.value; if (address.value) identifiersData.address = address.value;
if (email.value) identifiersData.email = email.value; if (email.value) identifiersData.email = email.value;
if (telegramId.value) identifiersData.telegramId = telegramId.value; if (telegramId.value) identifiersData.telegramId = telegramId.value;
console.log('Sending link-guest-messages request with data:', identifiersData); console.log('Sending link-guest-messages request with data:', identifiersData);
try { try {
// Отправляем запрос на связывание сообщений // Отправляем запрос на связывание сообщений
const response = await axios.post('/api/auth/link-guest-messages', identifiersData); const response = await axios.post('/api/auth/link-guest-messages', identifiersData);
if (response.data.success) { if (response.data.success) {
console.log('Messages linked successfully:', response.data); console.log('Messages linked successfully:', response.data);
// Обновляем список обработанных guestIds из ответа сервера // Обновляем список обработанных guestIds из ответа сервера
if (response.data.processedIds && Array.isArray(response.data.processedIds)) { if (response.data.processedIds && Array.isArray(response.data.processedIds)) {
processedGuestIds.value = [...response.data.processedIds]; processedGuestIds.value = [...response.data.processedIds];
@@ -181,49 +192,51 @@ export function useAuth() {
// В качестве запасного варианта также обрабатываем старый формат ответа // В качестве запасного варианта также обрабатываем старый формат ответа
else if (response.data.results && Array.isArray(response.data.results)) { else if (response.data.results && Array.isArray(response.data.results)) {
const newProcessedIds = response.data.results const newProcessedIds = response.data.results
.filter(result => result.guestId) .filter((result) => result.guestId)
.map(result => result.guestId); .map((result) => result.guestId);
if (newProcessedIds.length > 0) { if (newProcessedIds.length > 0) {
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...newProcessedIds])]; processedGuestIds.value = [
...new Set([...processedGuestIds.value, ...newProcessedIds]),
];
console.log('Updated processed guest IDs from results:', processedGuestIds.value); console.log('Updated processed guest IDs from results:', processedGuestIds.value);
} }
} }
// Очищаем гостевые сообщения из localStorage после успешного связывания // Очищаем гостевые сообщения из localStorage после успешного связывания
localStorage.removeItem('guestMessages'); localStorage.removeItem('guestMessages');
localStorage.removeItem('guestId'); localStorage.removeItem('guestId');
return { return {
success: true, success: true,
processedIds: processedGuestIds.value processedIds: processedGuestIds.value,
}; };
} }
} catch (error) { } catch (error) {
console.error('Error linking messages:', error); console.error('Error linking messages:', error);
return { return {
success: false, success: false,
error: error.message error: error.message,
}; };
} }
} }
return { success: false, message: 'Not authenticated' }; return { success: false, message: 'Not authenticated' };
} catch (error) { } catch (error) {
console.error('Error in linkMessages:', error); console.error('Error in linkMessages:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}; };
const checkAuth = async () => { const checkAuth = async () => {
try { try {
const response = await axios.get('/api/auth/check'); const response = await axios.get('/api/auth/check');
console.log('Auth check response:', response.data); console.log('Auth check response:', response.data);
const wasAuthenticated = isAuthenticated.value; const wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value; const previousUserId = userId.value;
const previousAuthType = authType.value; const previousAuthType = authType.value;
// Обновляем данные авторизации через updateAuth вместо прямого изменения // Обновляем данные авторизации через updateAuth вместо прямого изменения
await updateAuth({ await updateAuth({
authenticated: response.data.authenticated, authenticated: response.data.authenticated,
@@ -232,21 +245,21 @@ export function useAuth() {
address: response.data.address, address: response.data.address,
telegramId: response.data.telegramId, telegramId: response.data.telegramId,
email: response.data.email, email: response.data.email,
isAdmin: response.data.isAdmin isAdmin: response.data.isAdmin,
}); });
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения // Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
if (response.data.authenticated) { if (response.data.authenticated) {
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные // Сначала обновляем идентификаторы, чтобы иметь актуальные данные
await updateIdentities(); await updateIdentities();
// Если пользователь только что аутентифицировался или сменил аккаунт, // Если пользователь только что аутентифицировался или сменил аккаунт,
// связываем гостевые сообщения с его аккаунтом // связываем гостевые сообщения с его аккаунтом
if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) { if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) {
// Немедленно связываем сообщения // Немедленно связываем сообщения
const linkResult = await linkMessages(); const linkResult = await linkMessages();
console.log('Link messages result on auth change:', linkResult); console.log('Link messages result on auth change:', linkResult);
// Если пользователь только что аутентифицировался через Telegram, // Если пользователь только что аутентифицировался через Telegram,
// обновляем историю чата без перезагрузки страницы // обновляем историю чата без перезагрузки страницы
if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') { if (response.data.authType === 'telegram' && previousAuthType !== 'telegram') {
@@ -255,14 +268,14 @@ export function useAuth() {
window.dispatchEvent(new CustomEvent('load-chat-history')); window.dispatchEvent(new CustomEvent('load-chat-history'));
} }
} }
// Обновляем отображение подключенного состояния в UI // Обновляем отображение подключенного состояния в UI
updateConnectionDisplay(true, response.data.authType, response.data); updateConnectionDisplay(true, response.data.authType, response.data);
} else { } else {
// Обновляем отображение отключенного состояния // Обновляем отображение отключенного состояния
updateConnectionDisplay(false); updateConnectionDisplay(false);
} }
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Error checking auth:', error); console.error('Error checking auth:', error);
@@ -271,30 +284,30 @@ export function useAuth() {
return { authenticated: false }; return { authenticated: false };
} }
}; };
const disconnect = async () => { const disconnect = async () => {
try { try {
// Удаляем все идентификаторы перед выходом // Удаляем все идентификаторы перед выходом
await axios.post('/api/auth/logout'); await axios.post('/api/auth/logout');
// Обновляем состояние в памяти // Обновляем состояние в памяти
updateAuth({ updateAuth({
authenticated: false, authenticated: false,
authType: null, authType: null,
userId: null, userId: null,
address: null, address: null,
telegramId: null, telegramId: null,
email: null, email: null,
isAdmin: false isAdmin: false,
}); });
// Обновляем отображение отключенного состояния // Обновляем отображение отключенного состояния
updateConnectionDisplay(false); updateConnectionDisplay(false);
// Очищаем списки идентификаторов // Очищаем списки идентификаторов
identities.value = []; identities.value = [];
processedGuestIds.value = []; processedGuestIds.value = [];
// Очищаем localStorage полностью // Очищаем localStorage полностью
localStorage.removeItem('isAuthenticated'); localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userId'); localStorage.removeItem('userId');
@@ -304,38 +317,38 @@ export function useAuth() {
localStorage.removeItem('guestMessages'); localStorage.removeItem('guestMessages');
localStorage.removeItem('telegramId'); localStorage.removeItem('telegramId');
localStorage.removeItem('email'); localStorage.removeItem('email');
// Удаляем класс подключенного кошелька // Удаляем класс подключенного кошелька
document.body.classList.remove('wallet-connected'); document.body.classList.remove('wallet-connected');
console.log('User disconnected successfully and all identifiers cleared'); console.log('User disconnected successfully and all identifiers cleared');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error disconnecting:', error); console.error('Error disconnecting:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}; };
// Обновляем список обработанных guestIds // Обновляем список обработанных guestIds
const updateProcessedGuestIds = (ids) => { const updateProcessedGuestIds = (ids) => {
if (Array.isArray(ids)) { if (Array.isArray(ids)) {
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])]; processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])];
} }
}; };
// Функция для обновления отображения подключения в UI // Функция для обновления отображения подключения в UI
const updateConnectionDisplay = (isConnected, authType, authData = {}) => { const updateConnectionDisplay = (isConnected, authType, authData = {}) => {
try { try {
console.log('Updating connection display:', { isConnected, authType, authData }); console.log('Updating connection display:', { isConnected, authType, authData });
if (isConnected) { if (isConnected) {
document.body.classList.add('wallet-connected'); document.body.classList.add('wallet-connected');
const authDisplayEl = document.getElementById('auth-display'); const authDisplayEl = document.getElementById('auth-display');
if (authDisplayEl) { if (authDisplayEl) {
let displayText = 'Подключено'; let displayText = 'Подключено';
if (authType === 'wallet' && authData.address) { if (authType === 'wallet' && authData.address) {
const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`; const shortAddress = `${authData.address.substring(0, 6)}...${authData.address.substring(authData.address.length - 4)}`;
displayText = `Кошелек: <strong>${shortAddress}</strong>`; displayText = `Кошелек: <strong>${shortAddress}</strong>`;
@@ -344,30 +357,30 @@ export function useAuth() {
} else if (authType === 'telegram' && authData.telegramId) { } else if (authType === 'telegram' && authData.telegramId) {
displayText = `Telegram: <strong>${authData.telegramUsername || authData.telegramId}</strong>`; displayText = `Telegram: <strong>${authData.telegramUsername || authData.telegramId}</strong>`;
} }
authDisplayEl.innerHTML = displayText; authDisplayEl.innerHTML = displayText;
authDisplayEl.style.display = 'inline-block'; authDisplayEl.style.display = 'inline-block';
} }
// Скрываем кнопки авторизации и показываем кнопку выхода // Скрываем кнопки авторизации и показываем кнопку выхода
const authButtonsEl = document.getElementById('auth-buttons'); const authButtonsEl = document.getElementById('auth-buttons');
const logoutButtonEl = document.getElementById('logout-button'); const logoutButtonEl = document.getElementById('logout-button');
if (authButtonsEl) authButtonsEl.style.display = 'none'; if (authButtonsEl) authButtonsEl.style.display = 'none';
if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block'; if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block';
} else { } else {
document.body.classList.remove('wallet-connected'); document.body.classList.remove('wallet-connected');
// Скрываем отображение аутентификации // Скрываем отображение аутентификации
const authDisplayEl = document.getElementById('auth-display'); const authDisplayEl = document.getElementById('auth-display');
if (authDisplayEl) { if (authDisplayEl) {
authDisplayEl.style.display = 'none'; authDisplayEl.style.display = 'none';
} }
// Показываем кнопки авторизации и скрываем кнопку выхода // Показываем кнопки авторизации и скрываем кнопку выхода
const authButtonsEl = document.getElementById('auth-buttons'); const authButtonsEl = document.getElementById('auth-buttons');
const logoutButtonEl = document.getElementById('logout-button'); const logoutButtonEl = document.getElementById('logout-button');
if (authButtonsEl) authButtonsEl.style.display = 'flex'; if (authButtonsEl) authButtonsEl.style.display = 'flex';
if (logoutButtonEl) logoutButtonEl.style.display = 'none'; if (logoutButtonEl) logoutButtonEl.style.display = 'none';
} }
@@ -375,7 +388,7 @@ export function useAuth() {
console.error('Error updating connection display:', error); console.error('Error updating connection display:', error);
} }
}; };
onMounted(async () => { onMounted(async () => {
await checkAuth(); await checkAuth();
}); });
@@ -384,7 +397,7 @@ export function useAuth() {
onUnmounted(() => { onUnmounted(() => {
stopIdentitiesPolling(); stopIdentitiesPolling();
}); });
/** /**
* Связывает новый идентификатор с текущим аккаунтом пользователя * Связывает новый идентификатор с текущим аккаунтом пользователя
* @param {string} provider - Тип идентификатора (wallet, email, telegram) * @param {string} provider - Тип идентификатора (wallet, email, telegram)
@@ -397,12 +410,12 @@ export function useAuth() {
console.error('Невозможно связать идентификатор: пользователь не аутентифицирован'); console.error('Невозможно связать идентификатор: пользователь не аутентифицирован');
return { success: false, error: 'Пользователь не аутентифицирован' }; return { success: false, error: 'Пользователь не аутентифицирован' };
} }
const response = await axios.post('/api/auth/identities/link', { const response = await axios.post('/api/auth/identities/link', {
type: provider, type: provider,
value: providerId value: providerId,
}); });
if (response.data.success) { if (response.data.success) {
// Обновляем локальные данные при необходимости // Обновляем локальные данные при необходимости
if (provider === 'wallet') { if (provider === 'wallet') {
@@ -413,24 +426,24 @@ export function useAuth() {
} else if (provider === 'email') { } else if (provider === 'email') {
email.value = providerId; email.value = providerId;
} }
// Обновляем список идентификаторов // Обновляем список идентификаторов
await updateIdentities(); await updateIdentities();
console.log(`Идентификатор ${provider} успешно связан с аккаунтом`); console.log(`Идентификатор ${provider} успешно связан с аккаунтом`);
return { success: true }; return { success: true };
} }
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error('Ошибка при связывании идентификатора:', error); console.error('Ошибка при связывании идентификатора:', error);
return { return {
success: false, success: false,
error: error.response?.data?.error || error.message error: error.response?.data?.error || error.message,
}; };
} }
}; };
return { return {
isAuthenticated, isAuthenticated,
authType, authType,
@@ -449,6 +462,6 @@ export function useAuth() {
updateIdentities, updateIdentities,
updateProcessedGuestIds, updateProcessedGuestIds,
updateConnectionDisplay, updateConnectionDisplay,
linkIdentity linkIdentity,
}; };
} }

View File

@@ -8,7 +8,8 @@ import axios from 'axios';
// Настройка axios // Настройка axios
// В Docker контейнере localhost:8000 не работает, поэтому используем явное значение // В Docker контейнере localhost:8000 не работает, поэтому используем явное значение
const apiUrl = window.location.hostname === 'localhost' ? 'http://localhost:8000' : import.meta.env.VITE_API_URL; const apiUrl =
window.location.hostname === 'localhost' ? 'http://localhost:8000' : import.meta.env.VITE_API_URL;
axios.defaults.baseURL = apiUrl; axios.defaults.baseURL = apiUrl;
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;

View File

@@ -8,13 +8,13 @@ const routes = [
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: HomeView component: HomeView,
} },
]; ];
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes routes,
}); });
console.log('router/index.js: Router created'); console.log('router/index.js: Router created');
@@ -25,9 +25,9 @@ router.beforeEach(async (to, from, next) => {
if (!to.matched.length) { if (!to.matched.length) {
return next({ name: 'home' }); return next({ name: 'home' });
} }
// Проверяем аутентификацию, если маршрут требует авторизации // Проверяем аутентификацию, если маршрут требует авторизации
if (to.matched.some(record => record.meta.requiresAuth)) { if (to.matched.some((record) => record.meta.requiresAuth)) {
try { try {
const response = await axios.get('/api/auth/check'); const response = await axios.get('/api/auth/check');
if (response.data.authenticated) { if (response.data.authenticated) {

View File

@@ -3,25 +3,25 @@ import api from '../api/axios';
// Адреса смарт-контрактов токенов HB3A // Адреса смарт-контрактов токенов HB3A
export const TOKEN_CONTRACTS = { export const TOKEN_CONTRACTS = {
eth: { eth: {
address: "0xd95a45fc46a7300e6022885afec3d618d7d3f27c", address: '0xd95a45fc46a7300e6022885afec3d618d7d3f27c',
symbol: "HB3A", symbol: 'HB3A',
network: "Ethereum" network: 'Ethereum',
}, },
bsc: { bsc: {
address: "0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D", address: '0x1d47f12ffA279BFE59Ab16d56fBb10d89AECdD5D',
symbol: "HB3A", symbol: 'HB3A',
network: "BSC" network: 'BSC',
}, },
arbitrum: { arbitrum: {
address: "0xdce769b847a0a697239777d0b1c7dd33b6012ba0", address: '0xdce769b847a0a697239777d0b1c7dd33b6012ba0',
symbol: "HB3A", symbol: 'HB3A',
network: "Arbitrum" network: 'Arbitrum',
}, },
polygon: { polygon: {
address: "0x351f59de4fedbdf7601f5592b93db3b9330c1c1d", address: '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d',
symbol: "HB3A", symbol: 'HB3A',
network: "Polygon" network: 'Polygon',
} },
}; };
// Получение балансов токенов // Получение балансов токенов
@@ -35,7 +35,7 @@ export const fetchTokenBalances = async () => {
eth: '0', eth: '0',
bsc: '0', bsc: '0',
arbitrum: '0', arbitrum: '0',
polygon: '0' polygon: '0',
}; };
} }
}; };

View File

@@ -4,39 +4,39 @@ import { SiweMessage } from 'siwe';
export async function connectWithWallet() { export async function connectWithWallet() {
console.log('Starting wallet connection...'); console.log('Starting wallet connection...');
try { try {
// Проверяем наличие MetaMask // Проверяем наличие MetaMask
if (!window.ethereum) { if (!window.ethereum) {
throw new Error('MetaMask not detected. Please install MetaMask.'); throw new Error('MetaMask not detected. Please install MetaMask.');
} }
console.log('MetaMask detected, requesting accounts...'); console.log('MetaMask detected, requesting accounts...');
// Запрашиваем доступ к аккаунтам // Запрашиваем доступ к аккаунтам
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
console.log('Got accounts:', accounts); console.log('Got accounts:', accounts);
if (!accounts || accounts.length === 0) { if (!accounts || accounts.length === 0) {
throw new Error('No accounts found. Please unlock MetaMask.'); throw new Error('No accounts found. Please unlock MetaMask.');
} }
// Берем первый аккаунт // Берем первый аккаунт
const address = ethers.getAddress(accounts[0]); const address = ethers.getAddress(accounts[0]);
console.log('Normalized address:', address); console.log('Normalized address:', address);
// Запрашиваем nonce с сервера // Запрашиваем nonce с сервера
console.log('Requesting nonce...'); console.log('Requesting nonce...');
const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`); const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`);
const nonce = nonceResponse.data.nonce; const nonce = nonceResponse.data.nonce;
console.log('Got nonce:', nonce); console.log('Got nonce:', nonce);
// Создаем сообщение для подписи // Создаем сообщение для подписи
const domain = window.location.host; const domain = window.location.host;
const origin = window.location.origin; const origin = window.location.origin;
const statement = 'Sign in with Ethereum to the app.'; const statement = 'Sign in with Ethereum to the app.';
const siweMessage = new SiweMessage({ const siweMessage = new SiweMessage({
domain, domain,
address, address,
@@ -45,31 +45,31 @@ export async function connectWithWallet() {
version: '1', version: '1',
chainId: 1, chainId: 1,
nonce, nonce,
resources: [`${origin}/api/auth/verify`] resources: [`${origin}/api/auth/verify`],
}); });
const message = siweMessage.prepareMessage(); const message = siweMessage.prepareMessage();
console.log('SIWE message:', message); console.log('SIWE message:', message);
// Запрашиваем подпись // Запрашиваем подпись
console.log('Requesting signature...'); console.log('Requesting signature...');
const signature = await window.ethereum.request({ const signature = await window.ethereum.request({
method: 'personal_sign', method: 'personal_sign',
params: [message, address] params: [message, address],
}); });
console.log('Got signature:', signature); console.log('Got signature:', signature);
// Отправляем подпись на сервер для верификации // Отправляем подпись на сервер для верификации
console.log('Sending verification request...'); console.log('Sending verification request...');
const verificationResponse = await axios.post('/api/auth/verify', { const verificationResponse = await axios.post('/api/auth/verify', {
message, message,
signature, signature,
address address,
}); });
console.log('Verification response:', verificationResponse.data); console.log('Verification response:', verificationResponse.data);
// Обновляем состояние аутентификации // Обновляем состояние аутентификации
if (verificationResponse.data.success) { if (verificationResponse.data.success) {
// Обновляем состояние аутентификации в localStorage // Обновляем состояние аутентификации в localStorage
@@ -78,10 +78,10 @@ export async function connectWithWallet() {
localStorage.setItem('address', verificationResponse.data.address); localStorage.setItem('address', verificationResponse.data.address);
localStorage.setItem('isAdmin', verificationResponse.data.isAdmin); localStorage.setItem('isAdmin', verificationResponse.data.isAdmin);
} }
return verificationResponse.data; return verificationResponse.data;
} catch (error) { } catch (error) {
console.error('Error connecting wallet:', error); console.error('Error connecting wallet:', error);
throw error; throw error;
} }
} }

View File

@@ -5,56 +5,57 @@ import { SiweMessage } from 'siwe';
export const connectWallet = async () => { export const connectWallet = async () => {
try { try {
console.log('Starting wallet connection...'); console.log('Starting wallet connection...');
// Проверяем наличие MetaMask или другого Ethereum провайдера // Проверяем наличие MetaMask или другого Ethereum провайдера
if (!window.ethereum) { if (!window.ethereum) {
console.error('No Ethereum provider (like MetaMask) detected!'); console.error('No Ethereum provider (like MetaMask) detected!');
return { return {
success: false, success: false,
error: 'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.' error:
'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.',
}; };
} }
console.log('MetaMask detected, requesting accounts...'); console.log('MetaMask detected, requesting accounts...');
// Запрашиваем доступ к аккаунтам // Запрашиваем доступ к аккаунтам
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
console.log('Got accounts:', accounts); console.log('Got accounts:', accounts);
if (!accounts || accounts.length === 0) { if (!accounts || accounts.length === 0) {
return { return {
success: false, success: false,
error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.' error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.',
}; };
} }
// Берем первый аккаунт в списке // Берем первый аккаунт в списке
const address = accounts[0]; const address = accounts[0];
// Нормализуем адрес (приводим к нижнему регистру для последующих сравнений) // Нормализуем адрес (приводим к нижнему регистру для последующих сравнений)
const normalizedAddress = ethers.utils.getAddress(address); const normalizedAddress = ethers.utils.getAddress(address);
console.log('Normalized address:', normalizedAddress); console.log('Normalized address:', normalizedAddress);
// Запрашиваем nonce с сервера // Запрашиваем nonce с сервера
console.log('Requesting nonce...'); console.log('Requesting nonce...');
const nonceResponse = await axios.get(`/api/auth/nonce?address=${normalizedAddress}`); const nonceResponse = await axios.get(`/api/auth/nonce?address=${normalizedAddress}`);
const nonce = nonceResponse.data.nonce; const nonce = nonceResponse.data.nonce;
console.log('Got nonce:', nonce); console.log('Got nonce:', nonce);
if (!nonce) { if (!nonce) {
return { return {
success: false, success: false,
error: 'Не удалось получить nonce от сервера.' error: 'Не удалось получить nonce от сервера.',
}; };
} }
// Создаем провайдер Ethers // Создаем провайдер Ethers
const provider = new ethers.providers.Web3Provider(window.ethereum); const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner(); const signer = provider.getSigner();
// Создаем сообщение для подписи // Создаем сообщение для подписи
const domain = window.location.host; const domain = window.location.host;
const origin = window.location.origin; const origin = window.location.origin;
// Создаем SIWE сообщение // Создаем SIWE сообщение
const message = new SiweMessage({ const message = new SiweMessage({
domain, domain,
@@ -65,37 +66,37 @@ export const connectWallet = async () => {
chainId: 1, // Ethereum mainnet chainId: 1, // Ethereum mainnet
nonce: nonce, nonce: nonce,
issuedAt: new Date().toISOString(), issuedAt: new Date().toISOString(),
resources: [`${origin}/api/auth/verify`] resources: [`${origin}/api/auth/verify`],
}); });
// Получаем строку сообщения для подписи // Получаем строку сообщения для подписи
const messageToSign = message.prepareMessage(); const messageToSign = message.prepareMessage();
console.log('SIWE message:', messageToSign); console.log('SIWE message:', messageToSign);
// Запрашиваем подпись // Запрашиваем подпись
console.log('Requesting signature...'); console.log('Requesting signature...');
const signature = await signer.signMessage(messageToSign); const signature = await signer.signMessage(messageToSign);
if (!signature) { if (!signature) {
return { return {
success: false, success: false,
error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.' error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.',
}; };
} }
console.log('Got signature:', signature); console.log('Got signature:', signature);
// Отправляем верификацию на сервер // Отправляем верификацию на сервер
console.log('Sending verification request...'); console.log('Sending verification request...');
const verifyResponse = await axios.post('/api/auth/verify', { const verifyResponse = await axios.post('/api/auth/verify', {
address: normalizedAddress, address: normalizedAddress,
signature, signature,
nonce nonce,
}); });
// Обновляем интерфейс для отображения подключенного состояния // Обновляем интерфейс для отображения подключенного состояния
document.body.classList.add('wallet-connected'); document.body.classList.add('wallet-connected');
// Обновляем отображение адреса кошелька в UI // Обновляем отображение адреса кошелька в UI
const authDisplayEl = document.getElementById('auth-display'); const authDisplayEl = document.getElementById('auth-display');
if (authDisplayEl) { if (authDisplayEl) {
@@ -103,35 +104,35 @@ export const connectWallet = async () => {
authDisplayEl.innerHTML = `Кошелек: <strong>${shortAddress}</strong>`; authDisplayEl.innerHTML = `Кошелек: <strong>${shortAddress}</strong>`;
authDisplayEl.style.display = 'inline-block'; authDisplayEl.style.display = 'inline-block';
} }
// Скрываем кнопки авторизации и показываем кнопку выхода // Скрываем кнопки авторизации и показываем кнопку выхода
const authButtonsEl = document.getElementById('auth-buttons'); const authButtonsEl = document.getElementById('auth-buttons');
const logoutButtonEl = document.getElementById('logout-button'); const logoutButtonEl = document.getElementById('logout-button');
if (authButtonsEl) authButtonsEl.style.display = 'none'; if (authButtonsEl) authButtonsEl.style.display = 'none';
if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block'; if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block';
console.log('Verification response:', verifyResponse.data); console.log('Verification response:', verifyResponse.data);
if (verifyResponse.data.success) { if (verifyResponse.data.success) {
return { return {
success: true, success: true,
address: normalizedAddress, address: normalizedAddress,
userId: verifyResponse.data.userId, userId: verifyResponse.data.userId,
isAdmin: verifyResponse.data.isAdmin isAdmin: verifyResponse.data.isAdmin,
}; };
} else { } else {
return { return {
success: false, success: false,
error: verifyResponse.data.error || 'Ошибка верификации на сервере.' error: verifyResponse.data.error || 'Ошибка верификации на сервере.',
}; };
} }
} catch (error) { } catch (error) {
console.error('Error connecting wallet:', error); console.error('Error connecting wallet:', error);
// Формируем понятное сообщение об ошибке // Формируем понятное сообщение об ошибке
let errorMessage = 'Произошла ошибка при подключении кошелька.'; let errorMessage = 'Произошла ошибка при подключении кошелька.';
if (error.code === 4001) { if (error.code === 4001) {
errorMessage = 'Вы отклонили запрос на подпись в MetaMask.'; errorMessage = 'Вы отклонили запрос на подпись в MetaMask.';
} else if (error.response && error.response.data && error.response.data.error) { } else if (error.response && error.response.data && error.response.data.error) {
@@ -139,10 +140,10 @@ export const connectWallet = async () => {
} else if (error.message) { } else if (error.message) {
errorMessage = error.message; errorMessage = error.message;
} }
return { return {
success: false, success: false,
error: errorMessage error: errorMessage,
}; };
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -45,8 +45,8 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
credentials: true, credentials: true,
rewrite: (path) => path rewrite: (path) => path,
} },
} },
}, },
}); });

4
yarn.lock Normal file
View File

@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1