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

This commit is contained in:
2025-04-03 12:46:39 +03:00
parent 084c72462c
commit dbd4fd5cc2
8 changed files with 1314 additions and 643 deletions

View File

@@ -5,6 +5,8 @@ CREATE OR REPLACE FUNCTION link_guest_messages(
DECLARE DECLARE
v_conversation_id INTEGER; v_conversation_id INTEGER;
v_count INTEGER; v_count INTEGER;
v_first_message TEXT;
v_title TEXT;
BEGIN BEGIN
-- Логируем входные параметры -- Логируем входные параметры
RAISE NOTICE 'Linking messages for user_id: %, guest_id: %', p_user_id, p_guest_id; RAISE NOTICE 'Linking messages for user_id: %, guest_id: %', p_user_id, p_guest_id;
@@ -16,12 +18,31 @@ BEGIN
RAISE NOTICE 'Found % guest messages', v_count; RAISE NOTICE 'Found % guest messages', v_count;
-- Если нет гостевых сообщений, выходим
IF v_count = 0 THEN
RAISE NOTICE 'No guest messages found, exiting';
RETURN;
END IF;
-- Получаем первое сообщение для названия беседы
SELECT content INTO v_first_message
FROM guest_messages
WHERE guest_id = p_guest_id AND NOT is_ai
ORDER BY created_at ASC
LIMIT 1;
-- Формируем название диалога на основе первого сообщения
v_title := CASE
WHEN length(v_first_message) > 30 THEN substring(v_first_message from 1 for 30) || '...'
ELSE v_first_message
END;
-- Создаем новую беседу -- Создаем новую беседу
INSERT INTO conversations (user_id, created_at, updated_at) INSERT INTO conversations (user_id, title, created_at, updated_at)
VALUES (p_user_id, NOW(), NOW()) VALUES (p_user_id, v_title, NOW(), NOW())
RETURNING id INTO v_conversation_id; RETURNING id INTO v_conversation_id;
RAISE NOTICE 'Created conversation with id: %', v_conversation_id; RAISE NOTICE 'Created conversation with id: % and title: %', v_conversation_id, v_title;
-- Копируем сообщения пользователя -- Копируем сообщения пользователя
WITH inserted_messages AS ( WITH inserted_messages AS (
@@ -46,11 +67,19 @@ BEGIN
created_at created_at
FROM guest_messages FROM guest_messages
WHERE guest_id = p_guest_id WHERE guest_id = p_guest_id
-- Проверка, чтобы избежать дублирования сообщений
AND NOT EXISTS (
SELECT 1 FROM messages m
WHERE m.guest_message_id = guest_messages.id
)
ORDER BY created_at ORDER BY created_at
RETURNING id RETURNING id
) )
SELECT COUNT(*) INTO v_count FROM inserted_messages; SELECT COUNT(*) INTO v_count FROM inserted_messages;
RAISE NOTICE 'Inserted % messages', v_count; RAISE NOTICE 'Inserted % messages into conversation', v_count;
-- НЕ удаляем гостевые сообщения, позволяем им существовать на всякий случай
-- до автоматической очистки по cron job
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ 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'); const { saveGuestMessageToDatabase } = require('../db');
const { v4: uuidv4 } = require('uuid');
// Функция для обработки гостевых сообщений после аутентификации // Функция для обработки гостевых сообщений после аутентификации
async function processGuestMessages(userId, guestId) { async function processGuestMessages(userId, guestId) {
@@ -26,6 +27,30 @@ async function processGuestMessages(userId, guestId) {
const guestMessages = guestMessagesResult.rows; const guestMessages = guestMessagesResult.rows;
console.log(`Found ${guestMessages.length} guest messages`); console.log(`Found ${guestMessages.length} guest messages`);
// Получаем идентификаторы пользователя
const userIdentities = await db.query(
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
[userId]
);
console.log(`Found ${userIdentities.rows.length} identities for user ${userId}`, userIdentities.rows);
// Сохраняем guestId как отдельный идентификатор для пользователя
// если он еще не привязан к пользователю
const existingGuestIdentity = userIdentities.rows.find(
identity => identity.provider === 'guest' && identity.provider_id === guestId
);
if (!existingGuestIdentity) {
await db.query(
`INSERT INTO user_identities (user_id, provider, provider_id)
VALUES ($1, 'guest', $2)
ON CONFLICT (provider, provider_id) DO NOTHING`,
[userId, guestId]
);
console.log(`Linked guest ID ${guestId} to user ${userId}`);
}
// Создаем новый диалог для этих сообщений // Создаем новый диалог для этих сообщений
const firstMessage = guestMessages[0]; const firstMessage = guestMessages[0];
const title = firstMessage.content.length > 30 const title = firstMessage.content.length > 30
@@ -44,23 +69,23 @@ async function processGuestMessages(userId, guestId) {
for (const guestMessage of guestMessages) { for (const guestMessage of guestMessages) {
console.log(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content}`); console.log(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content}`);
// Проверяем существование гостевого сообщения перед созданием связанного сообщения // Проверяем, не было ли это сообщение уже обработано
const checkGuestMessage = await db.query( const existingMessage = await db.query(
'SELECT id FROM guest_messages WHERE id = $1', 'SELECT id FROM messages WHERE guest_message_id = $1',
[guestMessage.id] [guestMessage.id]
); );
if (checkGuestMessage.rows.length === 0) { if (existingMessage.rows.length > 0) {
console.log(`Guest message ${guestMessage.id} no longer exists, skipping`); console.log(`Guest message ${guestMessage.id} already processed, skipping`);
continue; continue;
} }
// Сохраняем сообщение пользователя // Сохраняем сообщение пользователя
const userMessageResult = await db.query( const userMessageResult = await db.query(
`INSERT INTO messages `INSERT INTO messages
(conversation_id, content, sender_type, role, channel, created_at) (conversation_id, content, sender_type, role, channel, guest_message_id, created_at)
VALUES VALUES
($1, $2, $3, $4, $5, $6) ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`, RETURNING *`,
[ [
conversation.id, conversation.id,
@@ -68,13 +93,15 @@ async function processGuestMessages(userId, guestId) {
'user', 'user',
'user', 'user',
'web', 'web',
guestMessage.id,
guestMessage.created_at guestMessage.created_at
] ]
); );
console.log(`Saved user message with ID ${userMessageResult.rows[0].id}`); console.log(`Saved user message with ID ${userMessageResult.rows[0].id}`);
// Получаем ответ от ИИ // Получаем ответ от ИИ только для сообщений пользователя (не AI)
if (!guestMessage.is_ai) {
console.log('Getting AI response for:', guestMessage.content); console.log('Getting AI response for:', guestMessage.content);
const language = guestMessage.language || 'auto'; const language = guestMessage.language || 'auto';
const aiResponse = await aiAssistant.getResponse(guestMessage.content, language); const aiResponse = await aiAssistant.getResponse(guestMessage.content, language);
@@ -98,10 +125,7 @@ async function processGuestMessages(userId, guestId) {
); );
console.log(`Saved AI response with ID ${aiMessageResult.rows[0].id}`); console.log(`Saved AI response with ID ${aiMessageResult.rows[0].id}`);
}
// Удаляем обработанное гостевое сообщение
await db.query('DELETE FROM guest_messages WHERE id = $1', [guestMessage.id]);
console.log(`Deleted processed guest message ${guestMessage.id}`);
} }
return { return {
@@ -255,6 +279,24 @@ router.get('/history', async (req, res) => {
const limit = parseInt(req.query.limit) || 50; const limit = parseInt(req.query.limit) || 50;
const offset = parseInt(req.query.offset) || 0; const offset = parseInt(req.query.offset) || 0;
// Если пользователь аутентифицирован и у него есть гостевые сообщения,
// автоматически связываем их перед получением истории
if (req.session.authenticated && req.session.userId && req.session.guestId) {
try {
console.log('Automatically linking guest messages before fetching history');
await processGuestMessages(req.session.userId, req.session.guestId);
// Очищаем guestId из сессии после связывания
req.session.guestId = null;
await req.session.save();
console.log('Guest messages automatically linked');
} catch (linkError) {
console.error('Error auto-linking guest messages:', linkError);
// Продолжаем выполнение, даже если связывание не удалось
}
}
let messages = []; let messages = [];
let total = 0; let total = 0;
@@ -288,34 +330,6 @@ router.get('/history', async (req, res) => {
messages = result.rows; messages = result.rows;
console.log(`Found ${messages.length} messages for authenticated user`); console.log(`Found ${messages.length} messages for authenticated user`);
} }
// Если есть guestId, получаем гостевые сообщения
else if (req.session.guestId) {
const countResult = await db.query(
`SELECT COUNT(*) as total FROM guest_messages
WHERE guest_id = $1`,
[req.session.guestId]
);
total = parseInt(countResult.rows[0].total) || 0;
const result = await db.query(
`SELECT
id,
content,
'user' as sender_type,
'user' as role,
created_at,
guest_id as user_id,
NULL as conversation_id
FROM guest_messages
WHERE guest_id = $1
ORDER BY created_at ASC
LIMIT $2 OFFSET $3`,
[req.session.guestId, limit, offset]
);
messages = result.rows;
console.log(`Found ${messages.length} guest messages`);
}
return res.json({ return res.json({
success: true, success: true,
@@ -329,173 +343,6 @@ router.get('/history', async (req, res) => {
} }
}); });
// Маршрут для получения всех диалогов (только для админов) // Экспортируем маршрутизатор и функцию processGuestMessages отдельно
router.get('/admin/history', requireAdmin, async (req, res) => {
try {
const { limit = 50, offset = 0, userId } = req.query;
let query = `
SELECT ch.id, ch.user_id, u.username, ch.channel,
ch.sender_type, ch.content, ch.metadata, ch.created_at
FROM chat_history ch
LEFT JOIN users u ON ch.user_id = u.id
`;
const params = [];
let paramIndex = 1;
if (userId) {
query += ` WHERE ch.user_id = $${paramIndex}`;
params.push(userId);
paramIndex++;
}
query += ` ORDER BY ch.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
params.push(limit, offset);
const result = await db.query(query, params);
res.json(result.rows);
} catch (error) {
logger.error('Error fetching admin chat history:', error);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
}
});
// Обработчик для связывания гостевых сообщений с пользователем
router.post('/link-guest-messages', requireAuth, async (req, res) => {
try {
const { userId } = req.session;
const guestId = req.session.guestId;
console.log('Linking messages:', { userId, guestId });
if (!guestId) {
console.log('No guestId in session');
return res.json({ success: true, message: 'No guest messages to link' });
}
// Проверяем наличие гостевых сообщений
const guestMessagesCheck = await db.query(
'SELECT EXISTS(SELECT 1 FROM guest_messages WHERE guest_id = $1)',
[guestId]
);
console.log('Guest messages check:', guestMessagesCheck.rows[0]);
if (!guestMessagesCheck.rows[0].exists) {
console.log('No guest messages found for guestId:', guestId);
return res.json({ success: true, message: 'No guest messages to link' });
}
try {
// Обрабатываем гостевые сообщения для получения ответов от AI
console.log('Processing guest messages to get AI responses');
const result = await processGuestMessages(userId, guestId);
console.log('Guest messages processed:', result);
// Очищаем guestId из сессии после связывания
req.session.guestId = null;
await req.session.save();
console.log('Messages linked and processed successfully');
return res.json({
success: true,
message: 'Guest messages linked and processed',
result
});
} catch (processError) {
console.error('Error processing guest messages:', processError);
return res.status(500).json({
success: false,
error: 'Error processing guest messages',
details: processError.message
});
}
} catch (error) {
console.error('Error linking guest messages:', error);
return res.status(500).json({
success: false,
error: 'Internal server error',
details: error.message
});
}
});
// Обновляем маршрут верификации 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
});
}
});
// Маршрут для удаления сообщений
router.delete('/message/:id', requireAuth, async (req, res) => {
try {
const messageId = req.params.id;
const userId = req.session.userId;
// Проверяем права на удаление
const messageCheck = await db.query(
`SELECT m.id
FROM messages m
JOIN conversations c ON m.conversation_id = c.id
WHERE m.id = $1 AND c.user_id = $2`,
[messageId, userId]
);
if (messageCheck.rows.length === 0) {
return res.status(403).json({ error: 'Forbidden' });
}
// Удаляем сообщение
await db.query(
'DELETE FROM messages WHERE id = $1',
[messageId]
);
res.json({ success: true });
} catch (error) {
logger.error('Error deleting message:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Маршрут для проверки и инициализации сессии
router.get('/check-session', async (req, res) => {
try {
// Если у пользователя нет guestId, создаем его
if (!req.session.guestId) {
req.session.guestId = crypto.randomBytes(16).toString('hex');
await req.session.save();
console.log('Created new guestId:', req.session.guestId);
}
res.json({
success: true,
guestId: req.session.guestId,
isAuthenticated: req.session.authenticated || false
});
} catch (error) {
console.error('Error checking session:', error);
res.status(500).json({
success: false,
error: 'Internal server error'
});
}
});
module.exports = router; module.exports = router;
module.exports.processGuestMessages = processGuestMessages;

View File

@@ -8,6 +8,7 @@ const { app, nonceStore } = require('./app');
const usersRouter = require('./routes/users'); const usersRouter = require('./routes/users');
const authRouter = require('./routes/auth'); const authRouter = require('./routes/auth');
const identitiesRouter = require('./routes/identities'); const identitiesRouter = require('./routes/identities');
const chatRouter = require('./routes/chat');
const { pool } = require('./db'); const { pool } = require('./db');
const helmet = require('helmet'); const helmet = require('helmet');
const { getBot, stopBot } = require('./services/telegramBot'); const { getBot, stopBot } = require('./services/telegramBot');
@@ -79,6 +80,7 @@ app.use(session({
app.use('/api/users', usersRouter); app.use('/api/users', usersRouter);
app.use('/api/auth', authRouter); app.use('/api/auth', authRouter);
app.use('/api/identities', identitiesRouter); app.use('/api/identities', identitiesRouter);
app.use('/api/chat', chatRouter);
// Эндпоинт для проверки состояния сервера // Эндпоинт для проверки состояния сервера
app.get('/api/health', (req, res) => { app.get('/api/health', (req, res) => {

View File

@@ -391,6 +391,42 @@ class AuthService {
throw error; throw error;
} }
} }
// Добавляем псевдоним функции checkAdminRole для обратной совместимости
async checkAdminTokens(address) {
if (!address) return false;
console.log(`Checking admin tokens for address: ${address}`);
const isAdmin = await this.checkAdminRole(address);
console.log(`Admin token check result for ${address}: ${isAdmin}`);
// Обновляем роль пользователя в базе данных, если есть админские токены
if (isAdmin) {
try {
// Находим userId по адресу
const userResult = await db.query(`
SELECT u.id FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1`,
[address.toLowerCase()]
);
if (userResult.rows.length > 0) {
const userId = userResult.rows[0].id;
// Обновляем роль пользователя
await db.query(
'UPDATE users SET role = $1 WHERE id = $2',
['admin', userId]
);
console.log(`Updated user ${userId} role to admin based on token holdings`);
}
} catch (error) {
console.error('Error updating user role:', error);
}
}
return isAdmin;
}
} }
// Создаем и экспортируем единственный экземпляр // Создаем и экспортируем единственный экземпляр

View File

@@ -73,15 +73,36 @@ class EmailAuth {
const userId = result.userId || session.tempUserId; const userId = result.userId || session.tempUserId;
const email = session.pendingEmail; const email = session.pendingEmail;
// Добавляем email в базу данных // Проверяем, существует ли уже этот email в user_identities
const existingUserQuery = await db.query(
`SELECT user_id FROM user_identities
WHERE provider = 'email' AND provider_id = $1`,
[email.toLowerCase()]
);
let finalUserId = userId;
// Если email уже связан с другим пользователем
if (existingUserQuery.rows.length > 0) {
finalUserId = existingUserQuery.rows[0].user_id;
logger.info(`Using existing user ID ${finalUserId} for email ${email}`);
// Обновляем идентификатор пользователя в сессии
if (userId !== finalUserId) {
logger.info(`Changing user ID from ${userId} to ${finalUserId} based on existing email identity`);
}
} else {
// Добавляем email в базу данных для нового пользователя
await db.query( await db.query(
`INSERT INTO user_identities `INSERT INTO user_identities
(user_id, provider, provider_id) (user_id, provider, provider_id)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
ON CONFLICT (provider, provider_id) ON CONFLICT (provider, provider_id)
DO UPDATE SET user_id = $1`, DO UPDATE SET user_id = $1`,
[userId, 'email', email.toLowerCase()] [finalUserId, 'email', email.toLowerCase()]
); );
logger.info(`Added new email identity ${email} for user ${finalUserId}`);
}
// Очищаем временные данные // Очищаем временные данные
delete session.pendingEmail; delete session.pendingEmail;
@@ -91,7 +112,7 @@ class EmailAuth {
return { return {
verified: true, verified: true,
userId, userId: finalUserId,
email: email.toLowerCase() email: email.toLowerCase()
}; };
} catch (error) { } catch (error) {

View File

@@ -9,15 +9,136 @@ export function useAuth() {
const telegramId = ref(null); const telegramId = ref(null);
const isAdmin = ref(false); const isAdmin = ref(false);
const email = ref(null); const email = ref(null);
const processedGuestIds = ref([]);
const identities = ref([]);
// Функция для обновления списка идентификаторов
const updateIdentities = async () => {
if (!isAuthenticated.value || !userId.value) return;
try {
const response = await axios.get('/api/auth/identities');
if (response.data.success) {
identities.value = response.data.identities;
console.log('User identities updated:', identities.value);
}
} catch (error) {
console.error('Error fetching user identities:', error);
}
};
const updateAuth = ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin, email: newEmail }) => { const updateAuth = ({ authenticated, authType: newAuthType, userId: newUserId, address: newAddress, telegramId: newTelegramId, isAdmin: newIsAdmin, email: newEmail }) => {
isAuthenticated.value = authenticated; const wasAuthenticated = isAuthenticated.value;
authType.value = newAuthType; const previousUserId = userId.value;
userId.value = newUserId;
address.value = newAddress; console.log('updateAuth called with:', {
telegramId.value = newTelegramId; authenticated,
isAdmin.value = newIsAdmin; newAuthType,
email.value = newEmail; newUserId,
newAddress,
newTelegramId,
newIsAdmin,
newEmail
});
// Убедимся, что переменные являются реактивными
isAuthenticated.value = authenticated === true;
authType.value = newAuthType || null;
userId.value = newUserId || null;
address.value = newAddress || null;
telegramId.value = newTelegramId || null;
isAdmin.value = newIsAdmin === true;
email.value = newEmail || null;
console.log('Auth updated:', {
authenticated: isAuthenticated.value,
userId: userId.value,
address: address.value,
telegramId: telegramId.value,
email: email.value,
isAdmin: isAdmin.value
});
// Если пользователь только что аутентифицировался или сменил аккаунт,
// пробуем связать сообщения и обновить идентификаторы
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
console.log('Auth change detected, linking messages and updating identities');
linkMessages();
updateIdentities();
}
};
// Функция для связывания сообщений после успешной авторизации
const linkMessages = async () => {
try {
if (isAuthenticated.value) {
console.log('Linking messages after authentication');
// Создаем объект с идентификаторами для передачи на сервер
const identifiersData = {
userId: userId.value
};
// Добавляем все доступные идентификаторы
if (address.value) identifiersData.address = address.value;
if (email.value) identifiersData.email = email.value;
if (telegramId.value) identifiersData.telegramId = telegramId.value;
// Сохраняем предыдущий guestId из localStorage, если есть
const localGuestId = localStorage.getItem('guestId');
if (localGuestId && !processedGuestIds.value.includes(localGuestId)) {
console.log('Found local guestId:', localGuestId);
// Добавляем guestId в идентификаторы
identifiersData.guestId = localGuestId;
// Добавляем guestId в список обработанных
processedGuestIds.value.push(localGuestId);
}
// Логируем попытку связывания сообщений
console.log('Sending link-guest-messages request with data:', identifiersData);
try {
// Отправляем запрос на связывание сообщений
const response = await axios.post('/api/auth/link-guest-messages', identifiersData);
if (response.data.success) {
console.log('Messages linked successfully:', response.data);
// Если в ответе есть обработанные guestIds, добавляем их в список
if (response.data.results && Array.isArray(response.data.results)) {
const newProcessedIds = response.data.results
.filter(result => result.guestId)
.map(result => result.guestId);
if (newProcessedIds.length > 0) {
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...newProcessedIds])];
console.log('Updated processed guest IDs:', processedGuestIds.value);
}
}
// Очищаем гостевые сообщения из localStorage после успешного связывания
localStorage.removeItem('guestMessages');
localStorage.removeItem('guestId');
return {
success: true,
processedIds: processedGuestIds.value
};
}
} catch (error) {
console.error('Error linking messages:', error);
return {
success: false,
error: error.message
};
}
}
return { success: false, message: 'Not authenticated' };
} catch (error) {
console.error('Error in linkMessages:', error);
return { success: false, error: error.message };
}
}; };
const checkAuth = async () => { const checkAuth = async () => {
@@ -25,13 +146,33 @@ export function useAuth() {
const response = await axios.get('/api/auth/check'); const response = await axios.get('/api/auth/check');
console.log('Auth check response:', response.data); console.log('Auth check response:', response.data);
isAuthenticated.value = response.data.authenticated; const wasAuthenticated = isAuthenticated.value;
userId.value = response.data.userId; const previousUserId = userId.value;
isAdmin.value = response.data.isAdmin;
authType.value = response.data.authType; // Обновляем данные авторизации через updateAuth вместо прямого изменения
address.value = response.data.address; updateAuth({
telegramId.value = response.data.telegramId; authenticated: response.data.authenticated,
email.value = response.data.email; authType: response.data.authType,
userId: response.data.userId,
address: response.data.address,
telegramId: response.data.telegramId,
email: response.data.email,
isAdmin: response.data.isAdmin
});
// Если пользователь аутентифицирован, обновляем список идентификаторов и связываем сообщения
if (response.data.authenticated) {
// Сначала обновляем идентификаторы, чтобы иметь актуальные данные
await updateIdentities();
// Если пользователь только что аутентифицировался или сменил аккаунт,
// связываем гостевые сообщения с его аккаунтом
if (!wasAuthenticated || (previousUserId && previousUserId !== response.data.userId)) {
// Немедленно связываем сообщения
const linkResult = await linkMessages();
console.log('Link messages result on auth change:', linkResult);
}
}
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -42,6 +183,11 @@ export function useAuth() {
const disconnect = async () => { const disconnect = async () => {
try { try {
// Сохраняем текущий guestId перед выходом
const newGuestId = crypto.randomUUID();
localStorage.setItem('guestId', newGuestId);
console.log('Created new guestId for future session:', newGuestId);
await axios.post('/api/auth/logout'); await axios.post('/api/auth/logout');
updateAuth({ updateAuth({
authenticated: false, authenticated: false,
@@ -53,7 +199,10 @@ export function useAuth() {
isAdmin: false isAdmin: false
}); });
// Очищаем localStorage // Очищаем списки идентификаторов
identities.value = [];
// Очищаем localStorage кроме guestId
localStorage.removeItem('isAuthenticated'); localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userId'); localStorage.removeItem('userId');
localStorage.removeItem('address'); localStorage.removeItem('address');
@@ -66,6 +215,13 @@ export function useAuth() {
} }
}; };
// Обновляем список обработанных guestIds
const updateProcessedGuestIds = (ids) => {
if (Array.isArray(ids)) {
processedGuestIds.value = [...new Set([...processedGuestIds.value, ...ids])];
}
};
onMounted(async () => { onMounted(async () => {
await checkAuth(); await checkAuth();
}); });
@@ -78,8 +234,13 @@ export function useAuth() {
isAdmin, isAdmin,
telegramId, telegramId,
email, email,
identities,
processedGuestIds,
updateAuth, updateAuth,
checkAuth, checkAuth,
disconnect disconnect,
linkMessages,
updateIdentities,
updateProcessedGuestIds
}; };
} }

View File

@@ -179,18 +179,39 @@
<!-- Блок информации о пользователе --> <!-- Блок информации о пользователе -->
<div v-if="isAuthenticated" class="user-info"> <div v-if="isAuthenticated" class="user-info">
<h3>Идентификаторы:</h3> <h3>Идентификаторы:</h3>
<div v-if="auth.address?.value" class="user-info-item"> <!-- Основные идентификаторы из auth объекта -->
<template v-if="auth.address?.value">
<div class="user-info-item">
<span class="user-info-label">Кошелек:</span> <span class="user-info-label">Кошелек:</span>
<span class="user-info-value">{{ truncateAddress(auth.address.value) }}</span> <span class="user-info-value">{{ truncateAddress(auth.address.value) }}</span>
</div> </div>
<div v-if="auth.telegramId" class="user-info-item"> </template>
<template v-if="auth.telegramId?.value">
<div class="user-info-item">
<span class="user-info-label">Telegram:</span> <span class="user-info-label">Telegram:</span>
<span class="user-info-value">{{ auth.telegramId }}</span> <span class="user-info-value">{{ auth.telegramId.value }}</span>
</div> </div>
<div v-if="auth.email" class="user-info-item"> </template>
<template v-if="auth.email?.value">
<div class="user-info-item">
<span class="user-info-label">Email:</span> <span class="user-info-label">Email:</span>
<span class="user-info-value">{{ auth.email }}</span> <span class="user-info-value">{{ auth.email.value }}</span>
</div> </div>
</template>
<!-- Дополнительные идентификаторы из списка identities (кроме уже отображенных) -->
<template v-for="identity in filteredIdentities" :key="`${identity.provider}-${identity.provider_id}`">
<div class="user-info-item">
<span class="user-info-label">{{ formatIdentityProvider(identity.provider) }}:</span>
<span class="user-info-value">{{
identity.provider === 'wallet'
? truncateAddress(identity.provider_id)
: identity.provider_id
}}</span>
</div>
</template>
</div> </div>
</div> </div>
</div> </div>
@@ -223,7 +244,7 @@ const userLanguage = ref('ru');
const isLoadingMore = ref(false); const isLoadingMore = ref(false);
const hasMoreMessages = ref(false); const hasMoreMessages = ref(false);
const offset = ref(0); const offset = ref(0);
const limit = ref(20); const limit = ref(30);
// Состояния для верификации // Состояния для верификации
const showTelegramVerification = ref(false); const showTelegramVerification = ref(false);
@@ -266,6 +287,32 @@ const tokenBalances = ref({
// Состояние для отображения правой панели // Состояние для отображения правой панели
const showWalletSidebar = ref(false); const showWalletSidebar = ref(false);
// Вычисленное свойство для фильтрации идентификаторов
const filteredIdentities = computed(() => {
if (!auth.identities?.value) return [];
// Фильтруем и оставляем только постоянные идентификаторы
return auth.identities.value.filter(identity =>
identity.provider !== 'guest' && // Исключаем guest идентификаторы
// Также исключаем дубликаты, если идентификатор уже показан выше
!(identity.provider === 'wallet' && auth.address?.value) &&
!(identity.provider === 'telegram' && auth.telegramId?.value) &&
!(identity.provider === 'email' && auth.email?.value)
);
});
// Функция для форматирования названий провайдеров
const formatIdentityProvider = (provider) => {
const providers = {
'wallet': 'Кошелек',
'telegram': 'Telegram',
'email': 'Email',
'guest': 'Гость'
};
return providers[provider] || provider;
};
// Функция для управления сайдбаром // Функция для управления сайдбаром
const toggleSidebar = () => { const toggleSidebar = () => {
showSidebar.value = !showSidebar.value; showSidebar.value = !showSidebar.value;
@@ -551,86 +598,44 @@ const cancelTelegramAuth = () => {
} }
}; };
// Загружаем сообщения при изменении аутентификации // Вычисленное свойство для определения, нужно ли загружать историю
watch(() => isAuthenticated.value, async (newValue, oldValue) => { const shouldLoadHistory = computed(() => {
// Если пользователь только что авторизовался // Загружаем историю только если пользователь авторизован или есть гостевой ID
if (newValue && !oldValue) { return isAuthenticated.value ||
try { (localStorage.getItem('guestId') && localStorage.getItem('guestId').length > 0);
// Связываем гостевые сообщения только один раз при первой авторизации });
const response = await api.post('/api/chat/link-guest-messages');
console.log('Guest messages linking response:', response.data);
} catch (linkError) {
console.error('Error linking guest messages:', linkError);
}
}
// В любом случае перезагружаем сообщения // Следим за изменением авторизации
watch(() => auth.isAuthenticated.value, async (newValue, oldValue) => {
console.log('Auth state changed:', {
from: oldValue,
to: newValue,
userId: auth.userId.value
});
if (newValue === true) {
// Если пользователь только что авторизовался
console.log('User authenticated, loading message history');
// Сначала связываем сообщения, затем загружаем историю
try {
// Сбрасываем текущие сообщения
messages.value = []; messages.value = [];
offset.value = 0; offset.value = 0;
hasMoreMessages.value = true;
await loadMoreMessages();
await nextTick(); // Сначала связываем гостевые сообщения
scrollToBottom(); await auth.linkMessages();
});
// Обработчик для Telegram аутентификации // Затем загружаем историю сообщений после небольшой задержки,
const handleTelegramAuth = async () => { // чтобы сервер успел обработать связанные сообщения
try { setTimeout(async () => {
const { data } = await axios.post('/api/auth/telegram/init'); await loadMoreMessages(true);
const { verificationCode, botLink } = data; }, 1000);
// Показываем код верификации
showTelegramVerification.value = true;
telegramVerificationCode.value = verificationCode;
telegramBotLink.value = botLink;
// Запускаем проверку статуса аутентификации
telegramAuthCheckInterval.value = setInterval(async () => {
try {
const response = await axios.get('/api/auth/check');
console.log('Проверка авторизации:', response.data);
if (response.data.authenticated) {
// Обновляем состояние аутентификации с полным набором данных
auth.updateAuth({
isAuthenticated: true,
authenticated: true,
authType: response.data.authType,
userId: response.data.userId,
telegramId: response.data.telegramId,
isAdmin: response.data.isAdmin,
address: response.data.address
});
console.log('Telegram authentication successful:', response.data);
// Обновляем баланс токенов
await updateBalances();
clearInterval(telegramAuthCheckInterval.value);
telegramAuthCheckInterval.value = null;
showTelegramVerification.value = false;
}
} catch (error) { } catch (error) {
console.error('Error checking auth status:', error); console.error('Error loading history after auth:', error);
} }
}, 2000);
// Очищаем интервал через 5 минут
setTimeout(() => {
if (telegramAuthCheckInterval.value) {
clearInterval(telegramAuthCheckInterval.value);
telegramAuthCheckInterval.value = null;
showTelegramVerification.value = false;
} }
}, 5 * 60 * 1000); });
} catch (error) {
console.error('Error initializing Telegram auth:', error);
alert('Ошибка при инициализации Telegram аутентификации');
}
};
// Функция для сокращения адреса кошелька // Функция для сокращения адреса кошелька
const truncateAddress = (address) => { const truncateAddress = (address) => {
@@ -641,63 +646,100 @@ const truncateAddress = (address) => {
// Функция прокрутки к последнему сообщению // Функция прокрутки к последнему сообщению
const scrollToBottom = () => { const scrollToBottom = () => {
if (messagesContainer.value) { if (messagesContainer.value) {
setTimeout(() => {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight; messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}, 100);
} }
}; };
// Загрузка сообщений // Функция для загрузки сообщений
const loadMoreMessages = async () => { const loadMoreMessages = async (silent = false) => {
if (isLoadingMore.value) return;
try { try {
isLoadingMore.value = true; isLoadingMore.value = true;
if (!silent) isLoading.value = true;
console.log('Fetching chat history...'); console.log('Fetching chat history...');
// Всегда запрашиваем историю, так как на сервере проверяется наличие // Проверяем сессию перед загрузкой истории
// userId или guestId в сессии и возвращаются соответствующие сообщения try {
const response = await api.get('/api/chat/history', { const sessionCheck = await axios.get('/api/auth/check');
console.log('Session check:', sessionCheck.data);
// Проверяем, что пользователь все еще аутентифицирован
if (!sessionCheck.data.authenticated && !shouldLoadHistory.value) {
console.warn('User is not authenticated, skipping message history load');
isLoadingMore.value = false;
isLoading.value = false;
return;
}
} catch (error) {
console.warn('Session check failed, but continuing anyway:', error);
}
// Сначала запрашиваем общее количество сообщений
const countResponse = await axios.get('/api/chat/history', {
params: { params: {
limit: limit.value, count_only: true
offset: offset.value }
});
if (!countResponse.data.success) {
throw new Error('Failed to get message count');
}
const totalMessages = countResponse.data.total || 0;
console.log(`Total messages in history: ${totalMessages}`);
// Рассчитываем offset так, чтобы получить последние сообщения
// при первой загрузке (когда offset = 0)
let effectiveOffset = offset.value;
if (offset.value === 0 && totalMessages > limit.value) {
// Загружаем последние сообщения вместо первых
effectiveOffset = Math.max(0, totalMessages - limit.value);
}
// Получаем историю сообщений с параметрами пагинации
const response = await axios.get('/api/chat/history', {
params: {
offset: effectiveOffset,
limit: limit.value
} }
}); });
console.log('Chat history response:', response.data); console.log('Chat history response:', response.data);
if (response.data.success) { if (response.data.success) {
const newMessages = response.data.messages.map(msg => { // Если это первая загрузка, заменяем сообщения
console.log('Processing message:', msg); // Иначе, добавляем полученные сообщения к существующим
return { if (offset.value === 0) {
id: msg.id, messages.value = response.data.messages || [];
content: msg.content, } else if (response.data.messages && response.data.messages.length) {
sender_type: msg.sender_type || (msg.role === 'assistant' ? 'assistant' : 'user'), messages.value = [...messages.value, ...response.data.messages];
role: msg.role || (msg.sender_type === 'assistant' ? 'assistant' : 'user'), }
timestamp: msg.created_at,
showAuthOptions: false
};
});
console.log('Processed messages:', newMessages); // Обновляем offset для следующей загрузки
offset.value = effectiveOffset + (response.data.messages?.length || 0);
// Объединяем сообщения и сортируем их по timestamp // Проверяем, есть ли еще сообщения для загрузки
const allMessages = [...messages.value, ...newMessages]; hasMoreMessages.value = offset.value < totalMessages;
allMessages.sort((a, b) => { }
const timeA = new Date(a.timestamp || a.created_at).getTime();
const timeB = new Date(b.timestamp || b.created_at).getTime();
return timeA - timeB;
});
messages.value = allMessages; // После загрузки первой порции сообщений считаем, что пользователь уже отправлял сообщения
console.log('Updated messages array:', messages.value); if (messages.value.length > 0) {
hasMoreMessages.value = response.data.total > messages.value.length; hasUserSentMessage.value = true;
offset.value += newMessages.length; localStorage.setItem('hasUserSentMessage', 'true');
}
// Прокручиваем к последнему сообщению // Прокручиваем контейнер с сообщениями вниз
await nextTick(); await nextTick();
scrollToBottom(); scrollToBottom();
}
} catch (error) { } catch (error) {
console.error('Error loading chat history:', error); console.error('Error loading chat history:', error);
} finally { } finally {
isLoadingMore.value = false; isLoadingMore.value = false;
isLoading.value = false;
} }
}; };
@@ -786,22 +828,33 @@ const formatMessage = (text) => {
return DOMPurify.sanitize(rawHtml); return DOMPurify.sanitize(rawHtml);
}; };
// Функция для проверки наличия гостевых сообщений // Проверяет наличие гостевых сообщений
const checkGuestMessages = async () => { const checkGuestMessages = async () => {
try { try {
const response = await api.get('/api/chat/check-session'); // Проверяем сессию через auth/check вместо chat/check-session
console.log('Session check response:', response.data); const sessionCheck = await axios.get('/api/auth/check');
console.log('Session auth check response:', sessionCheck.data);
// После инициализации сессии загружаем сообщения // Проверяем наличие сообщений в localStorage
if (!isAuthenticated.value) { const storedMessages = localStorage.getItem('guestMessages');
// Если пользователь не авторизован, попробуем загрузить гостевые сообщения if (storedMessages) {
await loadMoreMessages(); const parsedMessages = JSON.parse(storedMessages);
if (Array.isArray(parsedMessages) && parsedMessages.length > 0) {
// Если есть сообщения и пользователь не аутентифицирован, показываем их
if (!auth.isAuthenticated.value) {
console.log('Found guest messages in localStorage:', parsedMessages);
messages.value = [...messages.value, ...parsedMessages];
hasUserSentMessage.value = true;
localStorage.setItem('hasUserSentMessage', 'true');
} else {
// Если пользователь аутентифицирован, удаляем гостевые сообщения из localStorage
console.log('User is authenticated, removing guest messages from localStorage');
localStorage.removeItem('guestMessages');
}
}
} }
return response.data;
} catch (error) { } catch (error) {
console.error('Error checking guest messages:', error); console.error('Error checking guest messages:', error);
return { success: false };
} }
}; };
@@ -852,6 +905,21 @@ watch(() => auth.isAuthenticated.value, async (newValue) => {
} }
}); });
// Отслеживаем изменения в идентификаторах
watch(() => auth.telegramId?.value, (newValue) => {
if (newValue) {
console.log('Telegram ID изменился:', newValue);
}
});
// Отслеживаем успешную авторизацию через Telegram
watch(() => auth.authType?.value, (newValue) => {
if (newValue === 'telegram') {
console.log('Авторизация через Telegram завершена, authType:', newValue);
console.log('Текущий telegramId:', auth.telegramId?.value);
}
});
onBeforeUnmount(() => { onBeforeUnmount(() => {
// Удаляем слушатель // Удаляем слушатель
if (messagesContainer.value) { if (messagesContainer.value) {
@@ -862,4 +930,79 @@ onBeforeUnmount(() => {
} }
document.querySelector('.app-container')?.classList.remove('menu-open'); document.querySelector('.app-container')?.classList.remove('menu-open');
}); });
// Обработчик для Telegram аутентификации
const handleTelegramAuth = async () => {
try {
const { data } = await axios.post('/api/auth/telegram/init');
const { verificationCode, botLink } = data;
// Показываем код верификации
showTelegramVerification.value = true;
telegramVerificationCode.value = verificationCode;
telegramBotLink.value = botLink;
// Запускаем проверку статуса аутентификации
telegramAuthCheckInterval.value = setInterval(async () => {
try {
const response = await axios.get('/api/auth/check');
console.log('Проверка авторизации:', response.data);
if (response.data.authenticated && response.data.telegramId) {
clearInterval(telegramAuthCheckInterval.value);
telegramAuthCheckInterval.value = null;
showTelegramVerification.value = false;
console.log('Telegram ID получен:', response.data.telegramId);
// Обновляем информацию об авторизации через метод updateAuth
auth.updateAuth({
authenticated: true,
authType: response.data.authType,
userId: response.data.userId,
telegramId: response.data.telegramId,
isAdmin: response.data.isAdmin
});
// Сначала обновляем идентификаторы, затем связываем сообщения и обновляем историю
await auth.checkAuth();
// Весь процесс загрузки истории будет выполнен через watch на isAuthenticated
}
} catch (error) {
console.error('Error checking auth status:', error);
}
}, 2000);
// Очищаем интервал через 5 минут
setTimeout(() => {
if (telegramAuthCheckInterval.value) {
clearInterval(telegramAuthCheckInterval.value);
telegramAuthCheckInterval.value = null;
showTelegramVerification.value = false;
}
}, 5 * 60 * 1000);
} catch (error) {
console.error('Error initializing Telegram auth:', error);
alert('Ошибка при инициализации Telegram аутентификации');
}
};
// Отслеживаем изменения в сообщениях
watch(() => messages.value.length, (newLength, oldLength) => {
if (newLength > 0) {
// Сортируем сообщения по дате/времени
messages.value.sort((a, b) => {
const dateA = new Date(a.timestamp || a.created_at);
const dateB = new Date(b.timestamp || b.created_at);
return dateA - dateB;
});
// Прокручиваем к последнему сообщению
nextTick(() => {
scrollToBottom();
});
}
});
</script> </script>