Описание изменений
This commit is contained in:
@@ -25,10 +25,25 @@ const query = (text, params) => {
|
|||||||
return pool.query(text, params);
|
return pool.query(text, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Функция для сохранения гостевого сообщения в базе данных
|
||||||
|
async function saveGuestMessageToDatabase(message, language, guestId) {
|
||||||
|
try {
|
||||||
|
await query(`
|
||||||
|
INSERT INTO guest_messages (guest_id, content, language, created_at)
|
||||||
|
VALUES ($1, $2, $3, NOW())
|
||||||
|
`, [guestId, message, language]);
|
||||||
|
console.log('Гостевое сообщение успешно сохранено:', message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при сохранении гостевого сообщения:', error);
|
||||||
|
throw error; // Пробрасываем ошибку дальше
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Экспортируем функции для работы с базой данных
|
// Экспортируем функции для работы с базой данных
|
||||||
module.exports = {
|
module.exports = {
|
||||||
query,
|
query,
|
||||||
pool,
|
pool,
|
||||||
|
saveGuestMessageToDatabase,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для создания временного хранилища данных в памяти
|
// Функция для создания временного хранилища данных в памяти
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const { verifySignature, checkAccess, findOrCreateUser } = require('../utils/aut
|
|||||||
const authService = require('../services/auth-service');
|
const authService = require('../services/auth-service');
|
||||||
const { SiweMessage } = require('siwe');
|
const { SiweMessage } = require('siwe');
|
||||||
const { sendEmail } = require('../services/emailBot');
|
const { sendEmail } = require('../services/emailBot');
|
||||||
|
const { verificationCodes } = require('../services/telegramBot');
|
||||||
|
|
||||||
// Создайте лимитер для попыток аутентификации
|
// Создайте лимитер для попыток аутентификации
|
||||||
const authLimiter = rateLimit({
|
const authLimiter = rateLimit({
|
||||||
@@ -123,12 +124,20 @@ router.post('/verify', async (req, res) => {
|
|||||||
const { userId, isAdmin } = await findOrCreateUser(address);
|
const { userId, isAdmin } = await findOrCreateUser(address);
|
||||||
console.log('User found/created:', { userId, isAdmin });
|
console.log('User found/created:', { userId, isAdmin });
|
||||||
|
|
||||||
|
// Сохраняем guestId перед обновлением сессии
|
||||||
|
const currentGuestId = req.session.guestId;
|
||||||
|
|
||||||
// Устанавливаем пользователя в сессии
|
// Устанавливаем пользователя в сессии
|
||||||
req.session.userId = userId;
|
req.session.userId = userId;
|
||||||
req.session.address = address;
|
req.session.address = address;
|
||||||
req.session.isAdmin = isAdmin;
|
req.session.isAdmin = isAdmin;
|
||||||
req.session.authenticated = true;
|
req.session.authenticated = true;
|
||||||
|
|
||||||
|
// Сохраняем guestId в новой сессии
|
||||||
|
if (currentGuestId) {
|
||||||
|
req.session.guestId = currentGuestId;
|
||||||
|
}
|
||||||
|
|
||||||
// Сохраняем сессию ПЕРЕД отправкой ответа
|
// Сохраняем сессию ПЕРЕД отправкой ответа
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
req.session.save(err => {
|
req.session.save(err => {
|
||||||
@@ -142,12 +151,20 @@ router.post('/verify', async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Добавляем задержку для гарантии сохранения сессии (временное решение)
|
console.log('Authentication successful for user:', {
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
userId,
|
||||||
|
address,
|
||||||
console.log('Authentication successful for user:', { userId, address, isAdmin });
|
isAdmin,
|
||||||
|
guestId: currentGuestId
|
||||||
|
});
|
||||||
console.log('Session after save:', req.session);
|
console.log('Session after save:', req.session);
|
||||||
|
|
||||||
|
// Обрабатываем гостевые сообщения, если они есть
|
||||||
|
if (currentGuestId) {
|
||||||
|
console.log(`Processing guest messages for guestId: ${currentGuestId}`);
|
||||||
|
await authService.processGuestMessages(userId, currentGuestId);
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
userId,
|
userId,
|
||||||
@@ -156,8 +173,8 @@ router.post('/verify', async (req, res) => {
|
|||||||
authType: 'wallet'
|
authType: 'wallet'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error verifying signature:', error);
|
console.error('Error during wallet verification:', error);
|
||||||
return res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -604,57 +621,97 @@ router.get('/telegram/code', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Функция для проверки кода Telegram
|
||||||
|
async function verifyTelegramCode(code) {
|
||||||
|
try {
|
||||||
|
// Используем глобальное хранилище кодов
|
||||||
|
const verificationCodes = global.verificationCodes;
|
||||||
|
|
||||||
|
if (!verificationCodes) {
|
||||||
|
return { success: false, error: 'Система верификации не инициализирована' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем chatId по коду
|
||||||
|
for (const [chatId, data] of verificationCodes.entries()) {
|
||||||
|
if (data.code === code) {
|
||||||
|
// Проверяем срок действия
|
||||||
|
if (Date.now() > data.expires) {
|
||||||
|
verificationCodes.delete(chatId);
|
||||||
|
return { success: false, error: 'Код истек' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Код верный и не истек
|
||||||
|
const telegramId = chatId;
|
||||||
|
verificationCodes.delete(chatId);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
telegramId: telegramId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: false, error: 'Неверный код' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in verifyTelegramCode:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для проверки баланса токенов
|
||||||
|
async function checkTokenBalance(address) {
|
||||||
|
try {
|
||||||
|
const authService = require('../services/auth-service');
|
||||||
|
const isAdmin = await authService.checkTokensAndUpdateRole(address);
|
||||||
|
return isAdmin;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking token balance:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Маршрут для верификации Telegram
|
// Маршрут для верификации Telegram
|
||||||
router.post('/telegram/verify', async (req, res) => {
|
router.post('/telegram/verify', async (req, res) => {
|
||||||
try {
|
const { code } = req.body;
|
||||||
const { telegramId, code } = req.body;
|
|
||||||
const verificationData = req.session.telegramVerificationData;
|
|
||||||
|
|
||||||
// Проверяем, что код существует и не истек
|
try {
|
||||||
if (!verificationData ||
|
const telegramBot = require('../services/telegramBot');
|
||||||
verificationData.code !== code ||
|
const result = await telegramBot.verifyCode(code);
|
||||||
Date.now() > verificationData.expires) {
|
|
||||||
return res.status(400).json({
|
if (result.success) {
|
||||||
success: false,
|
// Проверяем, что у нас есть telegramId
|
||||||
error: 'Неверный или истекший код подтверждения'
|
if (!result.telegramId) {
|
||||||
});
|
return res.status(400).json({ error: 'Invalid Telegram ID' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ищем или создаем пользователя с этим Telegram ID
|
// Создаем или находим пользователя
|
||||||
const result = await db.query(
|
const userResult = await pool.query(
|
||||||
'SELECT * FROM find_or_create_user_by_identity($1, $2)',
|
`INSERT INTO users (created_at)
|
||||||
['telegram', telegramId]
|
VALUES (NOW())
|
||||||
|
RETURNING id`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const userId = result.rows[0].user_id;
|
const userId = userResult.rows[0].id;
|
||||||
const isNew = result.rows[0].is_new;
|
|
||||||
|
|
||||||
// Проверяем, есть ли у пользователя связанный кошелек
|
// Добавляем Telegram идентификатор
|
||||||
const walletResult = await db.query(`
|
await pool.query(
|
||||||
SELECT identity_value
|
`INSERT INTO user_identities
|
||||||
FROM user_identities ui
|
(user_id, identity_type, identity_value, verified, created_at)
|
||||||
WHERE ui.user_id = $1 AND ui.identity_type = 'wallet'
|
VALUES ($1, 'telegram', $2, true, NOW())
|
||||||
`, [userId]);
|
ON CONFLICT (identity_type, identity_value)
|
||||||
|
DO UPDATE SET verified = true
|
||||||
|
RETURNING user_id`,
|
||||||
|
[userId, result.telegramId]
|
||||||
|
);
|
||||||
|
|
||||||
const hasWallet = walletResult.rows.length > 0;
|
// Обновляем сессию
|
||||||
let walletAddress = null;
|
|
||||||
let isAdmin = false;
|
|
||||||
|
|
||||||
// Если есть кошелек, проверяем наличие токенов
|
|
||||||
if (hasWallet) {
|
|
||||||
walletAddress = walletResult.rows[0].identity_value;
|
|
||||||
const userResult = await db.query('SELECT is_admin FROM users WHERE id = $1', [userId]);
|
|
||||||
isAdmin = userResult.rows[0].is_admin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Устанавливаем сессию
|
|
||||||
req.session.authenticated = true;
|
|
||||||
req.session.userId = userId;
|
req.session.userId = userId;
|
||||||
|
req.session.authenticated = true;
|
||||||
req.session.authType = 'telegram';
|
req.session.authType = 'telegram';
|
||||||
req.session.telegramId = telegramId;
|
req.session.telegramId = result.telegramId;
|
||||||
|
|
||||||
|
// Если есть подключенный кошелек, проверяем баланс токенов
|
||||||
|
if (req.session.address) {
|
||||||
|
const isAdmin = await checkTokenBalance(req.session.address);
|
||||||
req.session.isAdmin = isAdmin;
|
req.session.isAdmin = isAdmin;
|
||||||
if (walletAddress) {
|
|
||||||
req.session.address = walletAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем сессию
|
// Сохраняем сессию
|
||||||
@@ -665,22 +722,19 @@ router.post('/telegram/verify', async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Очищаем данные верификации
|
return res.json({
|
||||||
delete req.session.telegramVerificationData;
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
success: true,
|
||||||
authenticated: true,
|
userId: userId,
|
||||||
userId,
|
telegramId: result.telegramId,
|
||||||
telegramId,
|
isAdmin: req.session.isAdmin || false,
|
||||||
isAdmin,
|
authenticated: true
|
||||||
hasWallet,
|
|
||||||
walletAddress,
|
|
||||||
isNew
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(400).json({ error: result.error || 'Invalid verification code' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error in telegram verification: ${error.message}`);
|
console.error('Error in telegram verification:', error);
|
||||||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1019,7 +1073,7 @@ router.get('/email/auth-status/:token', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Маршрут для прямой проверки кода, введенного пользователем
|
// Маршрут для проверки кода, введенного пользователем
|
||||||
router.post('/email/verify-code', async (req, res) => {
|
router.post('/email/verify-code', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { email, code } = req.body;
|
const { email, code } = req.body;
|
||||||
@@ -1032,14 +1086,14 @@ router.post('/email/verify-code', async (req, res) => {
|
|||||||
const emailBot = new EmailBotService(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD);
|
const emailBot = new EmailBotService(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD);
|
||||||
|
|
||||||
// Проверяем код из хранилища
|
// Проверяем код из хранилища
|
||||||
const verificationData = emailBot.verificationCodes.get(email.toLowerCase());
|
const verificationData = EmailBotService.verificationCodes.get(email.toLowerCase());
|
||||||
|
|
||||||
if (!verificationData) {
|
if (!verificationData) {
|
||||||
return res.status(400).json({ success: false, error: 'Код подтверждения не найден' });
|
return res.status(400).json({ success: false, error: 'Код подтверждения не найден' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() > verificationData.expires) {
|
if (Date.now() > verificationData.expires) {
|
||||||
emailBot.verificationCodes.delete(email.toLowerCase());
|
EmailBotService.verificationCodes.delete(email.toLowerCase());
|
||||||
return res.status(400).json({ success: false, error: 'Срок действия кода истек' });
|
return res.status(400).json({ success: false, error: 'Срок действия кода истек' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1083,21 +1137,8 @@ router.post('/email/verify-code', async (req, res) => {
|
|||||||
req.session.email = email.toLowerCase();
|
req.session.email = email.toLowerCase();
|
||||||
req.session.authType = 'email';
|
req.session.authType = 'email';
|
||||||
|
|
||||||
// Если был временный ID, удаляем его
|
|
||||||
if (req.session.tempUserId) {
|
|
||||||
delete req.session.tempUserId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем сессию
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
req.session.save(err => {
|
|
||||||
if (err) reject(err);
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удаляем код из хранилища
|
// Удаляем код из хранилища
|
||||||
emailBot.verificationCodes.delete(email.toLowerCase());
|
EmailBotService.verificationCodes.delete(email.toLowerCase());
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -1107,9 +1148,26 @@ router.post('/email/verify-code', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error verifying email code: ${error.message}`);
|
console.error('Error verifying email code:', error);
|
||||||
return res.status(500).json({ success: false, error: 'Ошибка сервера' });
|
return res.status(500).json({ success: false, error: 'Ошибка сервера' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Маршрут для очистки сессии
|
||||||
|
router.post('/clear-session', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Очищаем все данные сессии
|
||||||
|
req.session.destroy((err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error destroying session:', err);
|
||||||
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing session:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -6,6 +6,7 @@ const db = require('../db');
|
|||||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const { saveGuestMessageToDatabase } = require('../db');
|
||||||
|
|
||||||
// Добавьте эту функцию в начало файла chat.js
|
// Добавьте эту функцию в начало файла chat.js
|
||||||
async function getAIResponse(message, language = 'ru') {
|
async function getAIResponse(message, language = 'ru') {
|
||||||
@@ -66,149 +67,147 @@ async function getAIResponse(message, language = 'ru') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработчик сообщений чата
|
// Функция для обработки гостевых сообщений после аутентификации
|
||||||
|
async function processGuestMessages(userId, guestId) {
|
||||||
|
try {
|
||||||
|
console.log(`Starting to process guest messages for user ${userId} with guestId ${guestId}`);
|
||||||
|
|
||||||
|
// Получаем все гостевые сообщения
|
||||||
|
const guestMessages = await db.query(
|
||||||
|
`SELECT m.id, m.content, m.conversation_id, m.metadata, m.created_at
|
||||||
|
FROM messages m
|
||||||
|
WHERE m.metadata->>'guest_id' = $1
|
||||||
|
ORDER BY m.created_at ASC`,
|
||||||
|
[guestId]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Found ${guestMessages.rows.length} guest messages to process`);
|
||||||
|
|
||||||
|
// Обновляем user_id для всех бесед с гостевыми сообщениями
|
||||||
|
await db.query(
|
||||||
|
`UPDATE conversations c
|
||||||
|
SET user_id = $1
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT DISTINCT conversation_id
|
||||||
|
FROM messages m
|
||||||
|
WHERE m.metadata->>'guest_id' = $2
|
||||||
|
)`,
|
||||||
|
[userId, guestId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Обрабатываем каждое гостевое сообщение
|
||||||
|
for (const msg of guestMessages.rows) {
|
||||||
|
console.log(`Processing guest message ${msg.id}: ${msg.content}`);
|
||||||
|
|
||||||
|
// Получаем язык из метаданных
|
||||||
|
const metadata = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
|
||||||
|
const language = metadata?.language || 'ru';
|
||||||
|
|
||||||
|
// Получаем ответ от AI
|
||||||
|
console.log(`Getting AI response for message ${msg.id} in ${language}`);
|
||||||
|
const aiResponse = await getAIResponse(msg.content, language);
|
||||||
|
|
||||||
|
// Сохраняем ответ AI в ту же беседу
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO messages
|
||||||
|
(conversation_id, sender_type, content, channel, created_at)
|
||||||
|
VALUES ($1, 'assistant', $2, 'chat', NOW())`,
|
||||||
|
[msg.conversation_id, aiResponse]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Saved AI response for message ${msg.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Successfully processed all guest messages for user ${userId}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing guest messages:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик для гостевых сообщений
|
||||||
|
router.post('/guest-message', async (req, res) => {
|
||||||
|
const { message, language } = req.body;
|
||||||
|
|
||||||
|
// Генерируем временный ID сессии, если его нет
|
||||||
|
if (!req.session.guestId) {
|
||||||
|
req.session.guestId = crypto.randomBytes(16).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Создаем запись в conversations для гостя
|
||||||
|
const conversationResult = await db.query(
|
||||||
|
`INSERT INTO conversations (created_at)
|
||||||
|
VALUES (NOW())
|
||||||
|
RETURNING id`
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversationId = conversationResult.rows[0].id;
|
||||||
|
|
||||||
|
// Создаем метаданные
|
||||||
|
const metadata = {
|
||||||
|
guest_id: req.session.guestId,
|
||||||
|
language: language || 'en'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Сохраняем только сообщение пользователя
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO messages
|
||||||
|
(conversation_id, sender_type, content, channel, metadata, created_at)
|
||||||
|
VALUES ($1, 'guest', $2, 'chat', $3, NOW())`,
|
||||||
|
[
|
||||||
|
conversationId,
|
||||||
|
message,
|
||||||
|
JSON.stringify(metadata)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing message:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Маршрут для обычных сообщений (для аутентифицированных пользователей)
|
||||||
router.post('/message', requireAuth, async (req, res) => {
|
router.post('/message', requireAuth, async (req, res) => {
|
||||||
console.log('Сессия в /api/chat/message:', req.session);
|
const { message, language } = req.body;
|
||||||
console.log('Аутентифицирован:', req.session.authenticated);
|
const userId = req.session.userId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { message, language = 'ru' } = req.body;
|
// Создаем новую беседу или получаем существующую
|
||||||
const userId = typeof req.session.userId === 'object'
|
const conversationResult = await db.query(
|
||||||
? req.session.userId.userId
|
`INSERT INTO conversations (user_id, created_at)
|
||||||
: req.session.userId;
|
VALUES ($1, NOW())
|
||||||
|
RETURNING id`,
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`Получено сообщение: ${message}, язык: ${language}, userId: ${userId}`);
|
const conversationId = conversationResult.rows[0].id;
|
||||||
|
|
||||||
// Проверяем, что userId существует
|
|
||||||
if (!userId) {
|
|
||||||
return res.status(400).json({ error: 'User ID is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Определяем язык сообщения, если не указан явно
|
|
||||||
let detectedLanguage = language;
|
|
||||||
if (!language || language === 'auto') {
|
|
||||||
// Простая эвристика для определения языка
|
|
||||||
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
|
||||||
detectedLanguage = cyrillicPattern.test(message) ? 'ru' : 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Формируем системный промпт в зависимости от языка
|
|
||||||
let systemPrompt = '';
|
|
||||||
if (detectedLanguage === 'ru') {
|
|
||||||
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке.';
|
|
||||||
} else {
|
|
||||||
systemPrompt = 'You are a helpful assistant. Respond in English.';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляем запрос к Ollama с указанием языка
|
|
||||||
console.log(`Отправка запроса к Ollama (модель: ${process.env.OLLAMA_MODEL || 'mistral'}, язык: ${detectedLanguage}): ${message}`);
|
|
||||||
|
|
||||||
// Проверяем доступность Ollama
|
|
||||||
console.log('Проверка доступности Ollama...');
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/tags`);
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Ollama доступен. Доступные модели:');
|
|
||||||
data.models.forEach(model => {
|
|
||||||
console.log(`- ${model.name}`);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при проверке доступности Ollama:', error);
|
|
||||||
return res.status(500).json({ error: 'Сервис Ollama недоступен' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем экземпляр ChatOllama
|
|
||||||
const chat = new ChatOllama({
|
|
||||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
|
||||||
model: process.env.OLLAMA_MODEL || 'mistral',
|
|
||||||
system: systemPrompt
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Отправка запроса к Ollama...');
|
|
||||||
|
|
||||||
// Получаем ответ от модели
|
|
||||||
let aiResponse;
|
|
||||||
try {
|
|
||||||
const response = await chat.invoke(message);
|
|
||||||
aiResponse = response.content;
|
|
||||||
console.log('Ответ AI:', aiResponse);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при вызове ChatOllama:', error);
|
|
||||||
|
|
||||||
// Альтернативный метод запроса через прямой API
|
|
||||||
try {
|
|
||||||
console.log('Пробуем альтернативный метод запроса...');
|
|
||||||
const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/generate`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: process.env.OLLAMA_MODEL || 'mistral',
|
|
||||||
prompt: message,
|
|
||||||
system: systemPrompt,
|
|
||||||
stream: false
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
aiResponse = data.response;
|
|
||||||
console.log('Ответ AI (альтернативный метод):', aiResponse);
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.error('Ошибка при использовании альтернативного метода:', fallbackError);
|
|
||||||
throw error; // Выбрасываем исходную ошибку
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получаем или создаем диалог
|
|
||||||
let conversationId;
|
|
||||||
const conversationResult = await db.query(`
|
|
||||||
SELECT id FROM conversations
|
|
||||||
WHERE user_id = $1
|
|
||||||
ORDER BY updated_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
`, [userId]);
|
|
||||||
|
|
||||||
if (conversationResult.rows.length === 0) {
|
|
||||||
// Создаем новый диалог
|
|
||||||
const newConversationResult = await db.query(`
|
|
||||||
INSERT INTO conversations (user_id, created_at, updated_at)
|
|
||||||
VALUES ($1, NOW(), NOW())
|
|
||||||
RETURNING id
|
|
||||||
`, [userId]);
|
|
||||||
conversationId = newConversationResult.rows[0].id;
|
|
||||||
console.log('Created new conversation:', conversationId);
|
|
||||||
} else {
|
|
||||||
conversationId = conversationResult.rows[0].id;
|
|
||||||
console.log('Using existing conversation:', conversationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем сообщение пользователя
|
// Сохраняем сообщение пользователя
|
||||||
const userMessageResult = await db.query(`
|
await db.query(
|
||||||
INSERT INTO messages (conversation_id, sender_type, sender_id, content, channel, created_at)
|
`INSERT INTO messages
|
||||||
VALUES ($1, 'user', $2, $3, 'web', NOW())
|
(conversation_id, sender_type, content, channel, created_at)
|
||||||
RETURNING id
|
VALUES ($1, 'user', $2, 'chat', NOW())`,
|
||||||
`, [conversationId, userId, message]);
|
[conversationId, message]
|
||||||
console.log('Saved user message:', userMessageResult.rows[0].id);
|
);
|
||||||
|
|
||||||
// Сохраняем ответ ИИ
|
// Получаем ответ от AI
|
||||||
const aiMessageResult = await db.query(`
|
const aiResponse = await getAIResponse(message, language);
|
||||||
INSERT INTO messages (conversation_id, sender_type, content, channel, created_at)
|
|
||||||
VALUES ($1, 'ai', $2, 'web', NOW())
|
|
||||||
RETURNING id
|
|
||||||
`, [conversationId, aiResponse]);
|
|
||||||
console.log('Saved AI message:', aiMessageResult.rows[0].id);
|
|
||||||
|
|
||||||
// Обновляем время последнего сообщения в диалоге
|
// Сохраняем ответ AI
|
||||||
await db.query(`
|
await db.query(
|
||||||
UPDATE conversations
|
`INSERT INTO messages
|
||||||
SET updated_at = NOW()
|
(conversation_id, sender_type, content, channel, created_at)
|
||||||
WHERE id = $1
|
VALUES ($1, 'assistant', $2, 'chat', NOW())`,
|
||||||
`, [conversationId]);
|
[conversationId, aiResponse]
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
reply: aiResponse,
|
success: true,
|
||||||
language: detectedLanguage
|
message: aiResponse
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing message:', error);
|
console.error('Error processing message:', error);
|
||||||
@@ -233,82 +232,49 @@ router.get('/models', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Получение истории сообщений
|
// Получение истории сообщений
|
||||||
router.get('/history', requireAuth, async (req, res) => {
|
router.get('/history', async (req, res) => {
|
||||||
|
const limit = parseInt(req.query.limit) || 2; // По умолчанию только последнее сообщение и ответ
|
||||||
|
const offset = parseInt(req.query.offset) || 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Получаем ID пользователя из сессии или из объекта пользователя
|
if (!req.session.authenticated || !req.session.userId) {
|
||||||
const userId = req.session?.userId || req.user?.userId;
|
|
||||||
|
|
||||||
console.log('Запрос истории чата для пользователя:', userId);
|
|
||||||
console.log('User object from request:', req.user);
|
|
||||||
|
|
||||||
// Проверяем, что userId существует
|
|
||||||
if (!userId) {
|
|
||||||
console.error('Пользователь не аутентифицирован');
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем историю сообщений из базы данных
|
// Получаем общее количество сообщений
|
||||||
console.log('Querying chat history for user:', userId);
|
const countResult = await db.query(
|
||||||
|
`SELECT COUNT(*) as total
|
||||||
// Проверяем, существует ли таблица messages
|
|
||||||
try {
|
|
||||||
const tableCheck = await db.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_name = 'messages'
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('Table messages exists:', tableCheck.rows[0].exists);
|
|
||||||
|
|
||||||
if (tableCheck.rows[0].exists) {
|
|
||||||
// Используем таблицу messages
|
|
||||||
const result = await db.query(`
|
|
||||||
SELECT m.*, c.user_id
|
|
||||||
FROM messages m
|
FROM messages m
|
||||||
JOIN conversations c ON m.conversation_id = c.id
|
JOIN conversations c ON m.conversation_id = c.id
|
||||||
WHERE c.user_id = $1
|
WHERE c.user_id = $1
|
||||||
ORDER BY m.created_at ASC
|
OR (m.metadata->>'guest_id' = $2 AND m.metadata->>'processed' = 'true')`,
|
||||||
`, [userId]);
|
[req.session.userId, req.session.guestId]
|
||||||
|
|
||||||
console.log(`Найдено ${result.rows.length} сообщений для пользователя ${userId}`);
|
|
||||||
|
|
||||||
return res.json({ messages: result.rows });
|
|
||||||
} else {
|
|
||||||
// Проверяем, существует ли таблица chat_history
|
|
||||||
const chatHistoryCheck = await db.query(`
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT FROM information_schema.tables
|
|
||||||
WHERE table_name = 'chat_history'
|
|
||||||
);
|
);
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('Table chat_history exists:', chatHistoryCheck.rows[0].exists);
|
const total = parseInt(countResult.rows[0].total);
|
||||||
|
|
||||||
if (chatHistoryCheck.rows[0].exists) {
|
// Получаем сообщения с пагинацией
|
||||||
// Используем таблицу chat_history
|
const result = await db.query(
|
||||||
const result = await db.query(`
|
`SELECT m.id, m.content, m.sender_type as role, m.created_at,
|
||||||
SELECT * FROM chat_history
|
c.user_id, m.metadata
|
||||||
WHERE user_id = $1
|
FROM messages m
|
||||||
ORDER BY created_at ASC
|
JOIN conversations c ON m.conversation_id = c.id
|
||||||
`, [userId]);
|
WHERE c.user_id = $1
|
||||||
|
OR (m.metadata->>'guest_id' = $2 AND m.metadata->>'processed' = 'true')
|
||||||
|
ORDER BY m.created_at DESC
|
||||||
|
LIMIT $3 OFFSET $4`,
|
||||||
|
[req.session.userId, req.session.guestId, limit, offset]
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`Найдено ${result.rows.length} сообщений для пользователя ${userId}`);
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
messages: result.rows.reverse(),
|
||||||
|
total
|
||||||
|
});
|
||||||
|
|
||||||
return res.json({ messages: result.rows });
|
|
||||||
} else {
|
|
||||||
// Ни одна из таблиц не существует
|
|
||||||
console.log('No message tables found in database');
|
|
||||||
return res.json({ messages: [] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking tables:', error);
|
console.error('Error getting chat history:', error);
|
||||||
return res.json({ messages: [] });
|
return res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching chat history:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -345,39 +311,6 @@ router.get('/admin/history', requireAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработчик для гостевых сообщений
|
|
||||||
router.post('/guest-message', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { message, language } = req.body;
|
|
||||||
console.log(`Получено гостевое сообщение: ${message} язык: ${language}`);
|
|
||||||
|
|
||||||
// Генерируем временный ID сессии, если его нет
|
|
||||||
if (!req.session.guestId) {
|
|
||||||
req.session.guestId = crypto.randomBytes(16).toString('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем сообщение в базе данных с временным ID
|
|
||||||
await db.query(`
|
|
||||||
INSERT INTO guest_messages (guest_id, content, language, created_at)
|
|
||||||
VALUES ($1, $2, $3, NOW())
|
|
||||||
`, [req.session.guestId, message, language]);
|
|
||||||
|
|
||||||
// Отправляем запрос к AI
|
|
||||||
const aiResponse = await getAIResponse(message, language);
|
|
||||||
|
|
||||||
// Сохраняем ответ AI в базе данных
|
|
||||||
await db.query(`
|
|
||||||
INSERT INTO guest_messages (guest_id, content, language, created_at, is_ai)
|
|
||||||
VALUES ($1, $2, $3, NOW(), true)
|
|
||||||
`, [req.session.guestId, aiResponse, language]);
|
|
||||||
|
|
||||||
return res.json({ message: aiResponse, reply: aiResponse });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing guest message:', error);
|
|
||||||
return res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обработчик для связывания гостевых сообщений с пользователем
|
// Обработчик для связывания гостевых сообщений с пользователем
|
||||||
router.post('/link-guest-messages', requireAuth, async (req, res) => {
|
router.post('/link-guest-messages', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -413,4 +346,64 @@ router.post('/link-guest-messages', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обновляем маршрут верификации кошелька
|
||||||
|
router.post('/verify', async (req, res) => {
|
||||||
|
const { address, signature, message } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ... существующий код верификации ...
|
||||||
|
|
||||||
|
// После успешной верификации и создания пользователя
|
||||||
|
if (req.session.guestId) {
|
||||||
|
console.log('Found guest messages, processing...');
|
||||||
|
await processGuestMessages(userId, req.session.guestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем данные в сессии
|
||||||
|
req.session.userId = userId;
|
||||||
|
req.session.address = address;
|
||||||
|
req.session.isAdmin = isAdmin;
|
||||||
|
req.session.authenticated = true;
|
||||||
|
|
||||||
|
console.log('Authentication successful for user:', {
|
||||||
|
userId,
|
||||||
|
address,
|
||||||
|
isAdmin,
|
||||||
|
guestId: req.session.guestId
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
authenticated: true,
|
||||||
|
userId: userId,
|
||||||
|
address: address,
|
||||||
|
isAdmin: isAdmin,
|
||||||
|
authType: 'wallet'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during wallet verification:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем маршрут верификации Telegram
|
||||||
|
router.post('/auth/telegram/verify', async (req, res) => {
|
||||||
|
// ... существующий код ...
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Если есть гостевые сообщения, обрабатываем их
|
||||||
|
if (req.session.guestId) {
|
||||||
|
await processGuestMessages(userId, req.session.guestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
userId: userId,
|
||||||
|
telegramId: result.telegramId,
|
||||||
|
isAdmin: req.session.isAdmin || false,
|
||||||
|
authenticated: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const db = require('../db');
|
const db = require('../db');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
|
const { processMessage } = require('./ai-assistant'); // Используем AI Assistant
|
||||||
|
|
||||||
// В начале файла auth-service.js
|
// В начале файла auth-service.js
|
||||||
const getProvider = (network) => {
|
const getProvider = (network) => {
|
||||||
@@ -246,6 +247,108 @@ class AuthService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обрабатывает гостевые сообщения после аутентификации
|
||||||
|
*/
|
||||||
|
async processGuestMessages(userId, guestId) {
|
||||||
|
try {
|
||||||
|
logger.info(`Processing guest messages for user ${userId} with guestId ${guestId}`);
|
||||||
|
|
||||||
|
// Сначала обновляем user_id для всех бесед с гостевыми сообщениями
|
||||||
|
await db.query(
|
||||||
|
`UPDATE conversations c
|
||||||
|
SET user_id = $1
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT DISTINCT conversation_id
|
||||||
|
FROM messages m
|
||||||
|
WHERE m.metadata->>'guest_id' = $2
|
||||||
|
)`,
|
||||||
|
[userId, guestId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Получаем все гостевые сообщения без ответов
|
||||||
|
const guestMessages = await db.query(
|
||||||
|
`SELECT m.id, m.content, m.conversation_id, m.metadata, m.created_at
|
||||||
|
FROM messages m
|
||||||
|
WHERE m.metadata->>'guest_id' = $1
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM messages
|
||||||
|
WHERE conversation_id = m.conversation_id
|
||||||
|
AND sender_type = 'assistant'
|
||||||
|
)
|
||||||
|
ORDER BY m.created_at ASC`,
|
||||||
|
[guestId]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Found ${guestMessages.rows.length} unprocessed guest messages`);
|
||||||
|
|
||||||
|
// Обрабатываем каждое гостевое сообщение
|
||||||
|
for (const msg of guestMessages.rows) {
|
||||||
|
logger.info(`Processing guest message ${msg.id}: ${msg.content}`);
|
||||||
|
|
||||||
|
// Получаем язык из метаданных
|
||||||
|
const metadata = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : msg.metadata;
|
||||||
|
const language = metadata?.language || 'ru';
|
||||||
|
|
||||||
|
// Используем AI Assistant для обработки сообщения
|
||||||
|
const aiResponse = await processMessage(userId, msg.content, language);
|
||||||
|
|
||||||
|
// Сохраняем ответ AI в ту же беседу
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO messages
|
||||||
|
(conversation_id, sender_type, content, channel, created_at)
|
||||||
|
VALUES ($1, 'assistant', $2, 'chat', NOW())`,
|
||||||
|
[msg.conversation_id, aiResponse]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Saved AI response for message ${msg.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем метаданные сообщений, чтобы показать, что они обработаны
|
||||||
|
await db.query(
|
||||||
|
`UPDATE messages m
|
||||||
|
SET metadata = jsonb_set(
|
||||||
|
CASE
|
||||||
|
WHEN m.metadata IS NULL THEN '{}'::jsonb
|
||||||
|
ELSE m.metadata::jsonb
|
||||||
|
END,
|
||||||
|
'{processed}',
|
||||||
|
'true'
|
||||||
|
)
|
||||||
|
WHERE m.metadata->>'guest_id' = $1`,
|
||||||
|
[guestId]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Successfully processed all guest messages for user ${userId}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error processing guest messages:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnect() {
|
||||||
|
try {
|
||||||
|
// Очищаем состояние аутентификации
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
this.userId = null;
|
||||||
|
this.address = null;
|
||||||
|
this.isAdmin = false;
|
||||||
|
this.authType = null;
|
||||||
|
|
||||||
|
// Очищаем сессию
|
||||||
|
localStorage.removeItem('auth');
|
||||||
|
|
||||||
|
// Очищаем guestId
|
||||||
|
localStorage.removeItem('guestId');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error during disconnect:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new AuthService();
|
module.exports = new AuthService();
|
||||||
@@ -1,273 +1,218 @@
|
|||||||
const TelegramBot = require('node-telegram-bot-api');
|
const TelegramBot = require('node-telegram-bot-api');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
const { pool } = require('../db');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
// Создаем бота
|
// Создаем бота
|
||||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
let bot = null;
|
let bot = null;
|
||||||
|
|
||||||
// Добавим хранилище для кодов подтверждения
|
/**
|
||||||
const verificationCodes = new Map(); // Формат: { telegramId: { code: '123456', token: 'auth_token', expires: timestamp } }
|
* Функция для отправки кода подтверждения
|
||||||
|
*/
|
||||||
|
async function sendVerificationCode(chatId) {
|
||||||
|
try {
|
||||||
|
// Генерируем код и токен
|
||||||
|
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||||
|
const authToken = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
// Создаем пользователя и сохраняем код в базу данных
|
||||||
|
const result = await pool.query(
|
||||||
|
`WITH new_user AS (
|
||||||
|
INSERT INTO users (created_at)
|
||||||
|
VALUES (NOW())
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
INSERT INTO telegram_auth_tokens
|
||||||
|
(user_id, token, verification_code, telegram_id, expires_at)
|
||||||
|
VALUES (
|
||||||
|
(SELECT id FROM new_user),
|
||||||
|
$1, $2, $3,
|
||||||
|
NOW() + INTERVAL '5 minutes'
|
||||||
|
)
|
||||||
|
RETURNING user_id`,
|
||||||
|
[authToken, code, chatId.toString()]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Отправляем код с инлайн-кнопкой
|
||||||
|
const sentMessage = await bot.sendMessage(chatId,
|
||||||
|
'Привет! Я бот для аутентификации в DApp for Business.\n\n' +
|
||||||
|
'🔐 Ваш код подтверждения:\n\n' +
|
||||||
|
`<code>${code}</code>\n\n` +
|
||||||
|
'Введите этот код на сайте для завершения авторизации.\n' +
|
||||||
|
'Код действителен в течение 5 минут.',
|
||||||
|
{
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[{ text: '🔄 Получить новый код', callback_data: 'new_code' }]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Удаляем сообщение через 30 секунд
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await bot.deleteMessage(chatId, sentMessage.message_id);
|
||||||
|
await bot.sendMessage(chatId,
|
||||||
|
'Для получения нового кода используйте команду /start или меню команд',
|
||||||
|
{
|
||||||
|
reply_markup: {
|
||||||
|
keyboard: [
|
||||||
|
[{ text: '/start' }]
|
||||||
|
],
|
||||||
|
resize_keyboard: true,
|
||||||
|
persistent: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting message:', error);
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return { code, token: authToken, userId: result.rows[0].user_id };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending verification code:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Функция для проверки кода
|
||||||
|
*/
|
||||||
|
async function verifyCode(code) {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT token, telegram_id, user_id
|
||||||
|
FROM telegram_auth_tokens
|
||||||
|
WHERE verification_code = $1
|
||||||
|
AND expires_at > NOW()
|
||||||
|
AND NOT used`,
|
||||||
|
[code]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return { success: false, error: 'Неверный или истекший код' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { token, telegram_id, user_id } = result.rows[0];
|
||||||
|
|
||||||
|
// Помечаем токен как использованный
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE telegram_auth_tokens SET used = true WHERE token = $1',
|
||||||
|
[token]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Добавляем Telegram ID в таблицу идентификаторов
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO user_identities
|
||||||
|
(user_id, identity_type, identity_value, verified, created_at)
|
||||||
|
VALUES ($1, 'telegram', $2, true, NOW())
|
||||||
|
ON CONFLICT (identity_type, identity_value)
|
||||||
|
DO UPDATE SET verified = true`,
|
||||||
|
[user_id, telegram_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
telegramId: telegram_id,
|
||||||
|
userId: user_id
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying code:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Инициализация Telegram бота
|
* Инициализация Telegram бота
|
||||||
* @returns {Object|null} - Объект с методами для работы с ботом или null, если инициализация не удалась
|
|
||||||
*/
|
*/
|
||||||
function initTelegramBot() {
|
function initTelegramBot() {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.warn('TELEGRAM_BOT_TOKEN not set, Telegram integration disabled');
|
console.warn('TELEGRAM_BOT_TOKEN not set');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Создаем бота с опцией polling
|
// Создаем бота с опцией обработки ошибок
|
||||||
bot = new TelegramBot(token, { polling: true });
|
bot = new TelegramBot(token, {
|
||||||
|
polling: {
|
||||||
|
autoStart: true,
|
||||||
|
params: {
|
||||||
|
timeout: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
timeout: 30000, // увеличиваем таймаут до 30 секунд
|
||||||
|
proxy: process.env.HTTPS_PROXY // используем прокси если есть
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
console.log('Telegram bot initialized');
|
console.log('Telegram bot initialized');
|
||||||
|
|
||||||
// Регистрируем обработчики событий
|
// Очищаем все предыдущие обработчики
|
||||||
registerHandlers();
|
bot.removeAllListeners();
|
||||||
|
|
||||||
|
// Устанавливаем команды бота с обработкой ошибок
|
||||||
|
bot.setMyCommands([
|
||||||
|
{ command: '/start', description: 'Получить код подтверждения' },
|
||||||
|
{ command: '/help', description: 'Показать справку' }
|
||||||
|
]).catch(error => {
|
||||||
|
console.warn('Error setting bot commands:', error);
|
||||||
|
// Продолжаем работу даже если не удалось установить команды
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик команды /start
|
||||||
|
bot.onText(/\/start/, async (msg) => {
|
||||||
|
const chatId = msg.chat.id;
|
||||||
|
try {
|
||||||
|
await sendVerificationCode(chatId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling /start:', error);
|
||||||
|
await bot.sendMessage(chatId, 'Произошла ошибка. Пожалуйста, попробуйте позже.')
|
||||||
|
.catch(err => console.error('Error sending error message:', err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик ошибок polling
|
||||||
|
bot.on('polling_error', (error) => {
|
||||||
|
console.error('Telegram bot polling error:', error);
|
||||||
|
// Перезапускаем polling при ошибке
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
bot.startPolling();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error restarting polling:', e);
|
||||||
|
}
|
||||||
|
}, 10000); // пробуем перезапустить через 10 секунд
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик остановки polling
|
||||||
|
bot.on('stop', () => {
|
||||||
|
console.log('Bot polling stopped');
|
||||||
|
// Пробуем перезапустить
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
bot.startPolling();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error restarting polling after stop:', e);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
|
||||||
return {
|
|
||||||
bot,
|
|
||||||
sendMessage: (chatId, text) => bot.sendMessage(chatId, text)
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing Telegram bot:', error);
|
console.error('Error initializing Telegram bot:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Экспортируем функции
|
||||||
* Регистрация обработчиков событий для бота
|
|
||||||
*/
|
|
||||||
function registerHandlers() {
|
|
||||||
// Обработчик /start
|
|
||||||
bot.onText(/\/start(.*)/, async (msg, match) => {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const param = match[1] ? match[1].trim() : '';
|
|
||||||
|
|
||||||
console.log(`Получена команда /start с параметром: "${param}" от пользователя ${chatId}`);
|
|
||||||
|
|
||||||
if (param.startsWith('auth_')) {
|
|
||||||
// Это токен авторизации через deep link
|
|
||||||
const authToken = param.replace('auth_', '');
|
|
||||||
console.log(`Обработка токена авторизации: ${authToken}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем, существует ли токен
|
|
||||||
const { pool } = require('../db');
|
|
||||||
const tokenResult = await pool.query(
|
|
||||||
'SELECT user_id, expires_at FROM telegram_auth_tokens WHERE token = $1',
|
|
||||||
[authToken]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tokenResult.rows.length === 0 || new Date(tokenResult.rows[0].expires_at) < new Date()) {
|
|
||||||
bot.sendMessage(chatId, '❌ Недействительный или истекший токен авторизации.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Генерируем код подтверждения
|
|
||||||
const verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); // 6-значный код
|
|
||||||
|
|
||||||
// Сохраняем в хранилище
|
|
||||||
verificationCodes.set(chatId.toString(), {
|
|
||||||
code: verificationCode,
|
|
||||||
token: authToken,
|
|
||||||
expires: Date.now() + 5 * 60 * 1000 // Срок действия 5 минут
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отправляем код пользователю
|
|
||||||
bot.sendMessage(chatId,
|
|
||||||
'🔐 Для завершения связывания аккаунта, пожалуйста, введите этот код:\n\n' +
|
|
||||||
`<code>${verificationCode}</code>\n\n` +
|
|
||||||
'Код действителен в течение 5 минут.',
|
|
||||||
{ parse_mode: 'HTML' }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing auth token:', error);
|
|
||||||
bot.sendMessage(chatId, '❌ Произошла ошибка при обработке запроса авторизации.');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Получаем последний активный токен для этого чата, если есть
|
|
||||||
const { pool } = require('../db');
|
|
||||||
try {
|
|
||||||
const lastTokenResult = await pool.query(`
|
|
||||||
SELECT token FROM telegram_auth_tokens
|
|
||||||
WHERE expires_at > NOW() AND used = FALSE
|
|
||||||
ORDER BY created_at DESC LIMIT 1
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (lastTokenResult.rows.length > 0) {
|
|
||||||
const authToken = lastTokenResult.rows[0].token;
|
|
||||||
|
|
||||||
// Генерируем код подтверждения
|
|
||||||
const verificationCode = Math.floor(100000 + Math.random() * 900000).toString(); // 6-значный код
|
|
||||||
|
|
||||||
// Сохраняем в хранилище
|
|
||||||
verificationCodes.set(chatId.toString(), {
|
|
||||||
code: verificationCode,
|
|
||||||
token: authToken,
|
|
||||||
expires: Date.now() + 5 * 60 * 1000 // Срок действия 5 минут
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отправляем код пользователю
|
|
||||||
bot.sendMessage(chatId,
|
|
||||||
'🔐 Для завершения связывания аккаунта, пожалуйста, введите этот код:\n\n' +
|
|
||||||
`<code>${verificationCode}</code>\n\n` +
|
|
||||||
'Код действителен в течение 5 минут.',
|
|
||||||
{ parse_mode: 'HTML' }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking last token:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если нет активного токена, отправляем стандартное сообщение
|
|
||||||
bot.sendMessage(chatId,
|
|
||||||
'Привет! Я бот для аутентификации в DApp for Business.\n\n' +
|
|
||||||
'Для связи с вашим аккаунтом используйте кнопку на сайте.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обработчик для проверки кода подтверждения
|
|
||||||
bot.on('message', async (msg) => {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const text = msg.text;
|
|
||||||
|
|
||||||
// Игнорируем команды
|
|
||||||
if (text && text.startsWith('/')) return;
|
|
||||||
|
|
||||||
// Проверяем, есть ли ожидающая верификация для этого чата
|
|
||||||
const verificationData = verificationCodes.get(chatId.toString());
|
|
||||||
|
|
||||||
if (verificationData && text === verificationData.code) {
|
|
||||||
// Код верный, проверяем срок действия
|
|
||||||
if (Date.now() > verificationData.expires) {
|
|
||||||
bot.sendMessage(chatId, '❌ Срок действия кода истек. Пожалуйста, начните процесс заново.');
|
|
||||||
verificationCodes.delete(chatId.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Код верный и актуальный, завершаем аутентификацию
|
|
||||||
try {
|
|
||||||
const result = await linkTelegramAccount(chatId.toString(), verificationData.token);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
bot.sendMessage(chatId,
|
|
||||||
'✅ Аутентификация успешна!\n\n' +
|
|
||||||
'Ваш Telegram аккаунт связан с DApp for Business.\n' +
|
|
||||||
'Теперь вы можете использовать бота для общения с системой.'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
bot.sendMessage(chatId,
|
|
||||||
'❌ Ошибка аутентификации: ' + (result.error || 'неизвестная ошибка')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем данные верификации
|
|
||||||
verificationCodes.delete(chatId.toString());
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error completing authentication:', error);
|
|
||||||
bot.sendMessage(chatId, '❌ Произошла ошибка при завершении аутентификации.');
|
|
||||||
}
|
|
||||||
} else if (verificationData) {
|
|
||||||
// Есть ожидающая верификация, но код неверный
|
|
||||||
bot.sendMessage(chatId, '❌ Неверный код. Пожалуйста, попробуйте еще раз.');
|
|
||||||
} else {
|
|
||||||
// Нет ожидающей верификации
|
|
||||||
bot.sendMessage(chatId, 'Я могу помочь с аутентификацией. Используйте кнопку на сайте для начала процесса.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Добавить обработку прямых команд аутентификации
|
|
||||||
bot.onText(/\/auth (.+)/, async (msg, match) => {
|
|
||||||
const chatId = msg.chat.id;
|
|
||||||
const authToken = match[1].trim();
|
|
||||||
|
|
||||||
console.log(`Получена прямая команда авторизации с токеном: ${authToken}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Связываем Telegram ID с аккаунтом по токену
|
|
||||||
const result = await linkTelegramAccount(chatId.toString(), authToken);
|
|
||||||
console.log(`Результат связывания: ${JSON.stringify(result)}`);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
bot.sendMessage(chatId,
|
|
||||||
'✅ Аутентификация успешна!\n\n' +
|
|
||||||
'Ваш Telegram аккаунт связан с DApp for Business.\n' +
|
|
||||||
'Теперь вы можете использовать бота для общения с системой.'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
bot.sendMessage(chatId,
|
|
||||||
'❌ Ошибка аутентификации: ' + (result.error || 'неизвестная ошибка')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error linking telegram account:', error);
|
|
||||||
bot.sendMessage(chatId, '❌ Произошла ошибка при связывании аккаунта.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обработка ошибок
|
|
||||||
bot.on('polling_error', (error) => {
|
|
||||||
logger.error(`[polling_error] ${JSON.stringify(error)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Telegram bot handlers registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Связывание Telegram ID с аккаунтом пользователя
|
|
||||||
* @param {string} telegramId - ID пользователя в Telegram
|
|
||||||
* @param {string} authToken - Токен авторизации
|
|
||||||
* @returns {Promise<Object>} - Результат операции
|
|
||||||
*/
|
|
||||||
async function linkTelegramAccount(telegramId, authToken) {
|
|
||||||
try {
|
|
||||||
console.log(`Попытка связать Telegram ID ${telegramId} с токеном ${authToken}`);
|
|
||||||
|
|
||||||
// Здесь должен быть код для связывания через API или напрямую с БД
|
|
||||||
const { pool } = require('../db');
|
|
||||||
|
|
||||||
// Проверяем токен авторизации
|
|
||||||
const tokenResult = await pool.query(
|
|
||||||
'SELECT user_id, expires_at FROM telegram_auth_tokens WHERE token = $1',
|
|
||||||
[authToken]
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Результат запроса токена: ${JSON.stringify(tokenResult.rows)}`);
|
|
||||||
|
|
||||||
if (tokenResult.rows.length === 0 || new Date(tokenResult.rows[0].expires_at) < new Date()) {
|
|
||||||
console.log('Токен не найден или истек');
|
|
||||||
return { success: false, error: 'Недействительный или истекший токен' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = tokenResult.rows[0].user_id;
|
|
||||||
console.log(`Найден пользователь с ID: ${userId}`);
|
|
||||||
|
|
||||||
// Добавляем идентификатор Telegram для пользователя
|
|
||||||
await pool.query(
|
|
||||||
'INSERT INTO user_identities (user_id, identity_type, identity_value, verified, created_at) ' +
|
|
||||||
'VALUES ($1, $2, $3, true, NOW()) ' +
|
|
||||||
'ON CONFLICT (identity_type, identity_value) ' +
|
|
||||||
'DO UPDATE SET user_id = $1, verified = true',
|
|
||||||
[userId, 'telegram', telegramId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Отмечаем токен как использованный
|
|
||||||
await pool.query(
|
|
||||||
'UPDATE telegram_auth_tokens SET used = true WHERE token = $1',
|
|
||||||
[authToken]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in linkTelegramAccount:', error);
|
|
||||||
return { success: false, error: 'Внутренняя ошибка сервера' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initTelegramBot
|
initTelegramBot,
|
||||||
|
verifyCode,
|
||||||
|
sendVerificationCode
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { useAuthStore } from '../stores/auth';
|
||||||
|
|
||||||
// Создаем экземпляр axios с базовым URL
|
// Создаем экземпляр axios с базовым URL
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
@@ -13,11 +14,22 @@ const instance = axios.create({
|
|||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
console.log('Axios interceptor running');
|
console.log('Axios interceptor running');
|
||||||
const address = localStorage.getItem('walletAddress');
|
const authStore = useAuthStore();
|
||||||
if (address) {
|
|
||||||
console.log('Adding Authorization header in interceptor:', `Bearer ${address}`);
|
// Логируем параметры запроса
|
||||||
config.headers.Authorization = `Bearer ${address}`;
|
console.log('Request parameters:', config);
|
||||||
|
|
||||||
|
// Если уже есть заголовок Authorization, не перезаписываем его
|
||||||
|
if (config.headers.Authorization) {
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Если пользователь аутентифицирован и есть адрес кошелька
|
||||||
|
if (authStore.isAuthenticated && authStore.address) {
|
||||||
|
console.log('Adding Authorization header:', `Bearer ${authStore.address}`);
|
||||||
|
config.headers.Authorization = `Bearer ${authStore.address}`;
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -25,4 +37,36 @@ instance.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Добавляем перехватчик для обработки ответов
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
console.log('Response from server:', response.data);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Проверяем, что это действительно ошибка авторизации
|
||||||
|
if (error.response?.status === 401 &&
|
||||||
|
!error.config.url.includes('/auth/') &&
|
||||||
|
!error.config.url.includes('/verify') &&
|
||||||
|
!error.config.url.includes('/chat/history')) { // Не очищаем при ошибке загрузки истории
|
||||||
|
console.log('Auth error, clearing state');
|
||||||
|
const auth = useAuthStore();
|
||||||
|
auth.disconnect();
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Пример функции для отправки гостевого сообщения на сервер
|
||||||
|
const sendGuestMessageToServer = async (messageText) => {
|
||||||
|
try {
|
||||||
|
await axios.post('/api/chat/guest-message', {
|
||||||
|
message: messageText,
|
||||||
|
language: userLanguage.value
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при отправке гостевого сообщения на сервер:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default instance;
|
export default instance;
|
||||||
@@ -1,108 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="telegram-auth">
|
<div class="telegram-auth">
|
||||||
<div v-if="!isAuthenticating">
|
<button v-if="!showVerification" class="auth-btn telegram-btn" @click="startTelegramAuth">
|
||||||
<a :href="telegramBotLink" target="_blank" class="telegram-btn" @click="startAuth">
|
|
||||||
<span class="auth-icon">📱</span> Подключить Telegram
|
<span class="auth-icon">📱</span> Подключить Telegram
|
||||||
</a>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="auth-progress">
|
<div v-else class="verification-form">
|
||||||
<p>Для завершения авторизации:</p>
|
<input
|
||||||
<ol>
|
type="text"
|
||||||
<li>Перейдите в Telegram-бота <strong>@{{ botUsername }}</strong></li>
|
v-model="verificationCode"
|
||||||
<li>Если бот не открылся автоматически, скопируйте и отправьте ему команду:</li>
|
placeholder="Введите код из Telegram"
|
||||||
</ol>
|
/>
|
||||||
|
<button class="auth-btn verify-btn" @click="verifyCode">Подтвердить</button>
|
||||||
<div class="auth-code">
|
<button class="auth-btn cancel-btn" @click="cancelVerification">Отмена</button>
|
||||||
/auth {{ authToken }}
|
|
||||||
</div>
|
|
||||||
<button class="copy-btn" @click="copyAuthCommand">Копировать команду</button>
|
|
||||||
|
|
||||||
<div class="auth-actions">
|
|
||||||
<button class="cancel-btn" @click="cancelAuth">Отмена</button>
|
|
||||||
<button class="check-btn" @click="checkAuthStatus">Проверить статус</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="errorMessage" class="error-message">
|
|
||||||
{{ errorMessage }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
|
import axios from '../api/axios';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
const showVerification = ref(false);
|
||||||
|
const verificationCode = ref('');
|
||||||
|
|
||||||
const isAuthenticating = ref(false);
|
const startTelegramAuth = () => {
|
||||||
const authToken = ref('');
|
// Открываем Telegram бота в новом окне
|
||||||
const botUsername = ref(process.env.VUE_APP_TELEGRAM_BOT_USERNAME || 'HB3_Accelerator_Bot');
|
window.open('https://t.me/your_bot_username', '_blank');
|
||||||
const errorMessage = ref('');
|
showVerification.value = true;
|
||||||
const checkInterval = ref(null);
|
};
|
||||||
|
|
||||||
// Формируем ссылку на бота с параметром авторизации
|
const verifyCode = async () => {
|
||||||
const telegramBotLink = computed(() => {
|
|
||||||
// Возвращаем ссылку только если есть токен
|
|
||||||
if (!authToken.value) return `https://t.me/${botUsername.value}`;
|
|
||||||
return `https://t.me/${botUsername.value}?start=auth_${authToken.value}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function startAuth() {
|
|
||||||
try {
|
try {
|
||||||
// Сначала запрашиваем токен
|
const response = await axios.post('/api/auth/telegram/verify', {
|
||||||
const response = await auth.createTelegramAuthToken();
|
code: verificationCode.value
|
||||||
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.data.success) {
|
||||||
authToken.value = response.token;
|
auth.setTelegramAuth(response.data);
|
||||||
|
|
||||||
// Теперь можно включить режим авторизации
|
|
||||||
isAuthenticating.value = true;
|
|
||||||
|
|
||||||
// И запустить проверку
|
|
||||||
checkInterval.value = setInterval(checkAuthStatus, 3000);
|
|
||||||
|
|
||||||
// Открываем Telegram
|
|
||||||
console.log(`Открывается ссылка на Telegram: ${telegramBotLink.value}`);
|
|
||||||
window.open(telegramBotLink.value, '_blank');
|
|
||||||
} else {
|
|
||||||
errorMessage.value = response.error || 'Не удалось начать авторизацию';
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error starting Telegram auth:', error);
|
console.error('Error verifying Telegram code:', error);
|
||||||
errorMessage.value = 'Ошибка при инициализации авторизации';
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async function checkAuthStatus() {
|
const cancelVerification = () => {
|
||||||
try {
|
showVerification.value = false;
|
||||||
const response = await auth.checkTelegramAuthStatus(authToken.value);
|
verificationCode.value = '';
|
||||||
|
};
|
||||||
if (response.success && response.authenticated) {
|
|
||||||
// Авторизация успешна, очищаем интервал и состояние
|
|
||||||
clearInterval(checkInterval.value);
|
|
||||||
isAuthenticating.value = false;
|
|
||||||
|
|
||||||
// Здесь можно добавить дополнительные действия после успешной авторизации
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking auth status:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelAuth() {
|
|
||||||
clearInterval(checkInterval.value);
|
|
||||||
isAuthenticating.value = false;
|
|
||||||
authToken.value = '';
|
|
||||||
errorMessage.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyAuthCommand() {
|
|
||||||
const command = `/auth ${authToken.value}`;
|
|
||||||
navigator.clipboard.writeText(command);
|
|
||||||
// Можно добавить уведомление о копировании
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!authStore.isAuthenticated">
|
<div v-if="!authStore.isAuthenticated">
|
||||||
<button @click="handleConnectWallet" class="connect-button" :disabled="loading">
|
<button @click="connectWallet" class="connect-button" :disabled="loading">
|
||||||
<div v-if="loading" class="spinner"></div>
|
<div v-if="loading" class="spinner"></div>
|
||||||
{{ loading ? 'Подключение...' : 'Подключить кошелек' }}
|
{{ loading ? 'Подключение...' : 'Подключить кошелек' }}
|
||||||
</button>
|
</button>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { connectWallet } from '../utils/wallet';
|
import { connectWithWallet } from '../utils/wallet';
|
||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
@@ -29,6 +29,17 @@ const loading = ref(false);
|
|||||||
const error = ref('');
|
const error = ref('');
|
||||||
const isConnecting = ref(false);
|
const isConnecting = ref(false);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
onWalletAuth: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isAuthenticated: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Форматирование адреса кошелька
|
// Форматирование адреса кошелька
|
||||||
const formatAddress = (address) => {
|
const formatAddress = (address) => {
|
||||||
if (!address) return '';
|
if (!address) return '';
|
||||||
@@ -36,29 +47,17 @@ const formatAddress = (address) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Функция для подключения кошелька
|
// Функция для подключения кошелька
|
||||||
const handleConnectWallet = async () => {
|
const connectWallet = async () => {
|
||||||
console.log('Нажата кнопка "Подключить кошелек"');
|
loading.value = true;
|
||||||
isConnecting.value = true;
|
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await connectWallet();
|
await props.onWalletAuth();
|
||||||
console.log('Результат подключения:', result);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
authStore.isAuthenticated = true;
|
|
||||||
authStore.user = { address: result.address };
|
|
||||||
authStore.isAdmin = result.isAdmin;
|
|
||||||
authStore.authType = result.authType;
|
|
||||||
router.push({ name: 'home' });
|
|
||||||
} else {
|
|
||||||
error.value = result.error || 'Ошибка подключения кошелька';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка при подключении кошелька:', err);
|
console.error('Ошибка при подключении кошелька:', err);
|
||||||
error.value = 'Ошибка подключения кошелька';
|
error.value = 'Ошибка подключения кошелька';
|
||||||
} finally {
|
} finally {
|
||||||
isConnecting.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,7 +111,22 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Функция для отключения кошелька
|
// Функция для отключения кошелька
|
||||||
const disconnectWallet = async () => {
|
const disconnectWallet = async () => {
|
||||||
await authStore.logout();
|
try {
|
||||||
|
// Сначала отключаем MetaMask
|
||||||
|
if (window.ethereum) {
|
||||||
|
try {
|
||||||
|
// Просто очищаем слушатели событий
|
||||||
|
window.ethereum.removeAllListeners();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disconnecting MetaMask:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Затем выполняем выход из системы
|
||||||
|
await authStore.disconnect(router);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disconnecting wallet:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div v-if="authStore.isAuthenticated">
|
||||||
<div class="conversation-list">
|
<div class="conversation-list">
|
||||||
<div class="list-header">
|
<div class="list-header">
|
||||||
<h3>Диалоги</h3>
|
<h3>Диалоги</h3>
|
||||||
@@ -32,10 +33,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="connect-wallet-prompt">
|
||||||
|
<p>Подключите кошелек для просмотра бесед</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, defineEmits } from 'vue';
|
import { ref, onMounted, computed, defineEmits, watch } from 'vue';
|
||||||
import { useAuthStore } from '../../stores/auth';
|
import { useAuthStore } from '../../stores/auth';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -46,6 +51,14 @@ const conversations = ref([]);
|
|||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const selectedConversationId = ref(null);
|
const selectedConversationId = ref(null);
|
||||||
|
|
||||||
|
// Следим за изменением статуса аутентификации
|
||||||
|
watch(() => authStore.isAuthenticated, (isAuthenticated) => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
conversations.value = []; // Очищаем список бесед при отключении
|
||||||
|
selectedConversationId.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Загрузка списка диалогов
|
// Загрузка списка диалогов
|
||||||
const fetchConversations = async () => {
|
const fetchConversations = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -222,4 +235,10 @@ defineExpose({
|
|||||||
.time {
|
.time {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connect-wallet-prompt {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -44,15 +44,40 @@ const handleEnter = (event) => {
|
|||||||
|
|
||||||
// Отправка сообщения
|
// Отправка сообщения
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
if (!message.value.trim() || sending.value) return;
|
const messageText = message.value.trim();
|
||||||
|
if (!messageText) return;
|
||||||
|
|
||||||
|
const userMessage = {
|
||||||
|
id: Date.now(),
|
||||||
|
content: messageText,
|
||||||
|
role: auth.isAuthenticated ? 'user' : 'guest',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
messages.value.push(userMessage);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sending.value = true;
|
// Логируем параметры запроса
|
||||||
|
console.log('Sending message to Ollama:', {
|
||||||
|
message: messageText,
|
||||||
|
language: userLanguage.value
|
||||||
|
});
|
||||||
|
|
||||||
const response = await axios.post(
|
const response = await axios.post('/api/chat/message', {
|
||||||
`/api/messages/conversations/${props.conversationId}/messages`,
|
message: messageText,
|
||||||
{ content: message.value }
|
language: userLanguage.value
|
||||||
);
|
});
|
||||||
|
|
||||||
|
// Логируем ответ от Ollama
|
||||||
|
console.log('Response from Ollama:', response.data);
|
||||||
|
|
||||||
|
// Обработка ответа
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now() + 1,
|
||||||
|
content: response.data.message,
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
// Очищаем поле ввода
|
// Очищаем поле ввода
|
||||||
message.value = '';
|
message.value = '';
|
||||||
@@ -65,7 +90,7 @@ const sendMessage = async () => {
|
|||||||
// Уведомляем родительский компонент о новых сообщениях
|
// Уведомляем родительский компонент о новых сообщениях
|
||||||
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
|
emit('message-sent', [response.data.userMessage, response.data.aiMessage]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending message:', error);
|
console.error('Ошибка при отправке сообщения:', error);
|
||||||
} finally {
|
} finally {
|
||||||
sending.value = false;
|
sending.value = false;
|
||||||
}
|
}
|
||||||
@@ -81,6 +106,61 @@ defineExpose({
|
|||||||
resetInput,
|
resetInput,
|
||||||
focus: () => textareaRef.value?.focus(),
|
focus: () => textareaRef.value?.focus(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sendGuestMessage = async (messageText) => {
|
||||||
|
if (!messageText.trim()) return;
|
||||||
|
|
||||||
|
const userMessage = {
|
||||||
|
id: Date.now(),
|
||||||
|
content: messageText,
|
||||||
|
role: 'user',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
isGuest: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Добавляем сообщение пользователя в локальную историю
|
||||||
|
messages.value.push(userMessage);
|
||||||
|
|
||||||
|
// Сохраняем сообщение в массиве гостевых сообщений
|
||||||
|
guestMessages.value.push(userMessage);
|
||||||
|
|
||||||
|
// Сохраняем гостевые сообщения в localStorage
|
||||||
|
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
|
||||||
|
|
||||||
|
// Очищаем поле ввода
|
||||||
|
newMessage.value = '';
|
||||||
|
|
||||||
|
// Прокрутка вниз
|
||||||
|
await nextTick();
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем состояние загрузки
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Вместо отправки запроса к Ollama, отправляем сообщение с кнопками для аутентификации
|
||||||
|
const authMessage = {
|
||||||
|
id: Date.now() + 1,
|
||||||
|
content: 'Чтобы продолжить, пожалуйста, аутентифицируйтесь.',
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
isGuest: true,
|
||||||
|
showAuthOptions: true // Указываем, что нужно показать кнопки аутентификации
|
||||||
|
};
|
||||||
|
|
||||||
|
messages.value.push(authMessage);
|
||||||
|
guestMessages.value.push(authMessage);
|
||||||
|
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
|
||||||
|
|
||||||
|
// Прокрутка вниз
|
||||||
|
await nextTick();
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div v-if="authStore.isAuthenticated">
|
||||||
<div class="message-thread" ref="threadContainer">
|
<div class="message-thread" ref="threadContainer">
|
||||||
<div v-if="loading" class="loading">Загрузка сообщений...</div>
|
<div v-if="loading" class="loading">Загрузка сообщений...</div>
|
||||||
|
|
||||||
@@ -18,11 +19,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="connect-wallet-prompt">
|
||||||
|
<p>Пожалуйста, подключите кошелек для просмотра сообщений</p>
|
||||||
|
</div>
|
||||||
</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';
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
conversationId: {
|
conversationId: {
|
||||||
@@ -34,6 +40,7 @@ const props = defineProps({
|
|||||||
const messages = ref([]);
|
const messages = ref([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const threadContainer = ref(null);
|
const threadContainer = ref(null);
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
// Загрузка сообщений диалога
|
// Загрузка сообщений диалога
|
||||||
const fetchMessages = async () => {
|
const fetchMessages = async () => {
|
||||||
@@ -104,6 +111,13 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Следим за изменением статуса аутентификации
|
||||||
|
watch(() => authStore.isAuthenticated, (isAuthenticated) => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
messages.value = []; // Очищаем сообщения при отключении
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Загрузка сообщений при монтировании компонента
|
// Загрузка сообщений при монтировании компонента
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.conversationId) {
|
if (props.conversationId) {
|
||||||
@@ -191,4 +205,10 @@ defineExpose({
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connect-wallet-prompt {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,37 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import axios from '../api/axios';
|
import axios from '../api/axios';
|
||||||
|
|
||||||
|
const loadAuthState = () => {
|
||||||
|
const savedAuth = localStorage.getItem('auth');
|
||||||
|
if (savedAuth) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(savedAuth);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing saved auth state:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => {
|
||||||
|
const savedState = loadAuthState();
|
||||||
|
return {
|
||||||
user: null,
|
user: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: savedState?.isAuthenticated || false,
|
||||||
isAdmin: false,
|
isAdmin: savedState?.isAdmin || false,
|
||||||
authType: null,
|
authType: savedState?.authType || null,
|
||||||
identities: {},
|
identities: {},
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
address: null
|
address: null,
|
||||||
}),
|
wallet: null,
|
||||||
|
telegramId: savedState?.telegramId || null,
|
||||||
|
email: null,
|
||||||
|
userId: savedState?.userId || null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async connectWallet(address, signature, message) {
|
async connectWallet(address, signature, message) {
|
||||||
@@ -421,6 +440,99 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
console.error('Error checking Telegram auth status:', error);
|
console.error('Error checking Telegram auth status:', error);
|
||||||
return { success: false, error: 'Ошибка проверки статуса' };
|
return { success: false, error: 'Ошибка проверки статуса' };
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async disconnect(router) {
|
||||||
|
// Проверяем, действительно ли нужно выходить
|
||||||
|
if (!this.isAuthenticated) {
|
||||||
|
console.log('Already logged out, skipping disconnect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Сначала пробуем очистить сессию на сервере
|
||||||
|
await axios.post('/api/auth/clear-session');
|
||||||
|
await axios.post('/api/auth/logout');
|
||||||
|
|
||||||
|
// Очищаем состояние только после успешного выхода
|
||||||
|
this.clearState();
|
||||||
|
|
||||||
|
if (router) router.push('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during logout:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Выносим очистку состояния в отдельный метод
|
||||||
|
clearState() {
|
||||||
|
this.isAuthenticated = false;
|
||||||
|
this.wallet = null;
|
||||||
|
this.messages = [];
|
||||||
|
this.user = null;
|
||||||
|
this.address = null;
|
||||||
|
this.isAdmin = false;
|
||||||
|
this.authType = null;
|
||||||
|
this.identities = {};
|
||||||
|
this.telegramId = null;
|
||||||
|
this.userId = null;
|
||||||
|
|
||||||
|
// Очищаем локальное хранилище
|
||||||
|
localStorage.removeItem('wallet');
|
||||||
|
localStorage.removeItem('isAuthenticated');
|
||||||
|
localStorage.removeItem('walletAddress');
|
||||||
|
localStorage.removeItem('auth');
|
||||||
|
},
|
||||||
|
|
||||||
|
async setWalletAuth(authData) {
|
||||||
|
this.isAuthenticated = authData.authenticated;
|
||||||
|
this.address = authData.address;
|
||||||
|
this.isAdmin = authData.isAdmin;
|
||||||
|
},
|
||||||
|
|
||||||
|
async setTelegramAuth(authData) {
|
||||||
|
this.telegramId = authData.telegramId;
|
||||||
|
// Проверяем баланс токенов если есть подключенный кошелек
|
||||||
|
if (this.address) {
|
||||||
|
await this.checkTokenBalance();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkTokenBalance() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/auth/check-tokens?address=${this.address}`);
|
||||||
|
this.isAdmin = response.data.isAdmin;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking token balance:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setAuth(authData) {
|
||||||
|
console.log('Setting auth state:', authData);
|
||||||
|
|
||||||
|
// Обновляем все поля состояния
|
||||||
|
this.isAuthenticated = authData.authenticated || authData.isAuthenticated;
|
||||||
|
this.userId = authData.userId;
|
||||||
|
this.isAdmin = authData.isAdmin;
|
||||||
|
this.authType = authData.authType;
|
||||||
|
this.address = authData.address;
|
||||||
|
|
||||||
|
// Сохраняем состояние в localStorage
|
||||||
|
const stateToSave = {
|
||||||
|
isAuthenticated: this.isAuthenticated,
|
||||||
|
userId: this.userId,
|
||||||
|
isAdmin: this.isAdmin,
|
||||||
|
authType: this.authType,
|
||||||
|
address: this.address
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem('auth', JSON.stringify(stateToSave));
|
||||||
|
|
||||||
|
console.log('Auth state updated:', {
|
||||||
|
isAuthenticated: this.isAuthenticated,
|
||||||
|
userId: this.userId,
|
||||||
|
authType: this.authType,
|
||||||
|
address: this.address
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
|
import { ethers } from 'ethers';
|
||||||
import axios from '../api/axios';
|
import axios from '../api/axios';
|
||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
|
|
||||||
// Функция для подключения кошелька
|
// Переименовываем функцию для соответствия импорту
|
||||||
async function connectWallet() {
|
export async function connectWithWallet() {
|
||||||
try {
|
try {
|
||||||
// Проверяем, доступен ли MetaMask
|
// Проверяем, доступен ли MetaMask
|
||||||
if (!window.ethereum) {
|
if (!window.ethereum) {
|
||||||
throw new Error('MetaMask не установлен');
|
throw new Error('MetaMask не установлен');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запрашиваем доступ к аккаунтам
|
// Запрашиваем доступ к кошельку
|
||||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
const provider = new ethers.BrowserProvider(window.ethereum);
|
||||||
const address = accounts[0];
|
const signer = await provider.getSigner();
|
||||||
|
const address = await signer.getAddress();
|
||||||
|
|
||||||
// Получаем nonce от сервера
|
// Получаем nonce для подписи
|
||||||
const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`, {
|
const nonceResponse = await axios.get(`/api/auth/nonce?address=${address}`);
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
const nonce = nonceResponse.data.nonce;
|
const nonce = nonceResponse.data.nonce;
|
||||||
|
|
||||||
// Создаем сообщение для подписи
|
// Формируем сообщение для подписи
|
||||||
const message = `Подпишите это сообщение для аутентификации в DApp for Business. Nonce: ${nonce}`;
|
const message = `Подпишите это сообщение для аутентификации в DApp for Business. Nonce: ${nonce}`;
|
||||||
|
|
||||||
// Запрашиваем подпись
|
// Подписываем сообщение
|
||||||
const signature = await window.ethereum.request({
|
const signature = await signer.signMessage(message);
|
||||||
method: 'personal_sign',
|
|
||||||
params: [message, address]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отправляем подпись на сервер для верификации
|
// Верифицируем подпись на сервере
|
||||||
const response = await axios.post('/api/auth/verify', {
|
const response = await axios.post('/api/auth/verify', {
|
||||||
address,
|
address,
|
||||||
signature,
|
signature,
|
||||||
message
|
message: nonce
|
||||||
}, {
|
|
||||||
withCredentials: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Успешно подключен:', response.data);
|
console.log('Wallet verification response:', response.data);
|
||||||
|
|
||||||
// Обновляем состояние в хранилище auth
|
// Обновляем состояние в хранилище auth
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -47,7 +42,7 @@ async function connectWallet() {
|
|||||||
address: response.data.address
|
address: response.data.address
|
||||||
};
|
};
|
||||||
authStore.isAdmin = response.data.isAdmin;
|
authStore.isAdmin = response.data.isAdmin;
|
||||||
authStore.authType = response.data.authType;
|
authStore.authType = 'wallet';
|
||||||
|
|
||||||
// Сохраняем адрес кошелька в локальном хранилище
|
// Сохраняем адрес кошелька в локальном хранилище
|
||||||
localStorage.setItem('walletAddress', address);
|
localStorage.setItem('walletAddress', address);
|
||||||
@@ -55,13 +50,14 @@ async function connectWallet() {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
authenticated: response.data.authenticated,
|
authenticated: response.data.authenticated,
|
||||||
|
userId: response.data.userId,
|
||||||
address: response.data.address,
|
address: response.data.address,
|
||||||
isAdmin: response.data.isAdmin,
|
isAdmin: response.data.isAdmin,
|
||||||
authType: response.data.authType
|
authType: 'wallet'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при подключении кошелька:', error);
|
console.error('Error connecting wallet:', error);
|
||||||
return { success: false, error: error.message || 'Ошибка подключения кошелька' };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,5 +88,3 @@ async function disconnectWallet() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { connectWallet, disconnectWallet };
|
|
||||||
|
|||||||
@@ -7,60 +7,81 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
<WalletConnection />
|
<WalletConnection
|
||||||
|
:onWalletAuth="handleWalletAuth"
|
||||||
|
:isAuthenticated="auth.isAuthenticated"
|
||||||
|
/>
|
||||||
<div class="user-info" v-if="auth.isAuthenticated">
|
<div class="user-info" v-if="auth.isAuthenticated">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка загрузки предыдущих сообщений -->
|
||||||
|
<div v-if="hasMoreMessages" class="load-more-container">
|
||||||
|
<button @click="loadMoreMessages" class="load-more-btn" :disabled="isLoadingMore">
|
||||||
|
{{ isLoadingMore ? 'Загрузка...' : 'Показать предыдущие сообщения' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="chat-messages" ref="messagesContainer">
|
<div class="chat-messages" ref="messagesContainer">
|
||||||
<div v-for="message in messages" :key="message.id" :class="['message', message.role === 'assistant' ? 'ai-message' : 'user-message']">
|
<div v-for="message in messages" :key="message.id" :class="['message', message.role === 'assistant' ? 'ai-message' : 'user-message']">
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Опции аутентификации -->
|
<!-- Кнопки аутентификации -->
|
||||||
<div v-if="!auth.isAuthenticated && message.role === 'assistant' && !hasShownAuthOptions.value" class="auth-options">
|
<div v-if="message.showAuthButtons && !auth.isAuthenticated" class="auth-buttons">
|
||||||
<div class="auth-option">
|
<button class="auth-btn wallet-btn" @click="handleWalletAuth">
|
||||||
<WalletConnection />
|
<span class="auth-icon">👛</span> Подключить кошелек
|
||||||
</div>
|
</button>
|
||||||
|
<button class="auth-btn telegram-btn" @click="handleTelegramAuth">
|
||||||
<div class="auth-option">
|
<span class="auth-icon">📱</span> Подключить Telegram
|
||||||
<TelegramConnect />
|
</button>
|
||||||
</div>
|
<button class="auth-btn email-btn" @click="handleEmailAuth">
|
||||||
|
|
||||||
<!-- Email аутентификация: первый шаг - запрос кода -->
|
|
||||||
<div v-if="!showEmailVerification" class="auth-option email-option">
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
v-model="email"
|
|
||||||
placeholder="Введите ваш email"
|
|
||||||
class="email-input"
|
|
||||||
/>
|
|
||||||
<button class="auth-btn email-btn" @click="requestEmailCode" :disabled="!isValidEmail">
|
|
||||||
<span class="auth-icon">✉️</span> Подключить Email
|
<span class="auth-icon">✉️</span> Подключить Email
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email аутентификация: второй шаг - ввод кода -->
|
<!-- Email форма -->
|
||||||
<div v-else class="auth-option email-verification">
|
<div v-if="showEmailForm" class="auth-form">
|
||||||
<p>Код подтверждения отправлен на {{ email }}</p>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
v-model="emailInput"
|
||||||
v-model="emailVerificationCode"
|
type="email"
|
||||||
placeholder="Введите код подтверждения"
|
placeholder="Введите ваш email"
|
||||||
class="verification-input"
|
class="auth-input"
|
||||||
/>
|
/>
|
||||||
<div class="email-verification-actions">
|
<button @click="submitEmail" class="auth-btn">
|
||||||
<button class="auth-btn email-btn" @click="verifyEmailCode">
|
Отправить код
|
||||||
<span class="auth-icon">✓</span> Подтвердить
|
|
||||||
</button>
|
</button>
|
||||||
<button class="auth-btn cancel-btn" @click="cancelEmailVerification">
|
|
||||||
Отмена
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="emailErrorMessage" class="error-message">{{ emailErrorMessage }}</div>
|
<!-- Форма верификации email -->
|
||||||
|
<div v-if="showEmailVerification" class="auth-form">
|
||||||
|
<input
|
||||||
|
v-model="emailCode"
|
||||||
|
type="text"
|
||||||
|
placeholder="Введите код из email"
|
||||||
|
class="auth-input"
|
||||||
|
/>
|
||||||
|
<button @click="verifyEmailCode" class="auth-btn">
|
||||||
|
Подтвердить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telegram верификация -->
|
||||||
|
<div v-if="showTelegramVerification" class="auth-form">
|
||||||
|
<input
|
||||||
|
v-model="telegramCode"
|
||||||
|
type="text"
|
||||||
|
placeholder="Введите код из Telegram"
|
||||||
|
class="auth-input"
|
||||||
|
/>
|
||||||
|
<button @click="verifyTelegramCode" class="auth-btn">
|
||||||
|
Подтвердить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="emailError" class="error-message">
|
||||||
|
{{ emailError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message-time">
|
<div class="message-time">
|
||||||
@@ -72,11 +93,11 @@
|
|||||||
<div class="chat-input">
|
<div class="chat-input">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="newMessage"
|
v-model="newMessage"
|
||||||
@keydown.enter.prevent="sendMessage"
|
@keydown.enter.prevent="handleMessage(newMessage)"
|
||||||
placeholder="Введите сообщение..."
|
placeholder="Введите сообщение..."
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button @click="sendMessage" :disabled="isLoading || !newMessage.trim()">
|
<button @click="handleMessage(newMessage)" :disabled="isLoading || !newMessage.trim()">
|
||||||
{{ isLoading ? 'Отправка...' : 'Отправить' }}
|
{{ isLoading ? 'Отправка...' : 'Отправить' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,6 +111,7 @@ import { useAuthStore } from '../stores/auth';
|
|||||||
import WalletConnection from '../components/WalletConnection.vue';
|
import WalletConnection from '../components/WalletConnection.vue';
|
||||||
import TelegramConnect from '../components/TelegramConnect.vue';
|
import TelegramConnect from '../components/TelegramConnect.vue';
|
||||||
import axios from '../api/axios';
|
import axios from '../api/axios';
|
||||||
|
import { connectWithWallet } from '../utils/wallet';
|
||||||
|
|
||||||
console.log('HomeView.vue: Version with chat loaded');
|
console.log('HomeView.vue: Version with chat loaded');
|
||||||
|
|
||||||
@@ -110,168 +132,156 @@ const emailVerificationCode = ref('');
|
|||||||
const showEmailVerification = ref(false);
|
const showEmailVerification = ref(false);
|
||||||
const emailErrorMessage = ref('');
|
const emailErrorMessage = ref('');
|
||||||
|
|
||||||
// Простая функция для выхода
|
// Добавляем состояния для форм верификации
|
||||||
const logout = async () => {
|
const showTelegramVerification = ref(false);
|
||||||
await auth.logout();
|
const showEmailForm = ref(false);
|
||||||
messages.value = [];
|
const telegramCode = ref('');
|
||||||
};
|
const emailInput = ref('');
|
||||||
|
const emailCode = ref('');
|
||||||
|
const emailError = ref('');
|
||||||
|
|
||||||
// Форматирование времени
|
// Добавляем состояния для пагинации
|
||||||
const formatTime = (timestamp) => {
|
const PAGE_SIZE = 2; // Показываем только последнее сообщение и ответ
|
||||||
if (!timestamp) return '';
|
const allMessages = ref([]); // Все загруженные сообщения
|
||||||
|
const currentPage = ref(1); // Текущая страница
|
||||||
|
const hasMoreMessages = ref(false); // Есть ли еще сообщения
|
||||||
|
const isLoadingMore = ref(false); // Загружаются ли дополнительные сообщения
|
||||||
|
|
||||||
|
// Вычисляемое свойство для отображаемых сообщений
|
||||||
|
const displayedMessages = computed(() => {
|
||||||
|
const startIndex = Math.max(allMessages.value.length - (PAGE_SIZE * currentPage.value), 0);
|
||||||
|
return allMessages.value.slice(startIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция загрузки истории чата
|
||||||
|
const loadChatHistory = async () => {
|
||||||
try {
|
try {
|
||||||
const date = new Date(timestamp);
|
if (!auth.isAuthenticated || !auth.userId) {
|
||||||
|
|
||||||
// Проверяем, является ли дата валидной
|
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
console.warn('Invalid timestamp:', timestamp);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Форматируем дату с указанием дня, месяца, года и времени
|
|
||||||
return date.toLocaleString([], {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error formatting time:', error, timestamp);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для отправки сообщения
|
|
||||||
const sendMessage = async () => {
|
|
||||||
if (!newMessage.value.trim() || isLoading.value) return;
|
|
||||||
|
|
||||||
console.log('Отправка сообщения:', newMessage.value, 'язык:', userLanguage.value);
|
|
||||||
|
|
||||||
// Если пользователь не аутентифицирован, используем sendGuestMessage
|
|
||||||
if (!auth.isAuthenticated) {
|
|
||||||
await sendGuestMessage();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Код для аутентифицированных пользователей
|
const response = await axios.get('/api/chat/history', {
|
||||||
const userMessage = {
|
headers: { Authorization: `Bearer ${auth.address}` },
|
||||||
id: Date.now(),
|
params: { limit: PAGE_SIZE, offset: 0 }
|
||||||
content: newMessage.value,
|
|
||||||
role: 'user',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
messages.value.push(userMessage);
|
|
||||||
const messageText = newMessage.value;
|
|
||||||
newMessage.value = '';
|
|
||||||
|
|
||||||
// Прокрутка вниз
|
|
||||||
await nextTick();
|
|
||||||
if (messagesContainer.value) {
|
|
||||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/api/chat/message', {
|
|
||||||
message: messageText,
|
|
||||||
language: userLanguage.value
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Ответ от сервера:', response.data);
|
if (response.data.success) {
|
||||||
|
messages.value = response.data.messages.map(msg => ({
|
||||||
|
id: msg.id,
|
||||||
|
content: msg.content,
|
||||||
|
role: msg.role || (msg.sender_type === 'assistant' ? 'assistant' : 'user'),
|
||||||
|
timestamp: msg.created_at,
|
||||||
|
showAuthOptions: false
|
||||||
|
}));
|
||||||
|
|
||||||
// Добавляем ответ от ИИ
|
hasMoreMessages.value = response.data.total > PAGE_SIZE;
|
||||||
messages.value.push({
|
|
||||||
id: Date.now() + 1,
|
|
||||||
content: response.data.message,
|
|
||||||
role: 'assistant',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Прокрутка вниз
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (messagesContainer.value) {
|
scrollToBottom();
|
||||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при отправке сообщения:', error);
|
console.error('Error loading chat history:', error);
|
||||||
messages.value.push({
|
|
||||||
id: Date.now() + 1,
|
|
||||||
content: 'Произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте еще раз.',
|
|
||||||
role: 'assistant',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Добавим наблюдатель за изменением состояния аутентификации
|
// Функция загрузки дополнительных сообщений
|
||||||
watch(() => auth.isAuthenticated, async (newValue, oldValue) => {
|
const loadMoreMessages = async () => {
|
||||||
console.log('Auth state changed in HomeView:', newValue);
|
if (isLoadingMore.value) return;
|
||||||
|
|
||||||
if (newValue && !oldValue) {
|
try {
|
||||||
// Пользователь только что аутентифицировался
|
isLoadingMore.value = true;
|
||||||
|
const offset = messages.value.length;
|
||||||
|
|
||||||
|
const response = await axios.get('/api/chat/history', {
|
||||||
|
headers: { Authorization: `Bearer ${auth.address}` },
|
||||||
|
params: { limit: PAGE_SIZE, offset }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const newMessages = response.data.messages.map(msg => ({
|
||||||
|
id: msg.id,
|
||||||
|
content: msg.content,
|
||||||
|
role: msg.role || (msg.sender_type === 'assistant' ? 'assistant' : 'user'),
|
||||||
|
timestamp: msg.created_at,
|
||||||
|
showAuthOptions: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
messages.value = [...newMessages, ...messages.value];
|
||||||
|
hasMoreMessages.value = response.data.total > messages.value.length;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading more messages:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingMore.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция прокрутки к последнему сообщению
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (messagesContainer.value) {
|
||||||
|
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Инициализация при монтировании
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log('HomeView.vue: onMounted called');
|
||||||
|
console.log('Auth state:', auth.isAuthenticated);
|
||||||
|
|
||||||
|
// Определяем язык
|
||||||
|
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
||||||
|
userLanguage.value = cyrillicPattern.test(newMessage.value) ? 'ru' : 'en';
|
||||||
|
console.log('Detected language:', userLanguage.value);
|
||||||
|
|
||||||
|
// Если пользователь уже аутентифицирован, загружаем историю
|
||||||
|
if (auth.isAuthenticated && auth.userId) {
|
||||||
|
console.log('User authenticated, loading chat history...');
|
||||||
await loadChatHistory();
|
await loadChatHistory();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Загрузка истории сообщений
|
// Наблюдатель за изменением состояния аутентификации
|
||||||
const loadChatHistory = async () => {
|
watch(() => auth.isAuthenticated, async (newValue, oldValue) => {
|
||||||
console.log('Loading chat history...');
|
console.log('Auth state changed in HomeView:', newValue);
|
||||||
|
|
||||||
|
if (newValue && auth.userId) {
|
||||||
|
// Пользователь только что аутентифицировался
|
||||||
|
await loadChatHistory();
|
||||||
|
} else {
|
||||||
|
// Пользователь вышел из системы
|
||||||
|
messages.value = []; // Очищаем историю сообщений
|
||||||
|
hasMoreMessages.value = false; // Сбрасываем флаг наличия дополнительных сообщений
|
||||||
|
console.log('Chat history cleared after logout');
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Функция для подключения кошелька
|
||||||
|
const handleWalletAuth = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('User address from auth store:', auth.address);
|
const result = await connectWithWallet();
|
||||||
|
if (result.success) {
|
||||||
|
console.log('Wallet auth result:', result);
|
||||||
|
|
||||||
// Добавляем заголовок авторизации
|
// Обновляем состояние аутентификации
|
||||||
const headers = {};
|
auth.setAuth({
|
||||||
if (auth.address) {
|
authenticated: true,
|
||||||
const authHeader = `Bearer ${auth.address}`;
|
isAuthenticated: true,
|
||||||
console.log('Adding Authorization header:', authHeader);
|
userId: result.userId,
|
||||||
headers.Authorization = authHeader;
|
address: result.address,
|
||||||
}
|
isAdmin: result.isAdmin,
|
||||||
|
authType: 'wallet'
|
||||||
const response = await axios.get('/api/chat/history', { headers });
|
});
|
||||||
console.log('Chat history response:', response.data);
|
|
||||||
|
// Добавляем задержку для синхронизации сессии
|
||||||
if (response.data.messages) {
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
// Получаем историю с сервера
|
|
||||||
const serverMessages = response.data.messages.map(msg => ({
|
// Загружаем историю чата
|
||||||
id: msg.id,
|
await loadChatHistory();
|
||||||
content: msg.content,
|
|
||||||
role: msg.role,
|
|
||||||
timestamp: msg.timestamp || msg.created_at,
|
|
||||||
isGuest: false
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Объединяем гостевые сообщения с историей с сервера
|
|
||||||
// Сначала отправляем гостевые сообщения на сервер
|
|
||||||
await saveGuestMessagesToServer();
|
|
||||||
|
|
||||||
// Затем загружаем обновленную историю
|
|
||||||
const updatedResponse = await axios.get('/api/chat/history', { headers });
|
|
||||||
const updatedServerMessages = updatedResponse.data.messages.map(msg => ({
|
|
||||||
id: msg.id,
|
|
||||||
content: msg.content,
|
|
||||||
role: msg.role,
|
|
||||||
timestamp: msg.timestamp || msg.created_at,
|
|
||||||
isGuest: false
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Обновляем сообщения
|
|
||||||
messages.value = updatedServerMessages;
|
|
||||||
|
|
||||||
// Очищаем гостевые сообщения
|
|
||||||
guestMessages.value = [];
|
|
||||||
localStorage.removeItem('guestMessages');
|
|
||||||
|
|
||||||
console.log('Updated messages:', messages.value);
|
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading chat history:', error);
|
console.error('Error connecting wallet:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -363,36 +373,29 @@ async function requestEmailCode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Подтверждение кода подтверждения по email
|
// Функция проверки кода
|
||||||
async function verifyEmailCode() {
|
const verifyEmailCode = async () => {
|
||||||
emailErrorMessage.value = '';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await auth.verifyEmail(emailVerificationCode.value);
|
const response = await axios.post('/api/auth/email/verify-code', {
|
||||||
|
email: emailInput.value,
|
||||||
|
code: emailCode.value
|
||||||
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.data.success) {
|
||||||
// Успешная верификация
|
auth.setEmailAuth(response.data);
|
||||||
showEmailVerification.value = false;
|
showEmailVerification.value = false;
|
||||||
emailVerificationCode.value = '';
|
emailError.value = '';
|
||||||
|
|
||||||
// Связываем гостевые сообщения с аутентифицированным пользователем
|
// Загружаем историю чата после успешной аутентификации
|
||||||
try {
|
|
||||||
await axios.post('/api/chat/link-guest-messages');
|
|
||||||
console.log('Guest messages linked to authenticated user');
|
|
||||||
} catch (linkError) {
|
|
||||||
console.error('Error linking guest messages:', linkError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Загружаем историю сообщений
|
|
||||||
await loadChatHistory();
|
await loadChatHistory();
|
||||||
} else {
|
} else {
|
||||||
emailErrorMessage.value = response.error || 'Неверный код подтверждения';
|
emailError.value = response.data.error || 'Неверный код';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
emailError.value = error.response?.data?.error || 'Ошибка проверки кода';
|
||||||
console.error('Error verifying email code:', error);
|
console.error('Error verifying email code:', error);
|
||||||
emailErrorMessage.value = 'Ошибка верификации';
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Отмена верификации email
|
// Отмена верификации email
|
||||||
function cancelEmailVerification() {
|
function cancelEmailVerification() {
|
||||||
@@ -407,117 +410,218 @@ const formatAddress = (address) => {
|
|||||||
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
return address.substring(0, 6) + '...' + address.substring(address.length - 4);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
// Форматирование времени
|
||||||
console.log('HomeView.vue: onMounted called');
|
const formatTime = (timestamp) => {
|
||||||
console.log('Auth state:', auth.isAuthenticated);
|
if (!timestamp) return '';
|
||||||
|
|
||||||
// Определяем язык пользователя
|
try {
|
||||||
const browserLanguage = navigator.language || navigator.userLanguage;
|
const date = new Date(timestamp);
|
||||||
userLanguage.value = browserLanguage.split('-')[0];
|
|
||||||
console.log('Detected language:', userLanguage.value);
|
|
||||||
|
|
||||||
// Загружаем гостевые сообщения из localStorage
|
// Проверяем, является ли дата валидной
|
||||||
const savedGuestMessages = localStorage.getItem('guestMessages');
|
if (isNaN(date.getTime())) {
|
||||||
if (savedGuestMessages) {
|
console.warn('Invalid timestamp:', timestamp);
|
||||||
guestMessages.value = JSON.parse(savedGuestMessages);
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если пользователь аутентифицирован, загружаем историю чата с сервера
|
// Форматируем дату с указанием дня, месяца, года и времени
|
||||||
if (auth.isAuthenticated) {
|
return date.toLocaleString([], {
|
||||||
console.log('User authenticated, loading chat history...');
|
year: 'numeric',
|
||||||
await loadChatHistory();
|
month: 'short',
|
||||||
} else {
|
day: 'numeric',
|
||||||
// Если пользователь не аутентифицирован, отображаем гостевые сообщения
|
hour: '2-digit',
|
||||||
messages.value = [...guestMessages.value];
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error formatting time:', error, timestamp);
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// Функция для отправки сообщения от неаутентифицированного пользователя
|
// Функция для отправки сообщения
|
||||||
const sendGuestMessage = async () => {
|
const handleMessage = async (messageText) => {
|
||||||
if (!newMessage.value.trim()) return;
|
if (!messageText.trim() || isLoading.value) return;
|
||||||
|
|
||||||
const userMessage = {
|
console.log('Handling message:', messageText);
|
||||||
id: Date.now(),
|
|
||||||
content: newMessage.value,
|
|
||||||
role: 'user',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
isGuest: true
|
|
||||||
};
|
|
||||||
|
|
||||||
// Добавляем сообщение пользователя в локальную историю
|
|
||||||
messages.value.push(userMessage);
|
|
||||||
|
|
||||||
// Сохраняем сообщение в массиве гостевых сообщений
|
|
||||||
guestMessages.value.push(userMessage);
|
|
||||||
|
|
||||||
// Сохраняем гостевые сообщения в localStorage
|
|
||||||
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
|
|
||||||
|
|
||||||
// Очищаем поле ввода
|
|
||||||
const messageText = newMessage.value;
|
|
||||||
newMessage.value = '';
|
|
||||||
|
|
||||||
// Прокрутка вниз
|
|
||||||
await nextTick();
|
|
||||||
if (messagesContainer.value) {
|
|
||||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Устанавливаем состояние загрузки
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// Отправляем запрос на сервер
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/api/chat/guest-message', {
|
if (!auth.isAuthenticated) {
|
||||||
|
await sendGuestMessage(messageText);
|
||||||
|
} else {
|
||||||
|
await sendMessage(messageText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling message:', error);
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
content: 'Произошла ошибка при отправке сообщения.',
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
newMessage.value = '';
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для отправки сообщения аутентифицированного пользователя
|
||||||
|
const sendMessage = async (messageText) => {
|
||||||
|
try {
|
||||||
|
const userMessage = {
|
||||||
|
id: Date.now(),
|
||||||
|
content: messageText,
|
||||||
|
role: 'user',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
messages.value.push(userMessage);
|
||||||
|
|
||||||
|
const response = await axios.post('/api/chat/message', {
|
||||||
message: messageText,
|
message: messageText,
|
||||||
language: userLanguage.value
|
language: userLanguage.value
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Response from server:', response.data);
|
if (response.data.success) {
|
||||||
|
|
||||||
// Добавляем ответ AI в историю
|
|
||||||
const aiMessage = {
|
|
||||||
id: Date.now() + 1,
|
|
||||||
content: response.data.message || response.data.reply,
|
|
||||||
role: 'assistant',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
isGuest: true,
|
|
||||||
showAuthOptions: !hasShownAuthOptions.value
|
|
||||||
};
|
|
||||||
|
|
||||||
messages.value.push(aiMessage);
|
|
||||||
|
|
||||||
// Отмечаем, что опции аутентификации уже были показаны
|
|
||||||
if (!hasShownAuthOptions.value) {
|
|
||||||
hasShownAuthOptions.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем ответ AI в массиве гостевых сообщений
|
|
||||||
guestMessages.value.push(aiMessage);
|
|
||||||
|
|
||||||
// Обновляем localStorage
|
|
||||||
localStorage.setItem('guestMessages', JSON.stringify(guestMessages.value));
|
|
||||||
|
|
||||||
// Прокрутка вниз
|
|
||||||
await nextTick();
|
|
||||||
if (messagesContainer.value) {
|
|
||||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending guest message:', error);
|
|
||||||
|
|
||||||
// Добавляем сообщение об ошибке
|
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
id: Date.now() + 1,
|
id: Date.now() + 1,
|
||||||
content: 'Произошла ошибка при обработке вашего сообщения. Пожалуйста, попробуйте еще раз.',
|
content: response.data.message,
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для отправки гостевого сообщения
|
||||||
|
const sendGuestMessage = async (messageText) => {
|
||||||
|
try {
|
||||||
|
// Добавляем сообщение пользователя
|
||||||
|
const userMessage = {
|
||||||
|
id: Date.now(),
|
||||||
|
content: messageText,
|
||||||
|
role: 'user',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
showAuthButtons: false
|
||||||
|
};
|
||||||
|
messages.value.push(userMessage);
|
||||||
|
|
||||||
|
// Очищаем поле ввода
|
||||||
|
newMessage.value = '';
|
||||||
|
|
||||||
|
// Сохраняем сообщение на сервере без получения ответа от Ollama
|
||||||
|
await axios.post('/api/chat/guest-message', {
|
||||||
|
message: messageText,
|
||||||
|
language: userLanguage.value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем сообщение с кнопками аутентификации
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now() + 1,
|
||||||
|
content: 'Для получения ответа, пожалуйста, авторизуйтесь одним из способов:',
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
isGuest: true
|
showAuthButtons: true
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending guest message:', error);
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now() + 2,
|
||||||
|
content: 'Произошла ошибка. Пожалуйста, попробуйте позже.',
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
showAuthButtons: true
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Добавляем методы для аутентификации
|
||||||
|
const handleTelegramAuth = () => {
|
||||||
|
window.open('https://t.me/HB3_Accelerator_Bot', '_blank');
|
||||||
|
// Показываем форму для ввода кода через небольшую задержку
|
||||||
|
setTimeout(() => {
|
||||||
|
showTelegramVerification.value = true;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmailAuth = async () => {
|
||||||
|
showEmailForm.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция отправки email
|
||||||
|
const submitEmail = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/auth/email/request', {
|
||||||
|
email: emailInput.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
showEmailForm.value = false;
|
||||||
|
showEmailVerification.value = true;
|
||||||
|
} else {
|
||||||
|
emailError.value = response.data.error || 'Ошибка отправки кода';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
emailError.value = 'Ошибка отправки кода';
|
||||||
|
console.error('Error sending email code:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция верификации кода Telegram
|
||||||
|
const verifyTelegramCode = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/auth/telegram/verify', {
|
||||||
|
code: telegramCode.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
console.log('Telegram verification successful:', response.data);
|
||||||
|
|
||||||
|
// Обновляем состояние аутентификации
|
||||||
|
auth.setAuth({
|
||||||
|
isAuthenticated: response.data.authenticated,
|
||||||
|
userId: response.data.userId,
|
||||||
|
telegramId: response.data.telegramId,
|
||||||
|
isAdmin: response.data.isAdmin,
|
||||||
|
authType: 'telegram'
|
||||||
|
});
|
||||||
|
|
||||||
|
showTelegramVerification.value = false;
|
||||||
|
telegramCode.value = '';
|
||||||
|
|
||||||
|
// Показываем сообщение об успехе
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
content: 'Telegram успешно подключен!',
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Загружаем историю чата после небольшой задержки
|
||||||
|
setTimeout(async () => {
|
||||||
|
await loadChatHistory();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
content: response.data.error || 'Ошибка верификации кода',
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error verifying Telegram code:', error);
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
content: 'Произошла ошибка. Пожалуйста, попробуйте позже.',
|
||||||
|
role: 'assistant',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -813,7 +917,7 @@ h1 {
|
|||||||
.auth-btn {
|
.auth-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -858,4 +962,122 @@ h1 {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-btn {
|
||||||
|
background-color: #4a5568;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegram-btn {
|
||||||
|
background-color: #0088cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-btn {
|
||||||
|
background-color: #48bb78;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-form {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-form input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-form button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-form button:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #4a5568;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn:hover:not(:disabled) {
|
||||||
|
background-color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-btn:disabled {
|
||||||
|
background-color: #cbd5e0;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -41,9 +41,7 @@ export default defineConfig({
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8000',
|
target: 'http://localhost:8000',
|
||||||
changeOrigin: true,
|
changeOrigin: true
|
||||||
secure: false,
|
|
||||||
ws: true,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user