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

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":"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":"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"} {"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 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',
'vue/html-self-closing': [
'warn',
{
html: { html: {
void: 'always', void: 'always',
normal: 'always', normal: 'always',
component: '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,8 +15,8 @@ const api = axios.create({
baseURL: getBaseUrl(), baseURL: getBaseUrl(),
withCredentials: true, withCredentials: true,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} },
}); });
// Перехватчик запросов // Перехватчик запросов
@@ -50,7 +50,7 @@ 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);

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,27 +40,30 @@
</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(
() => isAuthenticated.value,
(authenticated) => {
if (!authenticated) { if (!authenticated) {
conversations.value = []; // Очищаем список бесед при отключении conversations.value = []; // Очищаем список бесед при отключении
selectedConversationId.value = null; selectedConversationId.value = null;
} }
}); }
);
// Загрузка списка диалогов // Загрузка списка диалогов
const fetchConversations = async () => { const fetchConversations = async () => {
try { try {
loading.value = true; loading.value = true;
const response = await axios.get('/api/messages/conversations'); const response = await axios.get('/api/messages/conversations');
@@ -75,16 +78,16 @@ const fetchConversations = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
// Выбор диалога // Выбор диалога
const selectConversation = (conversationId) => { const selectConversation = (conversationId) => {
selectedConversationId.value = conversationId; selectedConversationId.value = conversationId;
emit('select-conversation', conversationId); emit('select-conversation', conversationId);
}; };
// Создание нового диалога // Создание нового диалога
const createNewConversation = async () => { const createNewConversation = async () => {
try { try {
const response = await axios.post('/api/messages/conversations', { const response = await axios.post('/api/messages/conversations', {
title: 'Новый диалог', title: 'Новый диалог',
@@ -108,10 +111,10 @@ const createNewConversation = async () => {
} catch (error) { } catch (error) {
console.error('Error creating conversation:', error); console.error('Error creating conversation:', error);
} }
}; };
// Форматирование времени // Форматирование времени
const formatTime = (timestamp) => { const formatTime = (timestamp) => {
if (!timestamp) return ''; if (!timestamp) return '';
const date = new Date(timestamp); const date = new Date(timestamp);
@@ -132,113 +135,15 @@ const formatTime = (timestamp) => {
// Более недели назад - показываем дату // Более недели назад - показываем дату
return date.toLocaleDateString(); return date.toLocaleDateString();
} }
}; };
// Загрузка диалогов при монтировании компонента // Загрузка диалогов при монтировании компонента
onMounted(() => { onMounted(() => {
fetchConversations(); fetchConversations();
}); });
// Экспорт методов для использования в родительском компоненте // Экспорт методов для использования в родительском компоненте
defineExpose({ defineExpose({
fetchConversations, 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,23 +16,23 @@
</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;
@@ -40,10 +40,10 @@ const handleEnter = (event) => {
// Иначе отправляем сообщение // Иначе отправляем сообщение
sendMessage(); sendMessage();
}; };
// Отправка сообщения // Отправка сообщения
const sendMessage = async () => { const sendMessage = async () => {
const messageText = message.value.trim(); const messageText = message.value.trim();
if (!messageText) return; if (!messageText) return;
@@ -51,7 +51,7 @@ const sendMessage = async () => {
id: Date.now(), id: Date.now(),
content: messageText, content: messageText,
role: auth.isAuthenticated ? 'user' : 'guest', role: auth.isAuthenticated ? 'user' : 'guest',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}; };
messages.value.push(userMessage); messages.value.push(userMessage);
@@ -60,12 +60,12 @@ const sendMessage = async () => {
// Логируем параметры запроса // Логируем параметры запроса
console.log('Sending message to Ollama:', { console.log('Sending message to Ollama:', {
message: messageText, message: messageText,
language: userLanguage.value language: userLanguage.value,
}); });
const response = await axios.post('/api/chat/message', { const response = await axios.post('/api/chat/message', {
message: messageText, message: messageText,
language: userLanguage.value language: userLanguage.value,
}); });
// Логируем ответ от Ollama // Логируем ответ от Ollama
@@ -76,7 +76,7 @@ const sendMessage = async () => {
id: Date.now() + 1, id: Date.now() + 1,
content: response.data.message, content: response.data.message,
role: 'assistant', role: 'assistant',
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
// Очищаем поле ввода // Очищаем поле ввода
@@ -94,20 +94,20 @@ const sendMessage = async () => {
} finally { } finally {
sending.value = false; sending.value = false;
} }
}; };
// Сброс поля ввода // Сброс поля ввода
const resetInput = () => { const resetInput = () => {
message.value = ''; message.value = '';
}; };
// Экспорт методов для использования в родительском компоненте // Экспорт методов для использования в родительском компоненте
defineExpose({ defineExpose({
resetInput, resetInput,
focus: () => textareaRef.value?.focus(), focus: () => textareaRef.value?.focus(),
}); });
const sendGuestMessage = async (messageText) => { const sendGuestMessage = async (messageText) => {
if (!messageText.trim()) return; if (!messageText.trim()) return;
const userMessage = { const userMessage = {
@@ -115,7 +115,7 @@ const sendGuestMessage = async (messageText) => {
content: messageText, content: messageText,
role: 'user', role: 'user',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
isGuest: true isGuest: true,
}; };
// Добавляем сообщение пользователя в локальную историю // Добавляем сообщение пользователя в локальную историю
@@ -146,7 +146,7 @@ const sendGuestMessage = async (messageText) => {
role: 'assistant', role: 'assistant',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
isGuest: true, isGuest: true,
showAuthOptions: true // Указываем, что нужно показать кнопки аутентификации showAuthOptions: true, // Указываем, что нужно показать кнопки аутентификации
}; };
messages.value.push(authMessage); messages.value.push(authMessage);
@@ -160,55 +160,5 @@ const sendGuestMessage = async (messageText) => {
} }
isLoading.value = false; 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,23 +30,23 @@
</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 messages = ref([]);
const loading = ref(true); const loading = ref(true);
const threadContainer = ref(null); const threadContainer = ref(null);
const isAuthenticated = ref(false); const isAuthenticated = ref(false);
// Загрузка сообщений диалога // Загрузка сообщений диалога
const fetchMessages = async () => { const fetchMessages = async () => {
try { try {
loading.value = true; loading.value = true;
const response = await axios.get( const response = await axios.get(
@@ -58,10 +62,10 @@ const fetchMessages = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
// Добавление новых сообщений // Добавление новых сообщений
const addMessages = (newMessages) => { const addMessages = (newMessages) => {
if (Array.isArray(newMessages)) { if (Array.isArray(newMessages)) {
messages.value = [...messages.value, ...newMessages]; messages.value = [...messages.value, ...newMessages];
} else { } else {
@@ -72,25 +76,25 @@ const addMessages = (newMessages) => {
nextTick(() => { nextTick(() => {
scrollToBottom(); scrollToBottom();
}); });
}; };
// Прокрутка к последнему сообщению // Прокрутка к последнему сообщению
const scrollToBottom = () => { const scrollToBottom = () => {
if (threadContainer.value) { if (threadContainer.value) {
threadContainer.value.scrollTop = threadContainer.value.scrollHeight; threadContainer.value.scrollTop = threadContainer.value.scrollHeight;
} }
}; };
// Форматирование времени // Форматирование времени
const formatTime = (timestamp) => { const formatTime = (timestamp) => {
if (!timestamp) return ''; if (!timestamp) return '';
const date = new Date(timestamp); const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}; };
// Получение названия канала // Получение названия канала
const channelName = (channel) => { const channelName = (channel) => {
const channels = { const channels = {
web: 'Веб', web: 'Веб',
telegram: 'Telegram', telegram: 'Telegram',
@@ -98,116 +102,38 @@ const channelName = (channel) => {
}; };
return channels[channel] || channel; return channels[channel] || channel;
}; };
// Наблюдение за изменением ID диалога // Наблюдение за изменением ID диалога
watch( watch(
() => props.conversationId, () => props.conversationId,
(newId, oldId) => { (newId, oldId) => {
if (newId && newId !== oldId) { if (newId && newId !== oldId) {
fetchMessages(); fetchMessages();
} }
} }
); );
// Следим за изменением статуса аутентификации // Следим за изменением статуса аутентификации
watch(() => isAuthenticated.value, (authenticated) => { watch(
() => isAuthenticated.value,
(authenticated) => {
if (!authenticated) { if (!authenticated) {
messages.value = []; // Очищаем сообщения при отключении messages.value = []; // Очищаем сообщения при отключении
} }
}); }
);
// Загрузка сообщений при монтировании компонента // Загрузка сообщений при монтировании компонента
onMounted(() => { onMounted(() => {
if (props.conversationId) { if (props.conversationId) {
fetchMessages(); fetchMessages();
} }
}); });
// Экспорт методов для использования в родительском компоненте // Экспорт методов для использования в родительском компоненте
defineExpose({ defineExpose({
fetchMessages, fetchMessages,
addMessages, 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,71 +1,48 @@
<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) {
@@ -78,16 +55,16 @@ const requestCode = async () => {
} finally { } finally {
isLoading.value = false; 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) {
@@ -102,72 +79,12 @@ const verifyCode = async () => {
} finally { } finally {
isLoading.value = false; 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,23 +22,23 @@
</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 = ''; error.value = '';
@@ -66,12 +63,12 @@ const startConnection = async () => {
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
const checkConnection = async () => { const checkConnection = async () => {
try { try {
const response = await axios.post('/api/auth/telegram/check-connection', { const response = await axios.post('/api/auth/telegram/check-connection', {
token: connectionToken.value token: connectionToken.value,
}); });
if (response.data.success && response.data.telegramId) { if (response.data.success && response.data.telegramId) {
@@ -83,125 +80,29 @@ const checkConnection = async () => {
} catch (error) { } catch (error) {
console.error('Error checking connection:', error); console.error('Error checking connection:', error);
} }
}; };
const startPolling = () => { const startPolling = () => {
pollInterval.value = setInterval(checkConnection, 2000); pollInterval.value = setInterval(checkConnection, 2000);
}; };
const stopPolling = () => { const stopPolling = () => {
if (pollInterval.value) { if (pollInterval.value) {
clearInterval(pollInterval.value); clearInterval(pollInterval.value);
pollInterval.value = null; pollInterval.value = null;
} }
}; };
const resetConnection = () => { const resetConnection = () => {
stopPolling(); stopPolling();
showQR.value = false; showQR.value = false;
error.value = ''; error.value = '';
qrCode.value = ''; qrCode.value = '';
botLink.value = ''; botLink.value = '';
connectionToken.value = ''; connectionToken.value = '';
}; };
onUnmounted(() => { onUnmounted(() => {
stopPolling(); 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,25 +16,25 @@
</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 {
@@ -63,63 +59,5 @@ const connectWallet = async () => {
} finally { } finally {
isLoading.value = false; 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

@@ -22,10 +22,10 @@ export function useAuth() {
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);
} }
@@ -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 wasAuthenticated = isAuthenticated.value;
const previousUserId = userId.value; const previousUserId = userId.value;
@@ -80,7 +88,7 @@ export function useAuth() {
newAddress, newAddress,
newTelegramId, newTelegramId,
newIsAdmin, newIsAdmin,
newEmail newEmail,
}); });
// Убедимся, что переменные являются реактивными // Убедимся, что переменные являются реактивными
@@ -93,15 +101,18 @@ export function useAuth() {
email.value = newEmail || null; email.value = newEmail || null;
// Кэшируем данные аутентификации // Кэшируем данные аутентификации
localStorage.setItem('authData', JSON.stringify({ localStorage.setItem(
'authData',
JSON.stringify({
authenticated, authenticated,
authType: newAuthType, authType: newAuthType,
userId: newUserId, userId: newUserId,
address: newAddress, address: newAddress,
telegramId: newTelegramId, telegramId: newTelegramId,
isAdmin: newIsAdmin, isAdmin: newIsAdmin,
email: newEmail email: newEmail,
})); })
);
// Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса // Если аутентификация через кошелек, проверяем баланс токенов только при изменении адреса
if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) { if (authenticated && newAuthType === 'wallet' && newAddress && newAddress !== address.value) {
@@ -123,7 +134,7 @@ export function useAuth() {
address: address.value, address: address.value,
telegramId: telegramId.value, telegramId: telegramId.value,
email: email.value, email: email.value,
isAdmin: isAdmin.value isAdmin: isAdmin.value,
}); });
// Если пользователь только что аутентифицировался или сменил аккаунт, // Если пользователь только что аутентифицировался или сменил аккаунт,
@@ -149,14 +160,14 @@ export function useAuth() {
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,
}; };
// Добавляем все доступные идентификаторы // Добавляем все доступные идентификаторы
@@ -181,11 +192,13 @@ 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);
} }
} }
@@ -196,14 +209,14 @@ export function useAuth() {
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,
}; };
} }
} }
@@ -232,7 +245,7 @@ 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,
}); });
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения // Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
@@ -285,7 +298,7 @@ export function useAuth() {
address: null, address: null,
telegramId: null, telegramId: null,
email: null, email: null,
isAdmin: false isAdmin: false,
}); });
// Обновляем отображение отключенного состояния // Обновляем отображение отключенного состояния
@@ -400,7 +413,7 @@ export function useAuth() {
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) {
@@ -426,7 +439,7 @@ export function useAuth() {
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,
}; };
} }
}; };
@@ -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');
@@ -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 { 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

@@ -45,7 +45,7 @@ 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();
@@ -55,7 +55,7 @@ export async function connectWithWallet() {
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);
@@ -65,7 +65,7 @@ export async function connectWithWallet() {
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);

View File

@@ -11,7 +11,8 @@ export const connectWallet = async () => {
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.',
}; };
} }
@@ -24,7 +25,7 @@ export const connectWallet = async () => {
if (!accounts || accounts.length === 0) { if (!accounts || accounts.length === 0) {
return { return {
success: false, success: false,
error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.' error: 'Не удалось получить доступ к аккаунтам. Пожалуйста, разрешите доступ в MetaMask.',
}; };
} }
@@ -43,7 +44,7 @@ export const connectWallet = async () => {
if (!nonce) { if (!nonce) {
return { return {
success: false, success: false,
error: 'Не удалось получить nonce от сервера.' error: 'Не удалось получить nonce от сервера.',
}; };
} }
@@ -65,7 +66,7 @@ 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`],
}); });
// Получаем строку сообщения для подписи // Получаем строку сообщения для подписи
@@ -79,7 +80,7 @@ export const connectWallet = async () => {
if (!signature) { if (!signature) {
return { return {
success: false, success: false,
error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.' error: 'Подпись не была получена. Пожалуйста, подпишите сообщение в MetaMask.',
}; };
} }
@@ -90,7 +91,7 @@ export const connectWallet = async () => {
const verifyResponse = await axios.post('/api/auth/verify', { const verifyResponse = await axios.post('/api/auth/verify', {
address: normalizedAddress, address: normalizedAddress,
signature, signature,
nonce nonce,
}); });
// Обновляем интерфейс для отображения подключенного состояния // Обновляем интерфейс для отображения подключенного состояния
@@ -118,12 +119,12 @@ export const connectWallet = async () => {
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) {
@@ -142,7 +143,7 @@ export const connectWallet = async () => {
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