ваше сообщение коммита
This commit is contained in:
24
backend/db/migrations/011_cleanup_guest_relations.sql
Normal file
24
backend/db/migrations/011_cleanup_guest_relations.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-- Удаляем колонку guest_message_id из таблицы messages
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'messages' AND column_name = 'guest_message_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE messages DROP COLUMN guest_message_id;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Удаляем гостевые идентификаторы из user_identities
|
||||||
|
DELETE FROM user_identities WHERE provider = 'guest';
|
||||||
|
|
||||||
|
-- Удаляем индекс для guest_message_id если он существует
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_indexes
|
||||||
|
WHERE tablename = 'messages' AND indexname = 'idx_messages_guest_message_id'
|
||||||
|
) THEN
|
||||||
|
DROP INDEX idx_messages_guest_message_id;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -183,6 +183,9 @@ router.post('/verify', async (req, res) => {
|
|||||||
req.session.isAdmin = isAdmin;
|
req.session.isAdmin = isAdmin;
|
||||||
req.session.address = address.toLowerCase();
|
req.session.address = address.toLowerCase();
|
||||||
|
|
||||||
|
// Удаляем временный ID
|
||||||
|
delete req.session.tempUserId;
|
||||||
|
|
||||||
// Сохраняем сессию перед связыванием сообщений
|
// Сохраняем сессию перед связыванием сообщений
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
req.session.save(err => {
|
req.session.save(err => {
|
||||||
@@ -218,105 +221,138 @@ router.post('/verify', async (req, res) => {
|
|||||||
// Аутентификация через Telegram
|
// Аутентификация через Telegram
|
||||||
router.post('/telegram/verify', async (req, res) => {
|
router.post('/telegram/verify', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { telegramId, authData } = req.body;
|
const { code } = req.body;
|
||||||
|
|
||||||
logger.info(`[telegram/verify] Authentication request with telegramId: ${telegramId}`);
|
logger.info(`[telegram/verify] Verifying code: ${code}`);
|
||||||
|
|
||||||
// Сохраняем гостевые ID до проверки
|
// Проверяем код верификации
|
||||||
const guestId = req.session.guestId;
|
const verificationResult = await verifyTelegramCode(code);
|
||||||
const previousGuestId = req.session.previousGuestId;
|
|
||||||
|
|
||||||
logger.info(`[telegram/verify] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`);
|
if (!verificationResult.success) {
|
||||||
|
return res.status(400).json({
|
||||||
// Проверяем данные от Telegram
|
|
||||||
const isValid = await authService.verifyTelegramAuth(authData);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
logger.warn(`[telegram/verify] Invalid Telegram authentication data for user ${telegramId}`);
|
|
||||||
return res.status(401).json({
|
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Ошибка верификации Telegram'
|
error: 'Неверный код подтверждения'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[telegram/verify] Telegram authentication data verified for user ${telegramId}`);
|
const { telegramId, authData } = verificationResult;
|
||||||
|
logger.info(`[telegram/verify] Code verified successfully for telegramId: ${telegramId}`);
|
||||||
|
|
||||||
// Получаем или создаем пользователя
|
// Сохраняем гостевые ID до проверки
|
||||||
let userId = await authService.getUserIdByIdentity('telegram', telegramId);
|
const guestId = req.session.guestId;
|
||||||
|
|
||||||
if (!userId) {
|
let userId;
|
||||||
// Создаем нового пользователя
|
|
||||||
const userResult = await db.query(
|
|
||||||
'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id'
|
|
||||||
);
|
|
||||||
|
|
||||||
userId = userResult.rows[0].id;
|
// Если пользователь уже аутентифицирован, добавляем Telegram к существующему аккаунту
|
||||||
logger.info(`[telegram/verify] Created new user with ID ${userId} for Telegram user ${telegramId}`);
|
if (req.session.authenticated && req.session.userId) {
|
||||||
} else {
|
userId = req.session.userId;
|
||||||
logger.info(`[telegram/verify] Found existing user ID ${userId} for Telegram user ${telegramId}`);
|
|
||||||
|
// Проверяем не связан ли Telegram с другим аккаунтом
|
||||||
|
const existingTelegram = await db.query(`
|
||||||
|
SELECT user_id FROM user_identities
|
||||||
|
WHERE provider = 'telegram' AND provider_id = $1
|
||||||
|
`, [telegramId]);
|
||||||
|
|
||||||
|
if (existingTelegram.rows.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Этот Telegram аккаунт уже связан с другим пользователем'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Явно сохраняем идентификатор Telegram
|
// Добавляем Telegram к существующему пользователю
|
||||||
await saveUserIdentity(userId, 'telegram', telegramId, true);
|
await saveUserIdentity(userId, 'telegram', telegramId, true);
|
||||||
logger.info(`[telegram/verify] Saved Telegram identity ${telegramId} for user ${userId}`);
|
logger.info(`[telegram/verify] Added Telegram identity ${telegramId} to existing user ${userId}`);
|
||||||
|
|
||||||
// Если есть гостевые ID, сохраняем их
|
} else {
|
||||||
if (guestId) {
|
// Ищем существующего пользователя по Telegram ID
|
||||||
await saveUserIdentity(userId, 'guest', guestId, true);
|
const existingUser = await db.query(`
|
||||||
logger.info(`[telegram/verify] Saved guest ID ${guestId} for user ${userId}`);
|
SELECT u.* FROM users u
|
||||||
|
JOIN user_identities ui ON u.id = ui.user_id
|
||||||
|
WHERE ui.provider = 'telegram' AND ui.provider_id = $1
|
||||||
|
`, [telegramId]);
|
||||||
|
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
// Используем существующего пользователя
|
||||||
|
userId = existingUser.rows[0].id;
|
||||||
|
logger.info(`[telegram/verify] Found existing user with ID ${userId} for telegramId ${telegramId}`);
|
||||||
|
} else {
|
||||||
|
// Создаем нового пользователя
|
||||||
|
const newUser = await db.query(
|
||||||
|
'INSERT INTO users (role) VALUES ($1) RETURNING id',
|
||||||
|
['user']
|
||||||
|
);
|
||||||
|
userId = newUser.rows[0].id;
|
||||||
|
|
||||||
|
// Добавляем Telegram идентификатор
|
||||||
|
await saveUserIdentity(userId, 'telegram', telegramId, true);
|
||||||
|
logger.info(`[telegram/verify] Created new user with ID ${userId} for telegramId ${telegramId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousGuestId && previousGuestId !== guestId) {
|
// Если есть гостевые сообщения, переносим их
|
||||||
await saveUserIdentity(userId, 'guest', previousGuestId, true);
|
if (guestId && !req.session.processedGuestIds?.includes(guestId)) {
|
||||||
logger.info(`[telegram/verify] Saved previous guest ID ${previousGuestId} for user ${userId}`);
|
await processGuestMessages(userId, guestId);
|
||||||
|
// Сохраняем обработанный guestId чтобы избежать повторной обработки
|
||||||
|
if (!req.session.processedGuestIds) {
|
||||||
|
req.session.processedGuestIds = [];
|
||||||
|
}
|
||||||
|
req.session.processedGuestIds.push(guestId);
|
||||||
|
logger.info(`[telegram/verify] Processed guest messages for user ${userId} with guest ID ${guestId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем сессию
|
// Создаем новую сессию
|
||||||
|
req.session.regenerate(async (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('Error regenerating session:', err);
|
||||||
|
return res.status(500).json({ success: false, error: 'Server error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем данные новой сессии
|
||||||
|
req.session.authenticated = true;
|
||||||
req.session.userId = userId;
|
req.session.userId = userId;
|
||||||
req.session.telegramId = telegramId;
|
req.session.telegramId = telegramId;
|
||||||
req.session.authType = 'telegram';
|
req.session.authType = 'telegram';
|
||||||
req.session.authenticated = true;
|
|
||||||
|
|
||||||
// Дополнительно устанавливаем данные из Telegram, если они есть
|
// Сохраняем список обработанных гостевых ID
|
||||||
|
if (req.session.processedGuestIds?.length > 0) {
|
||||||
|
req.session.processedGuestIds = [...req.session.processedGuestIds];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем временные данные
|
||||||
|
delete req.session.tempUserId;
|
||||||
|
delete req.session.guestId;
|
||||||
|
delete req.session.pendingTelegramId;
|
||||||
|
|
||||||
if (authData.first_name) {
|
if (authData.first_name) {
|
||||||
req.session.telegramFirstName = authData.first_name;
|
req.session.telegramFirstName = authData.first_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authData.username) {
|
if (authData.username) {
|
||||||
req.session.telegramUsername = authData.username;
|
req.session.telegramUsername = authData.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем сессию перед связыванием сообщений
|
// Сохраняем сессию
|
||||||
await new Promise((resolve, reject) => {
|
req.session.save((err) => {
|
||||||
req.session.save(err => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error('[telegram/verify] Error saving session:', err);
|
logger.error('Error saving session:', err);
|
||||||
reject(err);
|
return res.status(500).json({ success: false, error: 'Server error' });
|
||||||
} else {
|
|
||||||
logger.info(`[telegram/verify] Session saved successfully for telegramId ${telegramId}, userId ${userId}`);
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Связываем гостевые сообщения с пользователем
|
res.json({
|
||||||
const linkResults = await linkGuestMessagesAfterAuth(req.session, userId);
|
|
||||||
logger.info(`[telegram/verify] Guest messages linking results:`, linkResults);
|
|
||||||
|
|
||||||
// Возвращаем успешный ответ
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
success: true,
|
||||||
userId,
|
userId,
|
||||||
telegramId,
|
telegramId,
|
||||||
authenticated: true
|
authenticated: true,
|
||||||
|
authType: 'telegram',
|
||||||
|
username: authData.username,
|
||||||
|
firstName: authData.first_name
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[telegram/verify] Error:', error);
|
logger.error('[telegram/verify] Error:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({ success: false, error: 'Server error' });
|
||||||
success: false,
|
|
||||||
error: 'Ошибка сервера при аутентификации через Telegram'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -366,6 +402,38 @@ router.post('/email/request', authLimiter, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Функция для проверки кода email
|
||||||
|
async function verifyEmailCode(code, email) {
|
||||||
|
try {
|
||||||
|
logger.info(`[verifyEmailCode] Verifying code ${code} for email ${email}`);
|
||||||
|
|
||||||
|
// Получаем код из базы данных
|
||||||
|
const result = await db.query(
|
||||||
|
`SELECT * FROM verification_codes
|
||||||
|
WHERE code = $1 AND provider_id = $2 AND provider = 'email'
|
||||||
|
AND used = false AND expires_at > NOW()`,
|
||||||
|
[code, email]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
logger.warn(`[verifyEmailCode] No valid code found for ${email}`);
|
||||||
|
return { success: false, error: 'Неверный или истекший код' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Помечаем код как использованный
|
||||||
|
await db.query(
|
||||||
|
'UPDATE verification_codes SET used = true WHERE code = $1 AND provider_id = $2 AND provider = \'email\'',
|
||||||
|
[code, email]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`[verifyEmailCode] Code verified successfully for ${email}`);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[verifyEmailCode] Error:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Маршрут для верификации email
|
// Маршрут для верификации email
|
||||||
router.post('/email/verify', async (req, res) => {
|
router.post('/email/verify', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -385,98 +453,122 @@ router.post('/email/verify', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем код через сервис верификации
|
// Сохраняем гостевые ID до проверки
|
||||||
const result = await verificationService.verifyCode(code, 'email', req.session.pendingEmail);
|
const guestId = req.session.guestId;
|
||||||
|
|
||||||
if (!result.success) {
|
// Проверяем код через сервис верификации
|
||||||
|
const verificationResult = await verifyEmailCode(code, req.session.pendingEmail);
|
||||||
|
|
||||||
|
if (!verificationResult.success) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: result.error || 'Неверный код подтверждения'
|
error: verificationResult.error || 'Неверный код подтверждения'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = result.userId;
|
let userId;
|
||||||
const email = req.session.pendingEmail;
|
|
||||||
|
|
||||||
// Проверяем, существует ли пользователь
|
// Если пользователь уже аутентифицирован, добавляем email к существующему аккаунту
|
||||||
const userResult = await db.query(
|
if (req.session.authenticated && req.session.userId) {
|
||||||
'SELECT * FROM users WHERE id = $1',
|
userId = req.session.userId;
|
||||||
[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userResult.rows.length === 0) {
|
// Проверяем не связан ли email с другим аккаунтом
|
||||||
return res.status(404).json({
|
const existingEmail = await db.query(`
|
||||||
|
SELECT user_id FROM user_identities
|
||||||
|
WHERE provider = 'email' AND provider_id = $1
|
||||||
|
`, [req.session.pendingEmail]);
|
||||||
|
|
||||||
|
if (existingEmail.rows.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Пользователь не найден'
|
error: 'Этот email уже связан с другим пользователем'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем email в базу данных
|
// Добавляем email к существующему пользователю
|
||||||
await db.query(
|
await saveUserIdentity(userId, 'email', req.session.pendingEmail, true);
|
||||||
`INSERT INTO user_identities
|
logger.info(`[email/verify] Added email identity ${req.session.pendingEmail} to existing user ${userId}`);
|
||||||
(user_id, provider, provider_id)
|
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
ON CONFLICT (provider, provider_id)
|
|
||||||
DO UPDATE SET user_id = $1`,
|
|
||||||
[userId, 'email', email.toLowerCase()]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Проверяем наличие кошелька и определяем роль
|
|
||||||
const wallet = await authService.getLinkedWallet(userId);
|
|
||||||
let role = 'user'; // Базовая роль для доступа к чату
|
|
||||||
|
|
||||||
if (wallet) {
|
|
||||||
// Если есть кошелек, проверяем баланс токенов
|
|
||||||
const isAdmin = await authService.checkAdminRole(wallet);
|
|
||||||
role = isAdmin ? 'admin' : 'user';
|
|
||||||
logger.info(`User ${userId} has wallet ${wallet}, role set to ${role}`);
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(`User ${userId} has no wallet, using basic user role`);
|
// Ищем существующего пользователя по email
|
||||||
|
const existingUser = await db.query(`
|
||||||
|
SELECT u.* FROM users u
|
||||||
|
JOIN user_identities ui ON u.id = ui.user_id
|
||||||
|
WHERE ui.provider = 'email' AND ui.provider_id = $1
|
||||||
|
`, [req.session.pendingEmail]);
|
||||||
|
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
// Используем существующего пользователя
|
||||||
|
userId = existingUser.rows[0].id;
|
||||||
|
logger.info(`[email/verify] Found existing user with ID ${userId} for email ${req.session.pendingEmail}`);
|
||||||
|
} else {
|
||||||
|
// Создаем нового пользователя
|
||||||
|
const newUser = await db.query(
|
||||||
|
'INSERT INTO users (role) VALUES ($1) RETURNING id',
|
||||||
|
['user']
|
||||||
|
);
|
||||||
|
userId = newUser.rows[0].id;
|
||||||
|
|
||||||
|
// Добавляем email идентификатор
|
||||||
|
await saveUserIdentity(userId, 'email', req.session.pendingEmail, true);
|
||||||
|
logger.info(`[email/verify] Created new user with ID ${userId} for email ${req.session.pendingEmail}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем аутентификацию пользователя
|
// Если есть гостевые сообщения, переносим их
|
||||||
|
if (guestId && !req.session.processedGuestIds?.includes(guestId)) {
|
||||||
|
await processGuestMessages(userId, guestId);
|
||||||
|
// Сохраняем обработанный guestId чтобы избежать повторной обработки
|
||||||
|
if (!req.session.processedGuestIds) {
|
||||||
|
req.session.processedGuestIds = [];
|
||||||
|
}
|
||||||
|
req.session.processedGuestIds.push(guestId);
|
||||||
|
logger.info(`[email/verify] Processed guest messages for user ${userId} with guest ID ${guestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем новую сессию
|
||||||
|
req.session.regenerate(async (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('Error regenerating session:', err);
|
||||||
|
return res.status(500).json({ success: false, error: 'Server error' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем данные новой сессии
|
||||||
req.session.authenticated = true;
|
req.session.authenticated = true;
|
||||||
req.session.userId = userId;
|
req.session.userId = userId;
|
||||||
req.session.email = email.toLowerCase();
|
req.session.email = req.session.pendingEmail;
|
||||||
req.session.authType = 'email';
|
req.session.authType = 'email';
|
||||||
req.session.role = role;
|
|
||||||
|
|
||||||
if (wallet) {
|
// Сохраняем список обработанных гостевых ID
|
||||||
req.session.address = wallet;
|
if (req.session.processedGuestIds?.length > 0) {
|
||||||
|
req.session.processedGuestIds = [...req.session.processedGuestIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Очищаем временные данные
|
// Удаляем временные данные
|
||||||
delete req.session.pendingEmail;
|
|
||||||
delete req.session.tempUserId;
|
delete req.session.tempUserId;
|
||||||
|
delete req.session.guestId;
|
||||||
|
delete req.session.pendingEmail;
|
||||||
|
|
||||||
// Сохраняем сессию перед отправкой ответа
|
// Сохраняем сессию
|
||||||
await new Promise((resolve, reject) => {
|
req.session.save((err) => {
|
||||||
req.session.save(err => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error('Error saving session:', err);
|
logger.error('Error saving session:', err);
|
||||||
reject(err);
|
return res.status(500).json({ success: false, error: 'Server error' });
|
||||||
} else {
|
|
||||||
logger.info(`Session saved successfully for user ${userId} with role ${role}`);
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Связываем гостевые сообщения с пользователем
|
res.json({
|
||||||
await linkGuestMessagesAfterAuth(req.session, userId);
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
success: true,
|
||||||
userId,
|
userId,
|
||||||
email: email.toLowerCase(),
|
email: req.session.email,
|
||||||
role,
|
authenticated: true,
|
||||||
wallet: wallet || null,
|
authType: 'email'
|
||||||
message: 'Аутентификация успешна'
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in email verification:', error);
|
logger.error('[email/verify] Error:', error);
|
||||||
res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' });
|
res.status(500).json({ success: false, error: 'Server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -637,29 +729,29 @@ router.get('/check', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Выход из системы
|
// Выход из системы
|
||||||
router.post('/logout', async (req, res) => {
|
router.post('/logout', (req, res) => {
|
||||||
try {
|
try {
|
||||||
logger.info('[logout] Logout request received');
|
// Сохраняем важные данные перед уничтожением сессии
|
||||||
|
const guestId = req.session.guestId;
|
||||||
|
|
||||||
const sessionService = require('../services/session-service');
|
// Полностью уничтожаем сессию
|
||||||
const result = await sessionService.logout(req.session);
|
req.session.destroy((err) => {
|
||||||
|
if (err) {
|
||||||
if (result) {
|
logger.error('Error destroying session:', err);
|
||||||
logger.info('[logout] User successfully logged out');
|
return res.status(500).json({ success: false, error: 'Server error' });
|
||||||
return res.json({ success: true });
|
|
||||||
} else {
|
|
||||||
logger.warn('[logout] Error during logout process');
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Error during logout process'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
logger.error('[logout] Error:', error);
|
// Очищаем куки сессии
|
||||||
return res.status(500).json({
|
res.clearCookie('connect.sid');
|
||||||
success: false,
|
|
||||||
error: 'Internal server error'
|
res.json({
|
||||||
|
success: true,
|
||||||
|
guestId // Возвращаем guestId для фронтенда
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error in logout:', error);
|
||||||
|
res.status(500).json({ success: false, error: 'Server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1100,63 +1192,90 @@ router.get('/email/auth-status/:token', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Маршрут для проверки кода email
|
// Маршрут для проверки кода email
|
||||||
router.post('/email/verify-code', authLimiter, async (req, res) => {
|
router.post('/email/verify-code', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { email, code } = req.body;
|
const { code } = req.body;
|
||||||
|
const pendingEmail = req.session.pendingEmail;
|
||||||
|
|
||||||
if (!email || !code) {
|
logger.info(`[email/verify-code] Verifying code for email: ${pendingEmail}`);
|
||||||
|
logger.info(`[email/verify-code] Guest context: guestId=${req.session.guestId}, previousGuestId=${req.session.previousGuestId}`);
|
||||||
|
|
||||||
|
if (!pendingEmail) {
|
||||||
|
logger.warn('[email/verify-code] No pending email found in session');
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Email и код обязательны'
|
error: 'Нет ожидающей верификации электронной почты'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[email/verify-code] Verifying code for email: ${email}`);
|
if (!code) {
|
||||||
|
logger.warn('[email/verify-code] No verification code provided');
|
||||||
// Сохраняем гостевой ID до проверки кода
|
|
||||||
const guestId = req.session.guestId;
|
|
||||||
const previousGuestId = req.session.previousGuestId;
|
|
||||||
|
|
||||||
logger.info(`[email/verify-code] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`);
|
|
||||||
|
|
||||||
// Проверяем код через сервис верификации
|
|
||||||
const result = await verificationService.verifyCode(code, 'email', email.toLowerCase());
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
logger.warn(`[email/verify-code] Invalid code for email ${email}: ${result.error}`);
|
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: result.error || 'Неверный код подтверждения'
|
error: 'Код подтверждения не указан'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = result.userId;
|
// Проверяем код верификации
|
||||||
logger.info(`[email/verify-code] Code verified successfully for email ${email}, userId: ${userId}`);
|
const verificationResult = await verifyEmailCode(code, pendingEmail);
|
||||||
|
|
||||||
// Явно сохраняем email в таблицу user_identities
|
if (!verificationResult.success) {
|
||||||
await saveUserIdentity(userId, 'email', email.toLowerCase(), true);
|
logger.warn(`[email/verify-code] Invalid verification code for email ${pendingEmail}`);
|
||||||
|
return res.status(400).json({
|
||||||
// Если есть гостевые ID, сохраняем их до установки аутентификации
|
success: false,
|
||||||
if (guestId) {
|
error: verificationResult.error || 'Неверный код подтверждения'
|
||||||
await saveUserIdentity(userId, 'guest', guestId, true);
|
});
|
||||||
logger.info(`[email/verify-code] Saved guest ID ${guestId} for user ${userId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousGuestId && previousGuestId !== guestId) {
|
// Используем существующего пользователя, если он уже аутентифицирован
|
||||||
await saveUserIdentity(userId, 'guest', previousGuestId, true);
|
let userId;
|
||||||
logger.info(`[email/verify-code] Saved previous guest ID ${previousGuestId} for user ${userId}`);
|
if (req.session.authenticated && req.session.userId) {
|
||||||
|
userId = req.session.userId;
|
||||||
|
logger.info(`[email/verify-code] Using existing authenticated user ID ${userId}`);
|
||||||
|
} else {
|
||||||
|
// Проверяем, существует ли пользователь с таким email
|
||||||
|
const existingUser = await db.query(
|
||||||
|
`SELECT u.id FROM users u
|
||||||
|
JOIN user_identities ui ON u.id = ui.user_id
|
||||||
|
WHERE ui.provider = 'email' AND ui.provider_id = $1`,
|
||||||
|
[pendingEmail]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingUser.rows.length > 0) {
|
||||||
|
userId = existingUser.rows[0].id;
|
||||||
|
logger.info(`[email/verify-code] Found existing user with ID ${userId} for email ${pendingEmail}`);
|
||||||
|
} else {
|
||||||
|
// Создаем нового пользователя только если нет существующей аутентификации
|
||||||
|
const newUser = await db.query(
|
||||||
|
'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id'
|
||||||
|
);
|
||||||
|
userId = newUser.rows[0].id;
|
||||||
|
logger.info(`[email/verify-code] Created new user with ID ${userId} for email ${pendingEmail}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Устанавливаем аутентификацию пользователя
|
// Сохраняем email как идентификатор пользователя
|
||||||
|
await saveUserIdentity(userId, 'email', pendingEmail, true);
|
||||||
|
logger.info(`[email/verify-code] Saved email identity ${pendingEmail} for user ${userId}`);
|
||||||
|
|
||||||
|
// Если есть гостевые ID, сохраняем их
|
||||||
|
if (req.session.guestId) {
|
||||||
|
await saveUserIdentity(userId, 'guest', req.session.guestId, true);
|
||||||
|
logger.info(`[email/verify-code] Saved guest ID ${req.session.guestId} for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.session.previousGuestId && req.session.previousGuestId !== req.session.guestId) {
|
||||||
|
await saveUserIdentity(userId, 'guest', req.session.previousGuestId, true);
|
||||||
|
logger.info(`[email/verify-code] Saved previous guest ID ${req.session.previousGuestId} for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем сессию
|
||||||
req.session.authenticated = true;
|
req.session.authenticated = true;
|
||||||
req.session.userId = userId;
|
req.session.userId = userId;
|
||||||
req.session.email = email.toLowerCase();
|
req.session.email = pendingEmail;
|
||||||
req.session.authType = 'email';
|
req.session.authType = 'email';
|
||||||
|
delete req.session.pendingEmail;
|
||||||
// Если был временный ID, удаляем его
|
|
||||||
if (req.session.tempUserId) {
|
|
||||||
delete req.session.tempUserId;
|
delete req.session.tempUserId;
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем сессию перед связыванием сообщений
|
// Сохраняем сессию перед связыванием сообщений
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
@@ -1178,8 +1297,8 @@ router.post('/email/verify-code', authLimiter, async (req, res) => {
|
|||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
userId,
|
userId,
|
||||||
email: email.toLowerCase(),
|
email: pendingEmail,
|
||||||
message: 'Аутентификация успешна'
|
authenticated: true
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1470,10 +1589,10 @@ router.post('/email/send', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обертка для функции processGuestMessages с правильным порядком аргументов
|
// Обертка для функции processGuestMessages
|
||||||
async function processGuestMessagesWrapper(guestId, userId) {
|
async function processGuestMessagesWrapper(userId, guestId) {
|
||||||
try {
|
try {
|
||||||
logger.info(`[processGuestMessagesWrapper] Correcting order of arguments: userId=${userId}, guestId=${guestId}`);
|
logger.info(`[processGuestMessagesWrapper] Processing messages: userId=${userId}, guestId=${guestId}`);
|
||||||
return await processGuestMessages(userId, guestId);
|
return await processGuestMessages(userId, guestId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error);
|
logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error);
|
||||||
@@ -1526,72 +1645,49 @@ async function saveUserIdentity(userId, provider, providerId, verified = true) {
|
|||||||
// Функция для связывания гостевых сообщений после аутентификации
|
// Функция для связывания гостевых сообщений после аутентификации
|
||||||
async function linkGuestMessagesAfterAuth(session, userId) {
|
async function linkGuestMessagesAfterAuth(session, userId) {
|
||||||
try {
|
try {
|
||||||
const guestId = session.guestId;
|
logger.info(`[linkGuestMessagesAfterAuth] Starting for user ${userId} with guestId=${session.guestId}, previousGuestId=${session.previousGuestId}`);
|
||||||
const previousGuestId = session.previousGuestId;
|
|
||||||
|
|
||||||
logger.info(`[linkGuestMessagesAfterAuth] Starting for user ${userId} with guestId=${guestId}, previousGuestId=${previousGuestId}`);
|
// Инициализируем массив обработанных гостевых ID, если его нет
|
||||||
|
|
||||||
// Проверяем, есть ли идентификаторы для обработки
|
|
||||||
if (!guestId && !previousGuestId) {
|
|
||||||
logger.debug('[linkGuestMessagesAfterAuth] No guest IDs to process');
|
|
||||||
return { success: true, message: 'No guest IDs to process' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализируем массив обработанных ID, если он не существует
|
|
||||||
if (!session.processedGuestIds) {
|
if (!session.processedGuestIds) {
|
||||||
session.processedGuestIds = [];
|
session.processedGuestIds = [];
|
||||||
logger.info(`[linkGuestMessagesAfterAuth] Initialized processedGuestIds array for session`);
|
logger.info('[linkGuestMessagesAfterAuth] Initialized processedGuestIds array for session');
|
||||||
}
|
}
|
||||||
|
|
||||||
let results = [];
|
// Получаем все гостевые ID для текущего пользователя
|
||||||
|
const guestIdsResult = await db.query(
|
||||||
|
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2',
|
||||||
|
[userId, 'guest']
|
||||||
|
);
|
||||||
|
const userGuestIds = guestIdsResult.rows.map(row => row.provider_id);
|
||||||
|
|
||||||
// Обрабатываем текущий гостевой ID
|
// Обрабатываем текущий гостевой ID
|
||||||
if (guestId) {
|
if (session.guestId && !session.processedGuestIds.includes(session.guestId)) {
|
||||||
logger.info(`[linkGuestMessagesAfterAuth] Processing current guest ID ${guestId} for user ${userId}`);
|
logger.info(`[linkGuestMessagesAfterAuth] Processing current guest ID ${session.guestId} for user ${userId}`);
|
||||||
|
await processGuestMessagesWrapper(userId, session.guestId);
|
||||||
try {
|
session.processedGuestIds.push(session.guestId);
|
||||||
// Связываем сообщения с пользователем через обертку с правильным порядком аргументов
|
|
||||||
const result = await processGuestMessagesWrapper(guestId, userId);
|
|
||||||
results.push({ guestId, result });
|
|
||||||
|
|
||||||
// Добавляем в список обработанных
|
|
||||||
session.processedGuestIds.push(guestId);
|
|
||||||
|
|
||||||
logger.info(`[linkGuestMessagesAfterAuth] Successfully processed guest ID ${guestId}`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[linkGuestMessagesAfterAuth] Error processing guest ID ${guestId}:`, error);
|
|
||||||
results.push({ guestId, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обрабатываем предыдущий гостевой ID
|
// Обрабатываем предыдущий гостевой ID
|
||||||
if (previousGuestId && previousGuestId !== guestId) {
|
if (session.previousGuestId && !session.processedGuestIds.includes(session.previousGuestId)) {
|
||||||
logger.info(`[linkGuestMessagesAfterAuth] Processing previous guest ID ${previousGuestId} for user ${userId}`);
|
logger.info(`[linkGuestMessagesAfterAuth] Processing previous guest ID ${session.previousGuestId} for user ${userId}`);
|
||||||
|
await processGuestMessagesWrapper(userId, session.previousGuestId);
|
||||||
try {
|
session.processedGuestIds.push(session.previousGuestId);
|
||||||
// Связываем сообщения с пользователем через обертку с правильным порядком аргументов
|
|
||||||
const result = await processGuestMessagesWrapper(previousGuestId, userId);
|
|
||||||
results.push({ guestId: previousGuestId, result });
|
|
||||||
|
|
||||||
// Добавляем в список обработанных
|
|
||||||
session.processedGuestIds.push(previousGuestId);
|
|
||||||
|
|
||||||
logger.info(`[linkGuestMessagesAfterAuth] Successfully processed previous guest ID ${previousGuestId}`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[linkGuestMessagesAfterAuth] Error processing previous guest ID ${previousGuestId}:`, error);
|
|
||||||
results.push({ guestId: previousGuestId, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Очищаем гостевые идентификаторы из сессии
|
// Обрабатываем все гостевые ID пользователя
|
||||||
delete session.guestId;
|
for (const guestId of userGuestIds) {
|
||||||
delete session.previousGuestId;
|
if (!session.processedGuestIds.includes(guestId)) {
|
||||||
|
logger.info(`[linkGuestMessagesAfterAuth] Processing user's guest ID ${guestId} for user ${userId}`);
|
||||||
|
await processGuestMessagesWrapper(userId, guestId);
|
||||||
|
session.processedGuestIds.push(guestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Сохраняем сессию
|
// Сохраняем сессию
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
session.save(err => {
|
session.save(err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error('[linkGuestMessagesAfterAuth] Error saving session after guest ID processing:', err);
|
logger.error('[linkGuestMessagesAfterAuth] Error saving session:', err);
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
logger.info('[linkGuestMessagesAfterAuth] Session saved successfully after guest ID processing');
|
logger.info('[linkGuestMessagesAfterAuth] Session saved successfully after guest ID processing');
|
||||||
@@ -1600,9 +1696,9 @@ async function linkGuestMessagesAfterAuth(session, userId) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true, results };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[linkGuestMessagesAfterAuth] Error: ${error.message}`, error);
|
logger.error('[linkGuestMessagesAfterAuth] Error:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,30 +27,6 @@ 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
|
||||||
@@ -69,23 +45,12 @@ 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 existingMessage = await db.query(
|
|
||||||
'SELECT id FROM messages WHERE guest_message_id = $1',
|
|
||||||
[guestMessage.id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingMessage.rows.length > 0) {
|
|
||||||
console.log(`Guest message ${guestMessage.id} already processed, skipping`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем сообщение пользователя
|
// Сохраняем сообщение пользователя
|
||||||
const userMessageResult = await db.query(
|
const userMessageResult = await db.query(
|
||||||
`INSERT INTO messages
|
`INSERT INTO messages
|
||||||
(conversation_id, content, sender_type, role, channel, guest_message_id, created_at)
|
(conversation_id, content, sender_type, role, channel, created_at)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, $3, $4, $5, $6, $7)
|
($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
conversation.id,
|
conversation.id,
|
||||||
@@ -93,7 +58,6 @@ async function processGuestMessages(userId, guestId) {
|
|||||||
'user',
|
'user',
|
||||||
'user',
|
'user',
|
||||||
'web',
|
'web',
|
||||||
guestMessage.id,
|
|
||||||
guestMessage.created_at
|
guestMessage.created_at
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -128,6 +92,10 @@ async function processGuestMessages(userId, guestId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем обработанные гостевые сообщения
|
||||||
|
await db.query('DELETE FROM guest_messages WHERE guest_id = $1', [guestId]);
|
||||||
|
console.log(`Deleted processed guest messages for guest ID ${guestId}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Processed ${guestMessages.length} guest messages`,
|
message: `Processed ${guestMessages.length} guest messages`,
|
||||||
|
|||||||
@@ -232,11 +232,17 @@ class AuthService {
|
|||||||
// Создание сессии с проверкой роли
|
// Создание сессии с проверкой роли
|
||||||
async createSession(session, { userId, authenticated, authType, guestId, address }) {
|
async createSession(session, { userId, authenticated, authType, guestId, address }) {
|
||||||
try {
|
try {
|
||||||
|
// Если пользователь аутентифицирован, обрабатываем гостевые сообщения
|
||||||
|
if (authenticated && guestId) {
|
||||||
|
await this.processAndCleanupGuestData(userId, guestId, session);
|
||||||
|
}
|
||||||
|
|
||||||
// Обновляем данные сессии
|
// Обновляем данные сессии
|
||||||
session.userId = userId;
|
session.userId = userId;
|
||||||
session.authenticated = authenticated;
|
session.authenticated = authenticated;
|
||||||
session.authType = authType;
|
session.authType = authType;
|
||||||
session.guestId = guestId;
|
|
||||||
|
// Сохраняем адрес кошелька если есть
|
||||||
if (address) {
|
if (address) {
|
||||||
session.address = address;
|
session.address = address;
|
||||||
}
|
}
|
||||||
@@ -250,7 +256,6 @@ class AuthService {
|
|||||||
userId,
|
userId,
|
||||||
authenticated,
|
authenticated,
|
||||||
authType,
|
authType,
|
||||||
guestId,
|
|
||||||
address,
|
address,
|
||||||
cookie: session.cookie
|
cookie: session.cookie
|
||||||
}), session.id]
|
}), session.id]
|
||||||
@@ -263,6 +268,31 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка и очистка гостевых данных после авторизации
|
||||||
|
* @param {number} userId - ID пользователя
|
||||||
|
* @param {string} guestId - Гостевой ID
|
||||||
|
* @param {Object} session - Объект сессии
|
||||||
|
*/
|
||||||
|
async processAndCleanupGuestData(userId, guestId, session) {
|
||||||
|
try {
|
||||||
|
// Обрабатываем гостевые сообщения
|
||||||
|
const { processGuestMessages } = require('../routes/chat');
|
||||||
|
await processGuestMessages(userId, guestId);
|
||||||
|
|
||||||
|
// Очищаем гостевой ID из сессии
|
||||||
|
delete session.guestId;
|
||||||
|
if (session.previousGuestId) {
|
||||||
|
delete session.previousGuestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Cleaned up guest data for user ${userId}, guest ID ${guestId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error processing and cleaning up guest data:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getSession(sessionId) {
|
async getSession(sessionId) {
|
||||||
try {
|
try {
|
||||||
const result = await db.query('SELECT * FROM session WHERE sid = $1', [sessionId]);
|
const result = await db.query('SELECT * FROM session WHERE sid = $1', [sessionId]);
|
||||||
|
|||||||
@@ -70,38 +70,57 @@ class EmailAuth {
|
|||||||
return { verified: false, message: result.error || 'Неверный код верификации' };
|
return { verified: false, message: result.error || 'Неверный код верификации' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = result.userId || session.tempUserId;
|
const email = session.pendingEmail.toLowerCase();
|
||||||
const email = session.pendingEmail;
|
let finalUserId;
|
||||||
|
|
||||||
// Проверяем, существует ли уже этот email в user_identities
|
// Ищем всех пользователей с похожими идентификаторами
|
||||||
const existingUserQuery = await db.query(
|
const identities = {
|
||||||
`SELECT user_id FROM user_identities
|
email: email,
|
||||||
WHERE provider = 'email' AND provider_id = $1`,
|
guest: session.guestId
|
||||||
[email.toLowerCase()]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
let finalUserId = userId;
|
const relatedUsers = await authService.identityService.findRelatedUsers(identities);
|
||||||
|
logger.info(`[checkEmailVerification] Found ${relatedUsers.length} related users for identities:`, identities);
|
||||||
|
|
||||||
// Если email уже связан с другим пользователем
|
if (relatedUsers.length > 0) {
|
||||||
if (existingUserQuery.rows.length > 0) {
|
// Берем первого найденного пользователя как основного
|
||||||
finalUserId = existingUserQuery.rows[0].user_id;
|
finalUserId = relatedUsers[0];
|
||||||
logger.info(`Using existing user ID ${finalUserId} for email ${email}`);
|
logger.info(`[checkEmailVerification] Using existing user ${finalUserId} as primary`);
|
||||||
|
|
||||||
// Обновляем идентификатор пользователя в сессии
|
// Мигрируем данные от остальных пользователей к основному
|
||||||
if (userId !== finalUserId) {
|
for (const userId of relatedUsers.slice(1)) {
|
||||||
logger.info(`Changing user ID from ${userId} to ${finalUserId} based on existing email identity`);
|
await authService.identityService.migrateUserData(userId, finalUserId);
|
||||||
|
logger.info(`[checkEmailVerification] Migrated data from user ${userId} to ${finalUserId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если у нас есть временный пользователь, мигрируем его данные тоже
|
||||||
|
if (session.tempUserId && !relatedUsers.includes(session.tempUserId)) {
|
||||||
|
await authService.identityService.migrateUserData(session.tempUserId, finalUserId);
|
||||||
|
logger.info(`[checkEmailVerification] Migrated temporary user ${session.tempUserId} to ${finalUserId}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Добавляем email в базу данных для нового пользователя
|
// Если связанных пользователей нет, используем временного или создаем нового
|
||||||
await db.query(
|
if (session.tempUserId) {
|
||||||
`INSERT INTO user_identities
|
finalUserId = session.tempUserId;
|
||||||
(user_id, provider, provider_id)
|
logger.info(`[checkEmailVerification] Using temporary user ${finalUserId}`);
|
||||||
VALUES ($1, $2, $3)
|
} else {
|
||||||
ON CONFLICT (provider, provider_id)
|
const newUserResult = await db.query(
|
||||||
DO UPDATE SET user_id = $1`,
|
'INSERT INTO users (role) VALUES ($1) RETURNING id',
|
||||||
[finalUserId, 'email', email.toLowerCase()]
|
['user']
|
||||||
);
|
);
|
||||||
logger.info(`Added new email identity ${email} for user ${finalUserId}`);
|
finalUserId = newUserResult.rows[0].id;
|
||||||
|
logger.info(`[checkEmailVerification] Created new user ${finalUserId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем email в базу данных
|
||||||
|
await authService.identityService.saveIdentity(finalUserId, 'email', email, true);
|
||||||
|
logger.info(`[checkEmailVerification] Added email identity ${email} for user ${finalUserId}`);
|
||||||
|
|
||||||
|
// Если есть гостевой ID, добавляем его тоже
|
||||||
|
if (session.guestId) {
|
||||||
|
await authService.identityService.saveIdentity(finalUserId, 'guest', session.guestId, true);
|
||||||
|
logger.info(`[checkEmailVerification] Added guest identity ${session.guestId} for user ${finalUserId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Очищаем временные данные
|
// Очищаем временные данные
|
||||||
@@ -113,7 +132,7 @@ class EmailAuth {
|
|||||||
return {
|
return {
|
||||||
verified: true,
|
verified: true,
|
||||||
userId: finalUserId,
|
userId: finalUserId,
|
||||||
email: email.toLowerCase()
|
email: email
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error checking email verification:', error);
|
logger.error('Error checking email verification:', error);
|
||||||
|
|||||||
@@ -195,6 +195,110 @@ class IdentityService {
|
|||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Мигрирует все идентификаторы и сообщения от одного пользователя к другому
|
||||||
|
* @param {number} fromUserId - ID исходного пользователя
|
||||||
|
* @param {number} toUserId - ID целевого пользователя
|
||||||
|
* @returns {Promise<object>} - Результат операции
|
||||||
|
*/
|
||||||
|
async migrateUserData(fromUserId, toUserId) {
|
||||||
|
try {
|
||||||
|
if (!fromUserId || !toUserId) {
|
||||||
|
logger.warn(`[IdentityService] Missing parameters: fromUserId=${fromUserId}, toUserId=${toUserId}`);
|
||||||
|
return { success: false, error: 'Missing required parameters' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Начинаем транзакцию
|
||||||
|
const client = await db.pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
|
||||||
|
// Получаем все идентификаторы исходного пользователя
|
||||||
|
const identitiesResult = await client.query(
|
||||||
|
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
|
||||||
|
[fromUserId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Переносим каждый идентификатор
|
||||||
|
for (const identity of identitiesResult.rows) {
|
||||||
|
await client.query(
|
||||||
|
`UPDATE user_identities
|
||||||
|
SET user_id = $1
|
||||||
|
WHERE user_id = $2 AND provider = $3 AND provider_id = $4`,
|
||||||
|
[toUserId, fromUserId, identity.provider, identity.provider_id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Переносим все сообщения
|
||||||
|
await client.query(
|
||||||
|
`UPDATE messages
|
||||||
|
SET user_id = $1
|
||||||
|
WHERE user_id = $2`,
|
||||||
|
[toUserId, fromUserId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Переносим все диалоги
|
||||||
|
await client.query(
|
||||||
|
`UPDATE conversations
|
||||||
|
SET user_id = $1
|
||||||
|
WHERE user_id = $2`,
|
||||||
|
[toUserId, fromUserId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Удаляем исходного пользователя
|
||||||
|
await client.query(
|
||||||
|
`DELETE FROM users WHERE id = $1`,
|
||||||
|
[fromUserId]
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.query('COMMIT');
|
||||||
|
|
||||||
|
logger.info(`[IdentityService] Successfully migrated data from user ${fromUserId} to user ${toUserId}`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
migratedIdentities: identitiesResult.rows.length
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[IdentityService] Error migrating data from user ${fromUserId} to user ${toUserId}:`, error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Находит всех пользователей с похожими идентификаторами
|
||||||
|
* @param {object} identities - Объект с идентификаторами
|
||||||
|
* @returns {Promise<Array>} - Массив ID пользователей
|
||||||
|
*/
|
||||||
|
async findRelatedUsers(identities) {
|
||||||
|
try {
|
||||||
|
const userIds = new Set();
|
||||||
|
|
||||||
|
for (const [provider, providerId] of Object.entries(identities)) {
|
||||||
|
if (!providerId) continue;
|
||||||
|
|
||||||
|
const result = await db.query(
|
||||||
|
`SELECT DISTINCT user_id
|
||||||
|
FROM user_identities
|
||||||
|
WHERE provider = $1 AND provider_id = $2`,
|
||||||
|
[provider, providerId]
|
||||||
|
);
|
||||||
|
|
||||||
|
result.rows.forEach(row => userIds.add(row.user_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(userIds);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[IdentityService] Error finding related users:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new IdentityService();
|
module.exports = new IdentityService();
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="email-connection">
|
<div class="email-connection">
|
||||||
<div v-if="!showVerification">
|
<div v-if="!showVerification" class="email-form">
|
||||||
<input
|
<input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
@@ -12,10 +12,11 @@
|
|||||||
:disabled="isLoading || !isValidEmail"
|
:disabled="isLoading || !isValidEmail"
|
||||||
class="email-btn"
|
class="email-btn"
|
||||||
>
|
>
|
||||||
Получить код
|
{{ isLoading ? 'Отправка...' : 'Получить код' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else class="verification-form">
|
||||||
|
<p class="verification-info">Код отправлен на {{ email }}</p>
|
||||||
<input
|
<input
|
||||||
v-model="code"
|
v-model="code"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -24,10 +25,16 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@click="verifyCode"
|
@click="verifyCode"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading || !code"
|
||||||
class="verify-btn"
|
class="verify-btn"
|
||||||
>
|
>
|
||||||
Подтвердить
|
{{ isLoading ? 'Проверка...' : 'Подтвердить' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="resetForm"
|
||||||
|
class="reset-btn"
|
||||||
|
>
|
||||||
|
Изменить email
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
@@ -36,21 +43,17 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from '@/api/axios';
|
||||||
|
import { useAuth } from '@/composables/useAuth';
|
||||||
|
|
||||||
const props = defineProps({
|
const emit = defineEmits(['close']);
|
||||||
onEmailAuth: {
|
const { linkIdentity } = useAuth();
|
||||||
type: Function,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const code = ref('');
|
const code = ref('');
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const showVerification = ref(false);
|
const showVerification = ref(false);
|
||||||
const isConnecting = ref(false);
|
|
||||||
|
|
||||||
const isValidEmail = computed(() => {
|
const isValidEmail = computed(() => {
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
|
||||||
@@ -59,11 +62,19 @@ const isValidEmail = computed(() => {
|
|||||||
const requestCode = async () => {
|
const requestCode = async () => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
await props.onEmailAuth(email.value);
|
|
||||||
showVerification.value = true;
|
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
|
||||||
|
const response = await axios.post('/api/auth/email/request-verification', {
|
||||||
|
email: email.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
showVerification.value = true;
|
||||||
|
} else {
|
||||||
|
error.value = response.data.error || 'Ошибка отправки кода';
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message || 'Ошибка отправки кода';
|
error.value = err.response?.data?.error || 'Ошибка отправки кода';
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -72,37 +83,80 @@ const requestCode = async () => {
|
|||||||
const verifyCode = async () => {
|
const verifyCode = async () => {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
await props.onEmailAuth(email.value, code.value);
|
|
||||||
error.value = '';
|
error.value = '';
|
||||||
|
|
||||||
|
const response = await axios.post('/api/auth/email/verify', {
|
||||||
|
email: email.value,
|
||||||
|
code: code.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
// Связываем email с текущим пользователем
|
||||||
|
await linkIdentity('email', email.value);
|
||||||
|
emit('close');
|
||||||
|
} else {
|
||||||
|
error.value = response.data.error || 'Неверный код';
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err.message || 'Неверный код';
|
error.value = err.response?.data?.error || 'Ошибка проверки кода';
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
email.value = '';
|
||||||
|
code.value = '';
|
||||||
|
error.value = '';
|
||||||
|
showVerification.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.email-connection {
|
.email-connection {
|
||||||
margin: 10px 0;
|
padding: 20px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-form,
|
||||||
|
.verification-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-input,
|
.email-input,
|
||||||
.code-input {
|
.code-input {
|
||||||
padding: 8px;
|
padding: 8px 12px;
|
||||||
margin-right: 10px;
|
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-btn,
|
||||||
|
.verify-btn,
|
||||||
|
.reset-btn {
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-btn,
|
.email-btn,
|
||||||
.verify-btn {
|
.verify-btn {
|
||||||
padding: 10px 20px;
|
|
||||||
background-color: #48bb78;
|
background-color: #48bb78;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
}
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
.reset-btn {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-info {
|
||||||
|
color: #4a5568;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +1,135 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="telegram-connect">
|
<div class="telegram-connect">
|
||||||
<p>Подключите свой аккаунт Telegram для быстрой авторизации.</p>
|
<div v-if="!showQR" class="intro">
|
||||||
<button @click="connectTelegram" class="connect-button">
|
<p>Подключите свой аккаунт Telegram для быстрой авторизации</p>
|
||||||
<span class="telegram-icon">📱</span> Подключить Telegram
|
<button @click="startConnection" class="connect-button" :disabled="loading">
|
||||||
|
<span class="telegram-icon">📱</span>
|
||||||
|
{{ loading ? 'Загрузка...' : 'Подключить Telegram' }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="qr-section">
|
||||||
|
<p>Отсканируйте QR-код в приложении Telegram</p>
|
||||||
|
<div class="qr-container" v-html="qrCode"></div>
|
||||||
|
<p class="or-divider">или</p>
|
||||||
|
<a :href="botLink" target="_blank" class="bot-link">
|
||||||
|
Открыть бота в Telegram
|
||||||
|
</a>
|
||||||
|
<button @click="resetConnection" class="reset-button">
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="loading">Загрузка...</div>
|
|
||||||
<div v-if="error" class="error">{{ error }}</div>
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
<div v-if="success" class="success">{{ success }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from '@/api/axios';
|
||||||
|
import { useAuth } from '@/composables/useAuth';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
const { linkIdentity } = useAuth();
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const success = ref('');
|
const showQR = ref(false);
|
||||||
const isConnecting = ref(false);
|
const qrCode = ref('');
|
||||||
|
const botLink = ref('');
|
||||||
|
const pollInterval = ref(null);
|
||||||
|
const connectionToken = ref('');
|
||||||
|
|
||||||
async function connectTelegram() {
|
const startConnection = async () => {
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
success.value = '';
|
|
||||||
|
|
||||||
// Запрос на получение ссылки для авторизации через Telegram
|
const response = await axios.post('/api/auth/telegram/start-connection');
|
||||||
const response = await axios.get('/api/auth/telegram', {
|
|
||||||
withCredentials: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.error) {
|
if (response.data.success) {
|
||||||
error.value = `Ошибка при подключении Telegram: ${response.data.error}`;
|
connectionToken.value = response.data.token;
|
||||||
return;
|
botLink.value = `https://t.me/${response.data.botUsername}?start=${connectionToken.value}`;
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data.authUrl) {
|
// Генерируем QR-код
|
||||||
success.value = 'Перейдите по ссылке для авторизации через Telegram';
|
const qr = await QRCode.toDataURL(botLink.value);
|
||||||
window.open(response.data.authUrl, '_blank');
|
qrCode.value = `<img src="${qr}" alt="Telegram QR Code" />`;
|
||||||
|
|
||||||
|
showQR.value = true;
|
||||||
|
startPolling();
|
||||||
} else {
|
} else {
|
||||||
error.value = 'Не удалось получить ссылку для авторизации';
|
error.value = response.data.error || 'Не удалось начать процесс подключения';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error connecting Telegram:', err);
|
error.value = err.response?.data?.error || 'Ошибка при подключении Telegram';
|
||||||
error.value = 'Ошибка при подключении Telegram';
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkConnection = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/auth/telegram/check-connection', {
|
||||||
|
token: connectionToken.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success && response.data.telegramId) {
|
||||||
|
// Связываем Telegram с текущим пользователем
|
||||||
|
await linkIdentity('telegram', response.data.telegramId);
|
||||||
|
stopPolling();
|
||||||
|
emit('close');
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking connection:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPolling = () => {
|
||||||
|
pollInterval.value = setInterval(checkConnection, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (pollInterval.value) {
|
||||||
|
clearInterval(pollInterval.value);
|
||||||
|
pollInterval.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetConnection = () => {
|
||||||
|
stopPolling();
|
||||||
|
showQR.value = false;
|
||||||
|
error.value = '';
|
||||||
|
qrCode.value = '';
|
||||||
|
botLink.value = '';
|
||||||
|
connectionToken.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopPolling();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.telegram-connect {
|
.telegram-connect {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro,
|
||||||
|
.qr-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-button {
|
.connect-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px 15px;
|
padding: 10px 20px;
|
||||||
background-color: #0088cc;
|
background-color: #0088cc;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -72,7 +139,7 @@ async function connectTelegram() {
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-button:hover {
|
.connect-button:hover:not(:disabled) {
|
||||||
background-color: #0077b5;
|
background-color: #0077b5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,23 +148,60 @@ async function connectTelegram() {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading, .error, .success {
|
.qr-container {
|
||||||
|
background: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.qr-container img {
|
||||||
background-color: #f8f9fa;
|
max-width: 200px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.or-divider {
|
||||||
|
color: #666;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-link {
|
||||||
|
color: #0088cc;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #0088cc;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-link:hover {
|
||||||
|
background-color: #0088cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
color: #4a5568;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-button:hover {
|
||||||
|
background-color: #cbd5e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background-color: #f8d7da;
|
color: #e53e3e;
|
||||||
color: #721c24;
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
button:disabled {
|
||||||
background-color: #d4edda;
|
opacity: 0.7;
|
||||||
color: #155724;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wallet-connection">
|
<div class="wallet-connection">
|
||||||
|
<div v-if="!isConnected" class="connect-section">
|
||||||
|
<p>Подключите свой кошелек для доступа к расширенным функциям</p>
|
||||||
<button
|
<button
|
||||||
@click="connectWallet"
|
@click="connectWallet"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
class="wallet-btn"
|
class="wallet-btn"
|
||||||
>
|
>
|
||||||
{{ isConnected ? 'Подключено' : 'Подключить кошелек' }}
|
<span class="wallet-icon">💳</span>
|
||||||
|
{{ isLoading ? 'Подключение...' : 'Подключить кошелек' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="status-section">
|
||||||
|
<p>Кошелек подключен</p>
|
||||||
|
<p class="address">{{ formatAddress(address) }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="error" class="error">{{ error }}</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { connectWithWallet } from '../../services/wallet';
|
import { useAuth } from '@/composables/useAuth';
|
||||||
import { ethers } from 'ethers';
|
import { connectWithWallet } from '@/services/wallet';
|
||||||
import { SiweMessage } from 'siwe';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
// Определяем props
|
const emit = defineEmits(['close']);
|
||||||
const props = defineProps({
|
const { linkIdentity } = useAuth();
|
||||||
isAuthenticated: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Определяем состояние
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const auth = inject('auth');
|
const error = ref('');
|
||||||
const isConnecting = ref(false);
|
|
||||||
const address = ref('');
|
const address = ref('');
|
||||||
|
|
||||||
// Вычисляемое свойство для статуса подключения
|
const isConnected = computed(() => !!address.value);
|
||||||
const isConnected = computed(() => auth.isAuthenticated.value);
|
|
||||||
|
|
||||||
const emit = defineEmits(['connect']);
|
const formatAddress = (addr) => {
|
||||||
|
if (!addr) return '';
|
||||||
|
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
||||||
|
};
|
||||||
|
|
||||||
// Метод подключения кошелька
|
|
||||||
const connectWallet = async () => {
|
const connectWallet = async () => {
|
||||||
if (isLoading.value) return;
|
if (isLoading.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
error.value = '';
|
||||||
|
|
||||||
|
// Подключаем кошелек
|
||||||
const result = await connectWithWallet();
|
const result = await connectWithWallet();
|
||||||
await auth.checkAuth();
|
|
||||||
console.log('Wallet connected, auth state:', auth.isAuthenticated.value);
|
if (result.success) {
|
||||||
emit('connect', result);
|
address.value = result.address;
|
||||||
} catch (error) {
|
|
||||||
console.error('Error connecting wallet:', error);
|
// Связываем кошелек с текущим пользователем
|
||||||
|
await linkIdentity('wallet', result.address);
|
||||||
|
emit('close');
|
||||||
|
} else {
|
||||||
|
error.value = result.error || 'Не удалось подключить кошелек';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error connecting wallet:', err);
|
||||||
|
error.value = err.message || 'Произошла ошибка при подключении кошелька';
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -56,25 +68,57 @@ const connectWallet = async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.wallet-connection {
|
.wallet-connection {
|
||||||
margin: 10px 0;
|
padding: 20px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connect-section,
|
||||||
|
.status-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-btn {
|
.wallet-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
background-color: #4a5568;
|
background-color: #4a5568;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 16px;
|
||||||
transition: background-color 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-btn:hover:not(:disabled) {
|
.wallet-btn:hover:not(:disabled) {
|
||||||
background-color: #2d3748;
|
background-color: #2d3748;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-btn:disabled {
|
.wallet-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address {
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #e53e3e;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, onUnmounted } from 'vue';
|
||||||
import axios from '../api/axios';
|
import axios from '../api/axios';
|
||||||
|
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
@@ -20,7 +20,19 @@ export function useAuth() {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/auth/identities');
|
const response = await axios.get('/api/auth/identities');
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
identities.value = response.data.identities;
|
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
|
||||||
|
const filteredIdentities = response.data.identities
|
||||||
|
.filter(identity => identity.provider !== 'guest')
|
||||||
|
.reduce((acc, identity) => {
|
||||||
|
// Для каждого типа провайдера оставляем только один идентификатор
|
||||||
|
const existingIdentity = acc.find(i => i.provider === identity.provider);
|
||||||
|
if (!existingIdentity) {
|
||||||
|
acc.push(identity);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
identities.value = filteredIdentities;
|
||||||
console.log('User identities updated:', identities.value);
|
console.log('User identities updated:', identities.value);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -28,6 +40,21 @@ export function useAuth() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Периодическое обновление идентификаторов
|
||||||
|
let identitiesInterval;
|
||||||
|
|
||||||
|
const startIdentitiesPolling = () => {
|
||||||
|
if (identitiesInterval) return;
|
||||||
|
identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopIdentitiesPolling = () => {
|
||||||
|
if (identitiesInterval) {
|
||||||
|
clearInterval(identitiesInterval);
|
||||||
|
identitiesInterval = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const checkTokenBalances = async (address) => {
|
const checkTokenBalances = async (address) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/auth/check-tokens/${address}`);
|
const response = await axios.get(`/api/auth/check-tokens/${address}`);
|
||||||
@@ -81,6 +108,15 @@ export function useAuth() {
|
|||||||
await checkTokenBalances(newAddress);
|
await checkTokenBalances(newAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем идентификаторы при любом изменении аутентификации
|
||||||
|
if (authenticated) {
|
||||||
|
await updateIdentities();
|
||||||
|
startIdentitiesPolling();
|
||||||
|
} else {
|
||||||
|
stopIdentitiesPolling();
|
||||||
|
identities.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Auth updated:', {
|
console.log('Auth updated:', {
|
||||||
authenticated: isAuthenticated.value,
|
authenticated: isAuthenticated.value,
|
||||||
userId: userId.value,
|
userId: userId.value,
|
||||||
@@ -91,11 +127,10 @@ export function useAuth() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
// Если пользователь только что аутентифицировался или сменил аккаунт,
|
||||||
// пробуем связать сообщения и обновить идентификаторы
|
// пробуем связать сообщения
|
||||||
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
|
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
|
||||||
console.log('Auth change detected, linking messages and updating identities');
|
console.log('Auth change detected, linking messages');
|
||||||
linkMessages();
|
linkMessages();
|
||||||
updateIdentities();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -344,6 +379,11 @@ export function useAuth() {
|
|||||||
await checkAuth();
|
await checkAuth();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Очищаем интервал при размонтировании компонента
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopIdentitiesPolling();
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
authType,
|
authType,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Добавляем дополнительные кнопки авторизации -->
|
<!-- Добавляем дополнительные кнопки авторизации -->
|
||||||
<div v-if="!isAuthenticated && messages.length > 0" class="auth-buttons">
|
<div v-if="!isAuthenticated" class="auth-buttons">
|
||||||
<h3>Авторизация через:</h3>
|
<h3>Авторизация через:</h3>
|
||||||
<div v-if="!showTelegramVerification" class="auth-btn-container">
|
<div v-if="!showTelegramVerification" class="auth-btn-container">
|
||||||
<button @click="handleTelegramAuth" class="auth-btn telegram-btn">
|
<button @click="handleTelegramAuth" class="auth-btn telegram-btn">
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Форма для Email верификации (встроена в auth-buttons) -->
|
<!-- Форма для Email верификации -->
|
||||||
<div v-if="showEmailForm" class="email-form">
|
<div v-if="showEmailForm" class="email-form">
|
||||||
<p>Введите ваш email для получения кода подтверждения:</p>
|
<p>Введите ваш email для получения кода подтверждения:</p>
|
||||||
<div class="email-form-container">
|
<div class="email-form-container">
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Форма для ввода кода верификации Email (встроена в auth-buttons) -->
|
<!-- Форма для ввода кода верификации Email -->
|
||||||
<div v-if="showEmailVerificationInput" class="email-verification-form">
|
<div v-if="showEmailVerificationInput" class="email-verification-form">
|
||||||
<p>На ваш email <strong>{{ emailVerificationEmail }}</strong> отправлен код подтверждения.</p>
|
<p>На ваш email <strong>{{ emailVerificationEmail }}</strong> отправлен код подтверждения.</p>
|
||||||
<div class="email-form-container">
|
<div class="email-form-container">
|
||||||
@@ -177,29 +177,35 @@
|
|||||||
<h3>Идентификаторы:</h3>
|
<h3>Идентификаторы:</h3>
|
||||||
<div class="user-info-item">
|
<div class="user-info-item">
|
||||||
<span class="user-info-label">Кошелек:</span>
|
<span class="user-info-label">Кошелек:</span>
|
||||||
<span v-if="auth.address?.value" class="user-info-value">{{ truncateAddress(auth.address.value) }}</span>
|
<span v-if="hasIdentityType('wallet')" class="user-info-value">
|
||||||
|
{{ truncateAddress(getIdentityValue('wallet')) }}
|
||||||
|
</span>
|
||||||
<button v-else @click="handleWalletAuth" class="connect-btn">
|
<button v-else @click="handleWalletAuth" class="connect-btn">
|
||||||
Подключить кошелек
|
Подключить кошелек
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info-item">
|
<div class="user-info-item">
|
||||||
<span class="user-info-label">Telegram:</span>
|
<span class="user-info-label">Telegram:</span>
|
||||||
<span v-if="auth.telegramId?.value" class="user-info-value">{{ auth.telegramId.value }}</span>
|
<span v-if="hasIdentityType('telegram')" class="user-info-value">
|
||||||
|
{{ getIdentityValue('telegram') }}
|
||||||
|
</span>
|
||||||
<button v-else @click="handleTelegramAuth" class="connect-btn">
|
<button v-else @click="handleTelegramAuth" class="connect-btn">
|
||||||
Подключить Telegram
|
Подключить Telegram
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-info-item">
|
<div class="user-info-item">
|
||||||
<span class="user-info-label">Email:</span>
|
<span class="user-info-label">Email:</span>
|
||||||
<span v-if="auth.email?.value" class="user-info-value">{{ auth.email.value }}</span>
|
<span v-if="hasIdentityType('email')" class="user-info-value">
|
||||||
|
{{ getIdentityValue('email') }}
|
||||||
|
</span>
|
||||||
<button v-else @click="handleEmailAuth" class="connect-btn">
|
<button v-else @click="handleEmailAuth" class="connect-btn">
|
||||||
Подключить Email
|
Подключить Email
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Блок форм подключения -->
|
<!-- Блок форм подключения для аутентифицированных пользователей -->
|
||||||
<div v-if="showEmailForm || showTelegramVerification || showEmailVerificationInput" class="connect-forms">
|
<div v-if="isAuthenticated && (showEmailForm || showTelegramVerification || showEmailVerificationInput)" class="connect-forms">
|
||||||
<!-- Форма для Email верификации -->
|
<!-- Форма для Email верификации -->
|
||||||
<div v-if="showEmailForm" class="email-form">
|
<div v-if="showEmailForm" class="email-form">
|
||||||
<p>Введите ваш email для получения кода подтверждения:</p>
|
<p>Введите ваш email для получения кода подтверждения:</p>
|
||||||
@@ -249,16 +255,6 @@
|
|||||||
<a :href="telegramBotLink" target="_blank" class="bot-link">Открыть бота Telegram</a>
|
<a :href="telegramBotLink" target="_blank" class="bot-link">Открыть бота Telegram</a>
|
||||||
<button @click="cancelTelegramAuth" class="cancel-btn">Отмена</button>
|
<button @click="cancelTelegramAuth" class="cancel-btn">Отмена</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Сообщения об ошибках -->
|
|
||||||
<div v-if="telegramError" class="error-message">
|
|
||||||
{{ telegramError }}
|
|
||||||
<button class="close-error" @click="telegramError = ''">×</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="emailError" class="error-message">
|
|
||||||
{{ emailError }}
|
|
||||||
<button class="close-error" @click="clearEmailError">×</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Блок баланса токенов -->
|
<!-- Блок баланса токенов -->
|
||||||
@@ -542,6 +538,9 @@ const sendEmailVerification = async () => {
|
|||||||
console.log('Showing verification code input form for email:', emailVerificationEmail.value);
|
console.log('Showing verification code input form for email:', emailVerificationEmail.value);
|
||||||
} else {
|
} else {
|
||||||
emailError.value = response.data.error || 'Ошибка инициализации аутентификации по email';
|
emailError.value = response.data.error || 'Ошибка инициализации аутентификации по email';
|
||||||
|
// Возвращаем форму ввода email в исходное состояние
|
||||||
|
showEmailForm.value = true;
|
||||||
|
showEmailVerificationInput.value = false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in email init request:', error);
|
console.error('Error in email init request:', error);
|
||||||
@@ -550,6 +549,9 @@ const sendEmailVerification = async () => {
|
|||||||
} else {
|
} else {
|
||||||
emailError.value = 'Ошибка при запросе кода подтверждения';
|
emailError.value = 'Ошибка при запросе кода подтверждения';
|
||||||
}
|
}
|
||||||
|
// Возвращаем форму ввода email в исходное состояние
|
||||||
|
showEmailForm.value = true;
|
||||||
|
showEmailVerificationInput.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
isEmailSending.value = false;
|
isEmailSending.value = false;
|
||||||
}
|
}
|
||||||
@@ -1401,6 +1403,18 @@ const cancelEmailAuth = () => {
|
|||||||
emailFormatError.value = false;
|
emailFormatError.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Методы для работы с идентификаторами
|
||||||
|
const hasIdentityType = (type) => {
|
||||||
|
if (!auth.identities?.value) return false;
|
||||||
|
return auth.identities.value.some(identity => identity.provider === type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIdentityValue = (type) => {
|
||||||
|
if (!auth.identities?.value) return null;
|
||||||
|
const identity = auth.identities.value.find(identity => identity.provider === type);
|
||||||
|
return identity ? identity.provider_id : null;
|
||||||
|
};
|
||||||
|
|
||||||
// Функции жизненного цикла
|
// Функции жизненного цикла
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log('HomeView.vue: компонент загружен');
|
console.log('HomeView.vue: компонент загружен');
|
||||||
|
|||||||
Reference in New Issue
Block a user