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

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