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

This commit is contained in:
2025-04-21 16:52:32 +03:00
parent fda664f5af
commit 9482443e2d
22 changed files with 11981 additions and 4068 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -19,3 +19,21 @@
{"address":"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b","contract":"0x4B294265720B09ca39BFBA18c7E368413c0f68eB","error":"Unknown error","level":"error","message":"Error getting balance for bsc:","timestamp":"2025-04-21T08:02:13.552Z"}
{"address":"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b","contract":"0xdce769b847a0a697239777d0b1c7dd33b6012ba0","error":"Unknown error","level":"error","message":"Error getting balance for arbitrum:","timestamp":"2025-04-21T08:02:13.567Z"}
{"address":"0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b","contract":"0x351f59de4fedbdf7601f5592b93db3b9330c1c1d","error":"Unknown error","level":"error","message":"Error getting balance for polygon:","timestamp":"2025-04-21T08:02:13.580Z"}
{"level":"error","message":"Provider for eth is not available: Network check timeout","timestamp":"2025-04-21T08:39:48.816Z"}
{"level":"error","message":"Provider for bsc is not available: Network check timeout","timestamp":"2025-04-21T08:39:48.817Z"}
{"level":"error","message":"Provider for arbitrum is not available: Network check timeout","timestamp":"2025-04-21T08:39:48.817Z"}
{"level":"error","message":"Provider for polygon is not available: Network check timeout","timestamp":"2025-04-21T08:39:48.817Z"}
{"level":"error","message":"All network checks for 0xf45aa4917b3775ba37f48aeb3dc1a943561e9e0b failed. Cannot verify admin status.","timestamp":"2025-04-21T08:39:48.818Z"}
{"level":"error","message":"Provider for arbitrum is not available: Network check timeout","timestamp":"2025-04-21T08:39:51.831Z"}
{"level":"error","message":"Provider for polygon is not available: Network check timeout","timestamp":"2025-04-21T08:40:02.425Z"}
{"level":"error","message":"Provider for eth is not available: Network check timeout","timestamp":"2025-04-21T08:40:05.437Z"}
{"level":"error","message":"Provider for polygon is not available: Network check timeout","timestamp":"2025-04-21T11:54:11.804Z"}
{"level":"error","message":"Provider for polygon is not available: Network check timeout","timestamp":"2025-04-21T12:08:10.148Z"}
{"level":"error","message":"Provider for bsc is not available: Network check timeout","timestamp":"2025-04-21T12:13:07.439Z"}
{"level":"error","message":"Provider for arbitrum is not available: Network check timeout","timestamp":"2025-04-21T12:13:12.799Z"}
{"level":"error","message":"Provider for eth is not available: Network check timeout","timestamp":"2025-04-21T12:23:30.146Z"}
{"level":"error","message":"Provider for bsc is not available: Network check timeout","timestamp":"2025-04-21T12:23:30.147Z"}
{"level":"error","message":"Provider for arbitrum is not available: Network check timeout","timestamp":"2025-04-21T12:23:30.147Z"}
{"level":"error","message":"Provider for polygon is not available: Network check timeout","timestamp":"2025-04-21T12:36:03.317Z"}
{"level":"error","message":"Provider for bsc is not available: Network check timeout","timestamp":"2025-04-21T13:48:41.033Z"}
{"level":"error","message":"Provider for polygon is not available: Network check timeout","timestamp":"2025-04-21T13:48:48.026Z"}

View File

@@ -1,7 +1,8 @@
import globals from 'globals';
import * as vueParser from 'vue-eslint-parser';
import vuePlugin from 'eslint-plugin-vue';
import prettierPlugin from 'eslint-plugin-prettier';
import prettierConfig from '@vue/eslint-config-prettier';
import eslintConfigPrettier from 'eslint-config-prettier';
export default [
{
@@ -35,29 +36,37 @@ export default [
...globals.browser,
...globals.es2021,
},
parser: vuePlugin.parser,
parser: vueParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
sourceType: 'module',
ecmaVersion: 2022,
},
},
plugins: {
vue: vuePlugin,
prettier: prettierPlugin,
},
processor: vuePlugin.processors['.vue'],
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/no-unused-vars': 'warn',
'vue/html-self-closing': ['warn', {
html: {
void: 'always',
normal: 'always',
component: 'always'
}
}],
'vue/no-v-html': 'off',
'vue/html-self-closing': [
'warn',
{
html: {
void: 'always',
normal: 'always',
component: 'always',
},
},
],
'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
},
},

View File

@@ -1,137 +1,28 @@
<template>
<div id="app">
<router-view />
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner" />
</div>
<RouterView />
</div>
</template>
<script setup>
import { onMounted, ref, provide, computed } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { ref, watch } from 'vue';
import { RouterView } from 'vue-router';
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
const authState = ref({
isAuthenticated: false,
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);
watch(isAuthenticated, (newValue, oldValue) => {
if (newValue !== oldValue) {
console.log('Состояние аутентификации изменилось:', newValue);
}
},
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>
<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,8 +15,8 @@ const api = axios.create({
baseURL: getBaseUrl(),
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
'Content-Type': 'application/json',
},
});
// Перехватчик запросов
@@ -50,7 +50,7 @@ const sendGuestMessageToServer = async (messageText) => {
try {
await axios.post('/api/chat/guest-message', {
message: messageText,
language: userLanguage.value
// language: userLanguage.value, // TODO: Реализовать получение языка пользователя
});
} catch (error) {
console.error('Ошибка при отправке гостевого сообщения на сервер:', error);

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
<div class="conversation-list">
<div class="list-header">
<h3>Диалоги</h3>
<button @click="createNewConversation" class="new-conversation-btn">
<button class="new-conversation-btn" @click="createNewConversation">
<span>+</span> Новый диалог
</button>
</div>
@@ -40,205 +40,110 @@
</template>
<script setup>
import { ref, onMounted, computed, defineEmits, watch, inject } from 'vue';
import axios from 'axios';
import { ref, onMounted, computed, defineEmits, watch, inject } from 'vue';
import axios from 'axios';
const emit = defineEmits(['select-conversation']);
const auth = inject('auth');
const isAuthenticated = computed(() => auth.isAuthenticated.value);
const emit = defineEmits(['select-conversation']);
const auth = inject('auth');
const isAuthenticated = computed(() => auth.isAuthenticated.value);
const conversations = ref([]);
const loading = ref(true);
const selectedConversationId = ref(null);
const conversations = ref([]);
const loading = ref(true);
const selectedConversationId = ref(null);
// Следим за изменением статуса аутентификации
watch(() => isAuthenticated.value, (authenticated) => {
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);
// Следим за изменением статуса аутентификации
watch(
() => isAuthenticated.value,
(authenticated) => {
if (!authenticated) {
conversations.value = []; // Очищаем список бесед при отключении
selectedConversationId.value = null;
}
}
} catch (error) {
console.error('Error fetching conversations:', error);
} finally {
loading.value = false;
}
};
);
// Выбор диалога
const selectConversation = (conversationId) => {
selectedConversationId.value = conversationId;
emit('select-conversation', conversationId);
};
// Загрузка списка диалогов
const fetchConversations = async () => {
try {
loading.value = true;
const response = await axios.get('/api/messages/conversations');
conversations.value = response.data;
// Создание нового диалога
const createNewConversation = async () => {
try {
const response = await axios.post('/api/messages/conversations', {
title: 'Новый диалог',
});
// Если есть диалоги и не выбран ни один, выбираем первый
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 newConversation = {
conversation_id: response.data.id,
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,
};
// Выбор диалога
const selectConversation = (conversationId) => {
selectedConversationId.value = conversationId;
emit('select-conversation', conversationId);
};
conversations.value.unshift(newConversation);
// Создание нового диалога
const createNewConversation = async () => {
try {
const response = await axios.post('/api/messages/conversations', {
title: 'Новый диалог',
});
// Выбираем новый диалог
selectConversation(newConversation.conversation_id);
} catch (error) {
console.error('Error creating conversation:', error);
}
};
// Добавляем новый диалог в список
const newConversation = {
conversation_id: response.data.id,
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,
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
conversations.value.unshift(newConversation);
const date = new Date(timestamp);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
// Выбираем новый диалог
selectConversation(newConversation.conversation_id);
} catch (error) {
console.error('Error creating conversation:', error);
}
};
if (diffDays === 0) {
// Сегодня - показываем только время
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();
}
};
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
// Загрузка диалогов при монтировании компонента
onMounted(() => {
fetchConversations();
});
const date = new Date(timestamp);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchConversations,
});
if (diffDays === 0) {
// Сегодня - показываем только время
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>
<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>
<div class="message-input">
<textarea
ref="textareaRef"
v-model="message"
placeholder="Введите сообщение..."
@keydown.enter.prevent="handleEnter"
ref="textareaRef"
: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-else>Отправить</span>
</button>
@@ -16,199 +16,149 @@
</template>
<script setup>
import { ref, defineEmits, nextTick } from 'vue';
import axios from 'axios';
import { ref, defineEmits, nextTick } from 'vue';
import axios from 'axios';
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
const props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
const emit = defineEmits(['message-sent']);
const message = ref('');
const sending = ref(false);
const textareaRef = ref(null);
const emit = defineEmits(['message-sent']);
const message = ref('');
const sending = ref(false);
const textareaRef = ref(null);
// Обработка нажатия Enter
const handleEnter = (event) => {
// Если нажат Shift+Enter, добавляем перенос строки
if (event.shiftKey) {
return;
}
// Обработка нажатия Enter
const handleEnter = (event) => {
// Если нажат Shift+Enter, добавляем перенос строки
if (event.shiftKey) {
return;
}
// Иначе отправляем сообщение
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()
// Иначе отправляем сообщение
sendMessage();
};
messages.value.push(userMessage);
// Отправка сообщения
const sendMessage = async () => {
const messageText = message.value.trim();
if (!messageText) return;
try {
// Логируем параметры запроса
console.log('Sending message to Ollama:', {
message: messageText,
language: userLanguage.value
});
const userMessage = {
id: Date.now(),
content: messageText,
role: auth.isAuthenticated ? 'user' : 'guest',
timestamp: new Date().toISOString(),
};
const response = await axios.post('/api/chat/message', {
message: messageText,
language: userLanguage.value
});
messages.value.push(userMessage);
// Логируем ответ от Ollama
console.log('Response from Ollama:', response.data);
try {
// Логируем параметры запроса
console.log('Sending message to Ollama:', {
message: messageText,
language: userLanguage.value,
});
// Обработка ответа
messages.value.push({
id: Date.now() + 1,
content: response.data.message,
role: 'assistant',
timestamp: new Date().toISOString()
});
const response = await axios.post('/api/chat/message', {
message: messageText,
language: userLanguage.value,
});
// Логируем ответ от 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(() => {
textareaRef.value.focus();
});
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
// Уведомляем родительский компонент о новых сообщениях
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
} catch (error) {
console.error('Ошибка при отправке сообщения:', error);
} finally {
sending.value = false;
}
};
// Устанавливаем состояние загрузки
isLoading.value = true;
// Сброс поля ввода
const resetInput = () => {
message.value = '';
};
// Вместо отправки запроса к Ollama, отправляем сообщение с кнопками для аутентификации
const authMessage = {
id: Date.now() + 1,
content: 'Чтобы продолжить, пожалуйста, аутентифицируйтесь.',
role: 'assistant',
timestamp: new Date().toISOString(),
isGuest: true,
showAuthOptions: true, // Указываем, что нужно показать кнопки аутентификации
};
// Экспорт методов для использования в родительском компоненте
defineExpose({
resetInput,
focus: () => textareaRef.value?.focus(),
});
messages.value.push(authMessage);
guestMessages.value.push(authMessage);
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
const sendGuestMessage = async (messageText) => {
if (!messageText.trim()) return;
// Прокрутка вниз
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
const userMessage = {
id: Date.now(),
content: messageText,
role: 'user',
timestamp: new Date().toISOString(),
isGuest: true
isLoading.value = false;
};
// Добавляем сообщение пользователя в локальную историю
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>
<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>
<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-else-if="messages.length === 0" class="empty-thread">
@@ -8,7 +8,11 @@
</div>
<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-meta">
<span class="time">{{ formatTime(message.created_at) }}</span>
@@ -26,188 +30,110 @@
</template>
<script setup>
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
import axios from 'axios';
import { ref, onMounted, watch, nextTick, defineExpose } from 'vue';
import axios from 'axios';
const props = defineProps({
conversationId: {
type: [Number, String],
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 props = defineProps({
conversationId: {
type: [Number, String],
required: true,
},
});
};
// Прокрутка к последнему сообщению
const scrollToBottom = () => {
if (threadContainer.value) {
threadContainer.value.scrollTop = threadContainer.value.scrollHeight;
}
};
const messages = ref([]);
const loading = ref(true);
const threadContainer = ref(null);
const isAuthenticated = ref(false);
// Форматирование времени
const formatTime = (timestamp) => {
if (!timestamp) return '';
// Загрузка сообщений диалога
const fetchMessages = async () => {
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' });
};
// Получение названия канала
const channelName = (channel) => {
const channels = {
web: 'Веб',
telegram: 'Telegram',
email: 'Email',
// Прокрутка к последнему сообщению
await nextTick();
scrollToBottom();
} catch (error) {
console.error('Error fetching messages:', error);
} finally {
loading.value = false;
}
};
return channels[channel] || channel;
};
// Добавление новых сообщений
const addMessages = (newMessages) => {
if (Array.isArray(newMessages)) {
messages.value = [...messages.value, ...newMessages];
} else {
messages.value.push(newMessages);
}
// Наблюдение за изменением ID диалога
watch(
() => props.conversationId,
(newId, oldId) => {
if (newId && newId !== oldId) {
// Прокрутка к последнему сообщению
nextTick(() => {
scrollToBottom();
});
};
// Прокрутка к последнему сообщению
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();
}
}
);
});
// Следим за изменением статуса аутентификации
watch(() => isAuthenticated.value, (authenticated) => {
if (!authenticated) {
messages.value = []; // Очищаем сообщения при отключении
}
});
// Загрузка сообщений при монтировании компонента
onMounted(() => {
if (props.conversationId) {
fetchMessages();
}
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchMessages,
addMessages,
});
// Экспорт методов для использования в родительском компоненте
defineExpose({
fetchMessages,
addMessages,
});
</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>
<div class="email-connection">
<div v-if="!showVerification" class="email-form">
<input
v-model="email"
type="email"
placeholder="Введите email"
class="email-input"
/>
<button
@click="requestCode"
:disabled="isLoading || !isValidEmail"
class="email-btn"
>
<input v-model="email" type="email" placeholder="Введите email" class="email-input" />
<button :disabled="isLoading || !isValidEmail" class="email-btn" @click="requestCode">
{{ isLoading ? 'Отправка...' : 'Получить код' }}
</button>
</div>
<div v-else class="verification-form">
<p class="verification-info">Код отправлен на {{ email }}</p>
<input
v-model="code"
type="text"
placeholder="Введите код"
class="code-input"
/>
<button
@click="verifyCode"
:disabled="isLoading || !code"
class="verify-btn"
>
<input v-model="code" type="text" placeholder="Введите код" class="code-input" />
<button :disabled="isLoading || !code" class="verify-btn" @click="verifyCode">
{{ isLoading ? 'Проверка...' : 'Подтвердить' }}
</button>
<button
@click="resetForm"
class="reset-btn"
>
Изменить email
</button>
<button class="reset-btn" @click="resetForm">Изменить email</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
import { ref, computed } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const email = ref('');
const code = ref('');
const error = ref('');
const isLoading = ref(false);
const showVerification = ref(false);
const email = ref('');
const code = ref('');
const error = ref('');
const isLoading = ref(false);
const showVerification = ref(false);
const isValidEmail = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
});
const isValidEmail = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
});
const requestCode = async () => {
try {
isLoading.value = true;
error.value = '';
const requestCode = async () => {
try {
isLoading.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/request-verification', {
email: email.value
});
const response = await axios.post('/api/auth/email/request-verification', {
email: email.value,
});
if (response.data.success) {
showVerification.value = true;
} else {
error.value = response.data.error || 'Ошибка отправки кода';
if (response.data.success) {
showVerification.value = true;
} else {
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 () => {
try {
isLoading.value = true;
error.value = '';
const verifyCode = async () => {
try {
isLoading.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/verify', {
email: email.value,
code: code.value
});
const response = await axios.post('/api/auth/email/verify', {
email: email.value,
code: code.value,
});
if (response.data.success) {
// Связываем email с текущим пользователем
await linkIdentity('email', email.value);
emit('close');
} else {
error.value = response.data.error || 'Неверный код';
if (response.data.success) {
// Связываем email с текущим пользователем
await linkIdentity('email', email.value);
emit('close');
} else {
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 = () => {
email.value = '';
code.value = '';
error.value = '';
showVerification.value = false;
};
const resetForm = () => {
email.value = '';
code.value = '';
error.value = '';
showVerification.value = false;
};
</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 v-if="!showQR" class="intro">
<p>Подключите свой аккаунт Telegram для быстрой авторизации</p>
<button @click="startConnection" class="connect-button" :disabled="loading">
<button class="connect-button" :disabled="loading" @click="startConnection">
<span class="telegram-icon">📱</span>
{{ loading ? 'Загрузка...' : 'Подключить Telegram' }}
</button>
@@ -10,14 +10,11 @@
<div v-else class="qr-section">
<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>
<a :href="botLink" target="_blank" class="bot-link">
Открыть бота в Telegram
</a>
<button @click="resetConnection" class="reset-button">
Отмена
</button>
<a :href="botLink" target="_blank" class="bot-link"> Открыть бота в Telegram </a>
<button class="reset-button" @click="resetConnection">Отмена</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
@@ -25,183 +22,87 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
import QRCode from 'qrcode';
import { ref, onMounted, onUnmounted } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
import QRCode from 'qrcode';
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const loading = ref(false);
const error = ref('');
const showQR = ref(false);
const qrCode = ref('');
const botLink = ref('');
const pollInterval = ref(null);
const connectionToken = ref('');
const loading = ref(false);
const error = ref('');
const showQR = ref(false);
const qrCode = ref('');
const botLink = ref('');
const pollInterval = ref(null);
const connectionToken = ref('');
const startConnection = async () => {
try {
loading.value = true;
const startConnection = async () => {
try {
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 = '';
qrCode.value = '';
botLink.value = '';
connectionToken.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 = '';
qrCode.value = '';
botLink.value = '';
connectionToken.value = '';
};
onUnmounted(() => {
stopPolling();
});
onUnmounted(() => {
stopPolling();
});
</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 v-if="!isConnected" class="connect-section">
<p>Подключите свой кошелек для доступа к расширенным функциям</p>
<button
@click="connectWallet"
:disabled="isLoading"
class="wallet-btn"
>
<button :disabled="isLoading" class="wallet-btn" @click="connectWallet">
<span class="wallet-icon">💳</span>
{{ isLoading ? 'Подключение...' : 'Подключить кошелек' }}
</button>
@@ -20,106 +16,48 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { connectWithWallet } from '@/services/wallet';
import { ref, computed } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { connectWithWallet } from '@/services/wallet';
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const isLoading = ref(false);
const error = ref('');
const address = ref('');
const isLoading = ref(false);
const error = ref('');
const address = ref('');
const isConnected = computed(() => !!address.value);
const isConnected = computed(() => !!address.value);
const formatAddress = (addr) => {
if (!addr) return '';
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
const formatAddress = (addr) => {
if (!addr) return '';
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
const connectWallet = async () => {
if (isLoading.value) return;
const connectWallet = async () => {
if (isLoading.value) return;
try {
isLoading.value = true;
error.value = '';
try {
isLoading.value = true;
error.value = '';
// Подключаем кошелек
const result = await connectWithWallet();
// Подключаем кошелек
const result = await connectWithWallet();
if (result.success) {
address.value = result.address;
if (result.success) {
address.value = result.address;
// Связываем кошелек с текущим пользователем
await linkIdentity('wallet', result.address);
emit('close');
} else {
error.value = result.error || 'Не удалось подключить кошелек';
// Связываем кошелек с текущим пользователем
await linkIdentity('wallet', result.address);
emit('close');
} else {
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>
<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 EmailConnect from './EmailConnect.vue';
export {
TelegramConnect,
WalletConnection,
EmailConnect
};
export { TelegramConnect, WalletConnection, EmailConnect };

View File

@@ -22,10 +22,10 @@ export function useAuth() {
if (response.data.success) {
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
const filteredIdentities = response.data.identities
.filter(identity => identity.provider !== 'guest')
.filter((identity) => identity.provider !== 'guest')
.reduce((acc, identity) => {
// Для каждого типа провайдера оставляем только один идентификатор
const existingIdentity = acc.find(i => i.provider === identity.provider);
const existingIdentity = acc.find((i) => i.provider === identity.provider);
if (!existingIdentity) {
acc.push(identity);
}
@@ -69,7 +69,15 @@ export function useAuth() {
}
};
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 previousUserId = userId.value;
@@ -80,7 +88,7 @@ export function useAuth() {
newAddress,
newTelegramId,
newIsAdmin,
newEmail
newEmail,
});
// Убедимся, что переменные являются реактивными
@@ -93,15 +101,18 @@ export function useAuth() {
email.value = newEmail || null;
// Кэшируем данные аутентификации
localStorage.setItem('authData', JSON.stringify({
authenticated,
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail
}));
localStorage.setItem(
'authData',
JSON.stringify({
authenticated,
authType: newAuthType,
userId: newUserId,
address: newAddress,
telegramId: newTelegramId,
isAdmin: newIsAdmin,
email: newEmail,
})
);
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
@@ -123,7 +134,7 @@ export function useAuth() {
address: address.value,
telegramId: telegramId.value,
email: email.value,
isAdmin: isAdmin.value
isAdmin: isAdmin.value,
});
// Если пользователь только что аутентифицировался или сменил аккаунт,
@@ -149,14 +160,14 @@ export function useAuth() {
return {
success: true,
message: 'No new guest IDs to process',
processedIds: processedGuestIds.value
processedIds: processedGuestIds.value,
};
}
// Создаем объект с идентификаторами для передачи на сервер
const identifiersData = {
userId: userId.value,
guestId: localGuestId
guestId: localGuestId,
};
// Добавляем все доступные идентификаторы
@@ -181,11 +192,13 @@ export function useAuth() {
// В качестве запасного варианта также обрабатываем старый формат ответа
else if (response.data.results && Array.isArray(response.data.results)) {
const newProcessedIds = response.data.results
.filter(result => result.guestId)
.map(result => result.guestId);
.filter((result) => result.guestId)
.map((result) => result.guestId);
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);
}
}
@@ -196,14 +209,14 @@ export function useAuth() {
return {
success: true,
processedIds: processedGuestIds.value
processedIds: processedGuestIds.value,
};
}
} catch (error) {
console.error('Error linking messages:', error);
return {
success: false,
error: error.message
error: error.message,
};
}
}
@@ -232,7 +245,7 @@ export function useAuth() {
address: response.data.address,
telegramId: response.data.telegramId,
email: response.data.email,
isAdmin: response.data.isAdmin
isAdmin: response.data.isAdmin,
});
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
@@ -285,7 +298,7 @@ export function useAuth() {
address: null,
telegramId: null,
email: null,
isAdmin: false
isAdmin: false,
});
// Обновляем отображение отключенного состояния
@@ -400,7 +413,7 @@ export function useAuth() {
const response = await axios.post('/api/auth/identities/link', {
type: provider,
value: providerId
value: providerId,
});
if (response.data.success) {
@@ -426,7 +439,7 @@ export function useAuth() {
console.error('Ошибка при связывании идентификатора:', error);
return {
success: false,
error: error.response?.data?.error || error.message
error: error.response?.data?.error || error.message,
};
}
};
@@ -449,6 +462,6 @@ export function useAuth() {
updateIdentities,
updateProcessedGuestIds,
updateConnectionDisplay,
linkIdentity
linkIdentity,
};
}

View File

@@ -8,7 +8,8 @@ import axios from 'axios';
// Настройка axios
// В 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.withCredentials = true;

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ export async function connectWithWallet() {
version: '1',
chainId: 1,
nonce,
resources: [`${origin}/api/auth/verify`]
resources: [`${origin}/api/auth/verify`],
});
const message = siweMessage.prepareMessage();
@@ -55,7 +55,7 @@ export async function connectWithWallet() {
console.log('Requesting signature...');
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, address]
params: [message, address],
});
console.log('Got signature:', signature);
@@ -65,7 +65,7 @@ export async function connectWithWallet() {
const verificationResponse = await axios.post('/api/auth/verify', {
message,
signature,
address
address,
});
console.log('Verification response:', verificationResponse.data);

View File

@@ -11,7 +11,8 @@ export const connectWallet = async () => {
console.error('No Ethereum provider (like MetaMask) detected!');
return {
success: false,
error: 'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.'
error:
'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.',
};
}
@@ -24,7 +25,7 @@ export const connectWallet = async () => {
if (!accounts || accounts.length === 0) {
return {
success: false,
error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.'
error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.',
};
}
@@ -43,7 +44,7 @@ export const connectWallet = async () => {
if (!nonce) {
return {
success: false,
error: 'Не удалось получить nonce от сервера.'
error: 'Не удалось получить nonce от сервера.',
};
}
@@ -65,7 +66,7 @@ export const connectWallet = async () => {
chainId: 1, // Ethereum mainnet
nonce: nonce,
issuedAt: new Date().toISOString(),
resources: [`${origin}/api/auth/verify`]
resources: [`${origin}/api/auth/verify`],
});
// Получаем строку сообщения для подписи
@@ -79,7 +80,7 @@ export const connectWallet = async () => {
if (!signature) {
return {
success: false,
error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.'
error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.',
};
}
@@ -90,7 +91,7 @@ export const connectWallet = async () => {
const verifyResponse = await axios.post('/api/auth/verify', {
address: normalizedAddress,
signature,
nonce
nonce,
});
// Обновляем интерфейс для отображения подключенного состояния
@@ -118,12 +119,12 @@ export const connectWallet = async () => {
success: true,
address: normalizedAddress,
userId: verifyResponse.data.userId,
isAdmin: verifyResponse.data.isAdmin
isAdmin: verifyResponse.data.isAdmin,
};
} else {
return {
success: false,
error: verifyResponse.data.error || 'Ошибка верификации на сервере.'
error: verifyResponse.data.error || 'Ошибка верификации на сервере.',
};
}
} catch (error) {
@@ -142,7 +143,7 @@ export const connectWallet = async () => {
return {
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,
secure: false,
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