From f371521511d0ef4daf18f3affe73417bbec85608 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Apr 2025 16:39:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=B0=D1=88=D0=B5=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/015_users_table_refactor.sql | 56 ++++ .../016_fix_duplicate_identities.sql | 93 ++++++ .../db/migrations/fix_duplicates_manual.sql | 40 +++ backend/package.json | 3 +- backend/routes/auth.js | 48 +++- backend/routes/identities.js | 34 ++- backend/routes/users.js | 72 +++++ backend/scripts/fix-duplicate-identities.js | 268 ++++++++++++++++++ backend/services/auth-service.js | 63 ++-- backend/services/identity-service.js | 70 +++-- backend/services/telegramBot.js | 106 +++++-- 11 files changed, 777 insertions(+), 76 deletions(-) create mode 100644 backend/db/migrations/015_users_table_refactor.sql create mode 100644 backend/db/migrations/016_fix_duplicate_identities.sql create mode 100644 backend/db/migrations/fix_duplicates_manual.sql create mode 100644 backend/scripts/fix-duplicate-identities.js diff --git a/backend/db/migrations/015_users_table_refactor.sql b/backend/db/migrations/015_users_table_refactor.sql new file mode 100644 index 0000000..372e552 --- /dev/null +++ b/backend/db/migrations/015_users_table_refactor.sql @@ -0,0 +1,56 @@ +-- Миграция для изменения структуры таблицы users +-- Переносим данные из email и address в user_identities, затем преобразуем эти поля в first_name и last_name + +-- Сначала проверяем, что все email и address уже существуют в user_identities +DO $$ +BEGIN + -- Переносим email в user_identities, если еще не перенесены + INSERT INTO user_identities (user_id, provider, provider_id) + SELECT id, 'email', email + FROM users + WHERE email IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM user_identities + WHERE user_id = users.id AND provider = 'email' AND provider_id = users.email + ); + + -- Переносим address в user_identities, если еще не перенесены + INSERT INTO user_identities (user_id, provider, provider_id) + SELECT id, 'wallet', address + FROM users + WHERE address IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM user_identities + WHERE user_id = users.id AND provider = 'wallet' AND provider_id = users.address + ); + + -- Логируем результаты миграции + RAISE NOTICE 'Данные из колонок email и address перенесены в таблицу user_identities'; +END $$; + +-- Теперь изменяем структуру таблицы users +ALTER TABLE users + DROP CONSTRAINT IF EXISTS users_email_key, + DROP CONSTRAINT IF EXISTS users_address_key; + +-- Добавляем временные колонки +ALTER TABLE users + ADD COLUMN first_name VARCHAR(255), + ADD COLUMN last_name VARCHAR(255); + +-- Убираем уникальность и переименовываем колонки email и address +ALTER TABLE users + ALTER COLUMN email DROP NOT NULL, + ALTER COLUMN address DROP NOT NULL; + +-- Удаляем колонки email и address +ALTER TABLE users + DROP COLUMN email, + DROP COLUMN address; + +-- Добавляем комментарии к столбцам +COMMENT ON COLUMN users.first_name IS 'Имя пользователя'; +COMMENT ON COLUMN users.last_name IS 'Фамилия пользователя'; + +-- Обновляем статистику таблицы +ANALYZE users; \ No newline at end of file diff --git a/backend/db/migrations/016_fix_duplicate_identities.sql b/backend/db/migrations/016_fix_duplicate_identities.sql new file mode 100644 index 0000000..cc09f20 --- /dev/null +++ b/backend/db/migrations/016_fix_duplicate_identities.sql @@ -0,0 +1,93 @@ +-- Миграция для исправления дублирующихся записей в user_identities из-за разного регистра букв +-- Исправляем записи для провайдеров wallet и email + +-- Сначала удаляем существующее ограничение уникальности +ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS user_identities_provider_provider_id_key; + +-- Создаем временную таблицу для хранения идентификаторов, которые нужно обработать +CREATE TEMP TABLE duplicate_identities AS +SELECT + provider, + LOWER(provider_id) as normalized_provider_id, + array_agg(id) as id_list, + array_agg(user_id) as user_id_list +FROM user_identities +WHERE provider IN ('wallet', 'email') +GROUP BY provider, LOWER(provider_id) +HAVING COUNT(*) > 1; + +-- Логируем количество найденных дубликатов +DO $$ +DECLARE + duplicate_count INTEGER; +BEGIN + SELECT COUNT(*) INTO duplicate_count FROM duplicate_identities; + RAISE NOTICE 'Найдено % групп дублирующихся идентификаторов', duplicate_count; +END $$; + +-- Обновляем все записи, приводя provider_id к нижнему регистру +UPDATE user_identities +SET provider_id = LOWER(provider_id) +WHERE provider IN ('wallet', 'email'); + +-- Удаляем дублирующиеся записи, оставляя только одну для каждой комбинации (provider, provider_id) +WITH + duplicates AS ( + SELECT + id, + provider, + provider_id, + ROW_NUMBER() OVER ( + PARTITION BY provider, provider_id + ORDER BY id + ) as row_num + FROM user_identities + WHERE provider IN ('wallet', 'email') + ) +DELETE FROM user_identities +WHERE id IN ( + SELECT id FROM duplicates WHERE row_num > 1 +); + +-- Удаляем дублирующиеся записи для одного пользователя +WITH + user_duplicates AS ( + SELECT + id, + user_id, + provider, + provider_id, + ROW_NUMBER() OVER ( + PARTITION BY user_id, provider, provider_id + ORDER BY id + ) as row_num + FROM user_identities + WHERE provider IN ('wallet', 'email') + ) +DELETE FROM user_identities +WHERE id IN ( + SELECT id FROM user_duplicates WHERE row_num > 1 +); + +-- Добавляем обратно ограничение уникальности +ALTER TABLE user_identities + ADD CONSTRAINT user_identities_provider_provider_id_key + UNIQUE (provider, provider_id); + +-- Добавляем уникальный индекс для пользователей +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_indexes + WHERE tablename = 'user_identities' AND indexname = 'unique_idx_user_identities_user_provider_provider_id' + ) THEN + CREATE UNIQUE INDEX unique_idx_user_identities_user_provider_provider_id + ON user_identities(user_id, provider, provider_id); + END IF; +END $$; + +-- Логируем завершение миграции +DO $$ +BEGIN + RAISE NOTICE 'Миграция для исправления дублирующихся идентификаторов завершена'; +END $$; \ No newline at end of file diff --git a/backend/db/migrations/fix_duplicates_manual.sql b/backend/db/migrations/fix_duplicates_manual.sql new file mode 100644 index 0000000..bcc4cbc --- /dev/null +++ b/backend/db/migrations/fix_duplicates_manual.sql @@ -0,0 +1,40 @@ +-- Скрипт для ручного исправления дублирующихся записей в базе данных + +-- 1. Удаляем существующее ограничение уникальности +ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS user_identities_provider_provider_id_key; + +-- 2. Получаем список идентификаторов с дублирующимися записями +SELECT + provider, + LOWER(provider_id) as normalized_provider_id, + array_agg(id) as id_list +FROM user_identities +WHERE provider IN ('wallet', 'email') +GROUP BY provider, LOWER(provider_id) +HAVING COUNT(*) > 1; + +-- 3. Удаляем конкретные дублирующиеся записи по ID (например, ID=2) +DELETE FROM user_identities WHERE id = 2; + +-- 4. Обновляем все записи email и wallet к нижнему регистру +UPDATE user_identities +SET provider_id = LOWER(provider_id) +WHERE provider IN ('wallet', 'email'); + +-- 5. Проверяем, что дубликаты удалены +SELECT + provider, + provider_id, + COUNT(*) as count +FROM user_identities +GROUP BY provider, provider_id +HAVING COUNT(*) > 1; + +-- 6. Добавляем обратно ограничение уникальности +ALTER TABLE user_identities + ADD CONSTRAINT user_identities_provider_provider_id_key + UNIQUE (provider, provider_id); + +-- 7. Создаем дополнительный индекс для (user_id, provider, provider_id) +CREATE UNIQUE INDEX IF NOT EXISTS unique_idx_user_identities_user_provider_provider_id +ON user_identities(user_id, provider, provider_id); \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index b68a3d3..542bd5b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,7 +19,8 @@ "lint:fix": "eslint . --fix", "format": "prettier --write \"**/*.{js,vue,json,md}\"", "format:check": "prettier --check \"**/*.{js,vue,json,md}\"", - "run-migrations": "node scripts/run-migrations.js" + "run-migrations": "node scripts/run-migrations.js", + "fix-duplicates": "node scripts/fix-duplicate-identities.js" }, "dependencies": { "@langchain/community": "^0.3.34", diff --git a/backend/routes/auth.js b/backend/routes/auth.js index cd817c2..d22b7ba 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -82,18 +82,49 @@ router.post('/verify', async (req, res) => { return res.status(401).json({ success: false, error: 'Invalid signature' }); } + // Нормализуем адрес для использования в запросах + const normalizedAddress = ethers.getAddress(address).toLowerCase(); + // Проверяем nonce - const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [address.toLowerCase()]); + const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [normalizedAddress]); if (nonceResult.rows.length === 0 || nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]) { return res.status(401).json({ success: false, error: 'Invalid nonce' }); } - // Находим или создаем пользователя - const { userId, isAdmin } = await authService.findOrCreateUser(address.toLowerCase()); + let userId; + let isAdmin = false; - // Сохраняем идентификаторы - await identityService.saveIdentity(userId, 'wallet', address.toLowerCase(), true); + // Проверяем, авторизован ли пользователь уже + if (req.session.authenticated && req.session.userId) { + // Если пользователь уже авторизован, привязываем кошелек к существующему пользователю + userId = req.session.userId; + logger.info(`[verify] Using existing authenticated user ${userId} for wallet ${normalizedAddress}`); + + // Связываем кошелек с пользователем через identity-service для предотвращения дубликатов + const linkResult = await authService.linkIdentity( + userId, + 'wallet', + address + ); + + if (!linkResult.success && linkResult.error) { + return res.status(400).json({ + success: false, + error: linkResult.error + }); + } + + // Если linkResult.message содержит 'already exists', значит кошелек уже привязан + logger.info(`[verify] Wallet ${normalizedAddress} linked to user ${userId}: ${linkResult.message || 'success'}`); + } else { + // Находим или создаем пользователя, если не авторизован + const result = await authService.findOrCreateUser(address); + userId = result.userId; + isAdmin = result.isAdmin; + logger.info(`[verify] Found or created user ${userId} for wallet ${normalizedAddress}`); + } + // Сохраняем идентификаторы гостевой сессии if (guestId) { await identityService.saveIdentity(userId, 'guest', guestId, true); } @@ -103,10 +134,11 @@ router.post('/verify', async (req, res) => { } // Проверяем наличие админских токенов - const adminStatus = await authService.checkAdminTokens(address.toLowerCase()); + const adminStatus = await authService.checkAdminTokens(normalizedAddress); if (adminStatus) { await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); + isAdmin = true; } // Обновляем сессию @@ -114,7 +146,7 @@ router.post('/verify', async (req, res) => { req.session.authenticated = true; req.session.authType = 'wallet'; req.session.isAdmin = adminStatus || isAdmin; - req.session.address = address.toLowerCase(); + req.session.address = normalizedAddress; // Всегда сохраняем нормализованный адрес // Удаляем временный ID delete req.session.tempUserId; @@ -129,7 +161,7 @@ router.post('/verify', async (req, res) => { return res.json({ success: true, userId, - address, + address: normalizedAddress, // Возвращаем нормализованный адрес isAdmin: adminStatus || isAdmin, authenticated: true }); diff --git a/backend/routes/identities.js b/backend/routes/identities.js index f152fc8..54d75c7 100644 --- a/backend/routes/identities.js +++ b/backend/routes/identities.js @@ -3,6 +3,7 @@ const router = express.Router(); const { requireAuth } = require('../middleware/auth'); const authService = require('../services/auth-service'); const logger = require('../utils/logger'); +const db = require('../db'); // Получение всех идентификаторов пользователя router.get('/', requireAuth, async (req, res) => { @@ -22,7 +23,29 @@ router.post('/link', requireAuth, async (req, res) => { const { type, value } = req.body; const userId = req.session.userId; - await authService.linkIdentity(userId, type, value); + // Если тип - wallet, сначала проверим, не привязан ли он уже к другому пользователю + if (type === 'wallet') { + const normalizedWallet = value.toLowerCase(); + + // Проверяем, существует ли уже такой кошелек + const existingCheck = await db.query( + `SELECT user_id FROM user_identities + WHERE provider = 'wallet' AND provider_id = $1`, + [normalizedWallet] + ); + + if (existingCheck.rows.length > 0) { + const existingUserId = existingCheck.rows[0].user_id; + if (existingUserId !== userId) { + return res.status(400).json({ + success: false, + error: `This wallet (${value}) is already linked to another account` + }); + } + } + } + + const result = await authService.linkIdentity(userId, type, value); // Обновляем сессию if (type === 'wallet') { @@ -41,6 +64,15 @@ router.post('/link', requireAuth, async (req, res) => { }); } catch (error) { logger.error('Error linking identity:', error); + + // Делаем более понятные сообщения об ошибках + if (error.message && error.message.includes('already belongs to another user')) { + return res.status(400).json({ + success: false, + error: `This identity is already linked to another account` + }); + } + res.status(500).json({ error: error.message || 'Internal server error' }); } }); diff --git a/backend/routes/users.js b/backend/routes/users.js index 3422eb5..9ba662d 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -43,4 +43,76 @@ router.post('/update-language', requireAuth, async (req, res) => { } }); +// Маршрут для обновления имени и фамилии пользователя +router.post('/update-profile', requireAuth, async (req, res) => { + try { + const { firstName, lastName } = req.body; + const userId = req.session.userId; + + // Проверка валидности данных + if (firstName && firstName.length > 255) { + return res.status(400).json({ error: 'Имя слишком длинное (максимум 255 символов)' }); + } + + if (lastName && lastName.length > 255) { + return res.status(400).json({ error: 'Фамилия слишком длинная (максимум 255 символов)' }); + } + + // Обновление имени и фамилии в базе данных + await db.query( + 'UPDATE users SET first_name = $1, last_name = $2 WHERE id = $3', + [firstName || null, lastName || null, userId] + ); + + res.json({ success: true }); + } catch (error) { + logger.error('Error updating user profile:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } +}); + +// Маршрут для получения профиля пользователя +router.get('/profile/current', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + + // Получение данных пользователя + const userResult = await db.query( + 'SELECT id, username, first_name, last_name, role, status, created_at, preferred_language FROM users WHERE id = $1', + [userId] + ); + + if (userResult.rows.length === 0) { + return res.status(404).json({ error: 'Пользователь не найден' }); + } + + // Получение идентификаторов пользователя + const identitiesResult = await db.query( + 'SELECT provider, provider_id FROM user_identities WHERE user_id = $1', + [userId] + ); + + const user = userResult.rows[0]; + const identities = identitiesResult.rows.reduce((acc, identity) => { + acc[identity.provider] = identity.provider_id; + return acc; + }, {}); + + res.json({ + id: user.id, + username: user.username, + firstName: user.first_name, + lastName: user.last_name, + role: user.role, + status: user.status, + createdAt: user.created_at, + preferredLanguage: user.preferred_language, + identities + }); + } catch (error) { + logger.error('Error getting user profile:', error); + res.status(500).json({ error: 'Внутренняя ошибка сервера' }); + } +}); + module.exports = router; diff --git a/backend/scripts/fix-duplicate-identities.js b/backend/scripts/fix-duplicate-identities.js new file mode 100644 index 0000000..dd98d2a --- /dev/null +++ b/backend/scripts/fix-duplicate-identities.js @@ -0,0 +1,268 @@ +/** + * Скрипт для поиска и исправления дубликатов идентификаторов в базе данных + */ +require('dotenv').config(); +const { Pool } = require('pg'); +const { ethers } = require('ethers'); +const path = require('path'); +const fs = require('fs'); + +// Настройка логирования +const logDir = path.join(__dirname, '../logs'); +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir); +} + +const logFile = path.join(logDir, 'fix-duplicates.log'); +const logger = { + log: message => { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}\n`; + console.log(message); + fs.appendFileSync(logFile, logMessage); + }, + error: (message, error) => { + const timestamp = new Date().toISOString(); + const errorDetail = error ? `: ${error.message}` : ''; + const logMessage = `[${timestamp}] ERROR: ${message}${errorDetail}\n`; + console.error(`ERROR: ${message}${errorDetail}`); + fs.appendFileSync(logFile, logMessage); + } +}; + +// Создаем подключение к базе данных +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +/** + * Нормализует адрес кошелька + * @param {string} address - Адрес кошелька + * @returns {string} - Нормализованный адрес в нижнем регистре + */ +function normalizeWalletAddress(address) { + try { + return ethers.getAddress(address).toLowerCase(); + } catch (error) { + logger.error(`Invalid wallet address: ${address}`, error); + return address; + } +} + +/** + * Находит все дубликаты идентификаторов кошельков + */ +async function findDuplicateWallets() { + const client = await pool.connect(); + + try { + logger.log('Поиск дубликатов wallet-идентификаторов...'); + + // Находим пары идентификаторов, которые отличаются только регистром + const result = await client.query(` + SELECT + ui1.id as id1, + ui1.user_id as user_id1, + ui1.provider_id as provider_id1, + ui2.id as id2, + ui2.user_id as user_id2, + ui2.provider_id as provider_id2 + FROM + user_identities ui1 + JOIN + user_identities ui2 ON ui1.id < ui2.id + WHERE + ui1.provider = 'wallet' AND + ui2.provider = 'wallet' AND + LOWER(ui1.provider_id) = LOWER(ui2.provider_id) AND + ui1.provider_id <> ui2.provider_id + `); + + logger.log(`Найдено ${result.rows.length} потенциальных дубликатов wallet-идентификаторов`); + + return result.rows; + } catch (error) { + logger.error('Ошибка при поиске дубликатов wallet-идентификаторов', error); + throw error; + } finally { + client.release(); + } +} + +/** + * Исправляет дубликаты идентификаторов + * @param {Array} duplicates - Массив найденных дубликатов + */ +async function fixDuplicates(duplicates) { + const client = await pool.connect(); + + try { + logger.log('Исправление дубликатов идентификаторов...'); + + await client.query('BEGIN'); + + for (const dup of duplicates) { + // Проверяем, принадлежат ли идентификаторы одному пользователю + if (dup.user_id1 === dup.user_id2) { + // Если да, удаляем один из дубликатов (не в нижнем регистре) + const normalizedAddress = normalizeWalletAddress(dup.provider_id1); + + // Определяем, какой идентификатор нужно удалить + const idToDelete = dup.provider_id1 === normalizedAddress ? dup.id2 : dup.id1; + + logger.log(`Удаление дубликата ID ${idToDelete} для адреса ${normalizedAddress}`); + + await client.query('DELETE FROM user_identities WHERE id = $1', [idToDelete]); + + // Проверяем, что второй идентификатор в нормализованной форме + const remainingId = dup.provider_id1 === normalizedAddress ? dup.id1 : dup.id2; + const remainingAddress = dup.provider_id1 === normalizedAddress ? dup.provider_id1 : dup.provider_id2; + + if (remainingAddress !== normalizedAddress) { + logger.log(`Обновление идентификатора ID ${remainingId} до нормализованного значения ${normalizedAddress}`); + + await client.query( + 'UPDATE user_identities SET provider_id = $1 WHERE id = $2', + [normalizedAddress, remainingId] + ); + } + } else { + // Если идентификаторы принадлежат разным пользователям, нужно решить конфликт + // Для определения какой пользователь является основным, можно использовать: + // 1. Количество сообщений/активности + // 2. Дату создания аккаунта + logger.log(`Конфликт: адрес ${dup.provider_id1}/${dup.provider_id2} привязан к разным пользователям: ${dup.user_id1} и ${dup.user_id2}`); + + // Определяем, какой пользователь является основным + const userInfoResult = await client.query(` + SELECT + id, + (SELECT COUNT(*) FROM messages WHERE user_id = users.id) as message_count, + (SELECT created_at FROM user_identities WHERE user_id = users.id ORDER BY created_at ASC LIMIT 1) as created_at + FROM + users + WHERE + id IN ($1, $2) + ORDER BY + message_count DESC, created_at ASC + `, [dup.user_id1, dup.user_id2]); + + // Если нет пользователей, пропускаем + if (userInfoResult.rows.length === 0) { + logger.log(`Пропуск: не найдены пользователи ${dup.user_id1} и ${dup.user_id2}`); + continue; + } + + // Выбираем первого пользователя как основного (с наибольшим количеством сообщений или самого старого) + const mainUserId = userInfoResult.rows[0].id; + const secondaryUserId = mainUserId === dup.user_id1 ? dup.user_id2 : dup.user_id1; + + logger.log(`Объединение пользователей: сохраняем ID ${mainUserId}, удаляем ID ${secondaryUserId}`); + + // Переносим все идентификаторы от вторичного пользователя к основному + await client.query(` + INSERT INTO user_identities (user_id, provider, provider_id) + SELECT $1, provider, provider_id + FROM user_identities + WHERE user_id = $2 + ON CONFLICT DO NOTHING + `, [mainUserId, secondaryUserId]); + + // Переносим сообщения + await client.query(` + UPDATE messages + SET user_id = $1 + WHERE user_id = $2 + `, [mainUserId, secondaryUserId]); + + // Переносим другие связанные данные... + // ... + + // Удаляем вторичного пользователя + await client.query('DELETE FROM user_identities WHERE user_id = $1', [secondaryUserId]); + await client.query('DELETE FROM users WHERE id = $1', [secondaryUserId]); + } + } + + await client.query('COMMIT'); + logger.log('Исправление дубликатов успешно завершено'); + } catch (error) { + await client.query('ROLLBACK'); + logger.error('Ошибка при исправлении дубликатов', error); + throw error; + } finally { + client.release(); + } +} + +/** + * Основная функция + */ +async function main() { + try { + logger.log('Запуск скрипта исправления дубликатов идентификаторов...'); + + // Шаг 1: Нормализация всех адресов кошельков (приведение к нижнему регистру) + const client = await pool.connect(); + + try { + logger.log('Нормализация всех существующих адресов кошельков...'); + + await client.query('BEGIN'); + + // Получаем все идентификаторы кошельков + const walletsResult = await client.query(` + SELECT id, provider_id + FROM user_identities + WHERE provider = 'wallet' + `); + + logger.log(`Найдено ${walletsResult.rows.length} идентификаторов кошельков`); + + // Обновляем каждый адрес к нормализованной форме + let updatedCount = 0; + + for (const wallet of walletsResult.rows) { + try { + const normalizedAddress = normalizeWalletAddress(wallet.provider_id); + + if (normalizedAddress !== wallet.provider_id) { + await client.query( + 'UPDATE user_identities SET provider_id = $1 WHERE id = $2', + [normalizedAddress, wallet.id] + ); + updatedCount++; + } + } catch (error) { + logger.error(`Ошибка при нормализации адреса ${wallet.provider_id}`, error); + } + } + + await client.query('COMMIT'); + logger.log(`Нормализовано ${updatedCount} адресов кошельков`); + } catch (error) { + await client.query('ROLLBACK'); + logger.error('Ошибка при нормализации адресов кошельков', error); + } finally { + client.release(); + } + + // Шаг 2: Поиск и исправление дубликатов + const duplicates = await findDuplicateWallets(); + + if (duplicates.length > 0) { + await fixDuplicates(duplicates); + } else { + logger.log('Дубликатов wallet-идентификаторов не найдено'); + } + + logger.log('Скрипт успешно завершил работу'); + } catch (error) { + logger.error('Критическая ошибка при выполнении скрипта', error); + } finally { + pool.end(); + } +} + +// Запускаем скрипт +main(); \ No newline at end of file diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index dfc5ed5..6d34f61 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -30,8 +30,15 @@ class AuthService { async verifySignature(message, signature, address) { try { if (!message || !signature || !address) return false; + + // Нормализуем входящий адрес + const normalizedAddress = ethers.getAddress(address).toLowerCase(); + + // Восстанавливаем адрес из подписи const recoveredAddress = ethers.verifyMessage(message, signature); - return ethers.getAddress(recoveredAddress) === ethers.getAddress(address); + + // Сравниваем нормализованные адреса + return ethers.getAddress(recoveredAddress).toLowerCase() === normalizedAddress; } catch (error) { logger.error('Error in signature verification:', error); return false; @@ -45,15 +52,15 @@ class AuthService { */ async findOrCreateUser(address) { try { - // Нормализуем адрес - address = ethers.getAddress(address); + // Нормализуем адрес - всегда приводим к нижнему регистру + const normalizedAddress = ethers.getAddress(address).toLowerCase(); // Ищем пользователя по адресу в таблице user_identities const userResult = await db.query(` SELECT u.* FROM users u JOIN user_identities ui ON u.id = ui.user_id WHERE ui.provider = 'wallet' AND ui.provider_id = $1 - `, [address]); + `, [normalizedAddress]); if (userResult.rows.length > 0) { const user = userResult.rows[0]; @@ -71,13 +78,22 @@ class AuthService { const userId = newUserResult.rows[0].id; - // Добавляем идентификатор кошелька + // Добавляем идентификатор кошелька (всегда в нижнем регистре) await db.query( 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)', - [userId, 'wallet', address] + [userId, 'wallet', normalizedAddress] ); - return { userId, isAdmin: false }; + // Проверяем, есть ли у пользователя роль админа + const isAdmin = await this.checkAdminRole(normalizedAddress); + + // Если у пользователя есть админские токены, обновляем его роль + if (isAdmin) { + await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); + logger.info(`New user ${userId} with wallet ${normalizedAddress} automatically granted admin role`); + } + + return { userId, isAdmin }; } catch (error) { console.error('Error finding or creating user:', error); throw error; @@ -454,8 +470,8 @@ class AuthService { // Если есть гостевой ID в сессии, сохраняем его для нового пользователя if (session.guestId && isNewUser) { await db.query( - 'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING', - [userId, 'guest', session.guestId] + 'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1', + [userId, session.guestId] ); logger.info(`[verifyTelegramAuth] Saved guest ID ${session.guestId} for user ${userId}`); } @@ -597,18 +613,25 @@ class AuthService { } // Нормализуем значение идентификатора - if (provider === 'wallet' && providerId) { - providerId = providerId.toLowerCase(); - } else if (provider === 'email' && providerId) { - providerId = providerId.toLowerCase(); + let normalizedProviderId = providerId; + if (provider === 'wallet') { + // Для кошельков используем ethers для валидации и нормализации + try { + normalizedProviderId = ethers.getAddress(providerId).toLowerCase(); + } catch (error) { + logger.error(`[AuthService] Invalid wallet address: ${providerId}`, error); + throw new Error('Invalid wallet address'); + } + } else if (provider === 'email') { + normalizedProviderId = providerId.toLowerCase(); } - logger.info(`[AuthService] Linking identity ${provider}:${providerId} to user ${userId}`); + logger.info(`[AuthService] Linking identity ${provider}:${normalizedProviderId} to user ${userId}`); // Проверяем, существует ли уже такой идентификатор const existingResult = await db.query( `SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, - [provider, providerId] + [provider, normalizedProviderId] ); if (existingResult.rows.length > 0) { @@ -616,11 +639,11 @@ class AuthService { // Если идентификатор уже принадлежит этому пользователю, ничего не делаем if (existingUserId === userId) { - logger.info(`[AuthService] Identity ${provider}:${providerId} already exists for user ${userId}`); + logger.info(`[AuthService] Identity ${provider}:${normalizedProviderId} already exists for user ${userId}`); return { success: true, message: 'Identity already exists' }; } else { // Если идентификатор принадлежит другому пользователю, возвращаем ошибку - logger.warn(`[AuthService] Identity ${provider}:${providerId} already belongs to user ${existingUserId}, not user ${userId}`); + logger.warn(`[AuthService] Identity ${provider}:${normalizedProviderId} already belongs to user ${existingUserId}, not user ${userId}`); throw new Error(`Identity already belongs to another user (${existingUserId})`); } } @@ -629,13 +652,13 @@ class AuthService { await db.query( `INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)`, - [userId, provider, providerId] + [userId, provider, normalizedProviderId] ); // Проверяем и обновляем роль администратора, если это идентификатор кошелька let isAdmin = false; if (provider === 'wallet') { - isAdmin = await this.checkAdminTokens(providerId); + isAdmin = await this.checkAdminTokens(normalizedProviderId); // Обновляем роль пользователя в базе данных, если нужно if (isAdmin) { @@ -647,7 +670,7 @@ class AuthService { } } - logger.info(`[AuthService] Identity ${provider}:${providerId} successfully linked to user ${userId}`); + logger.info(`[AuthService] Identity ${provider}:${normalizedProviderId} successfully linked to user ${userId}`); return { success: true, isAdmin }; } catch (error) { logger.error(`[AuthService] Error linking identity ${provider}:${providerId} to user ${userId}:`, error); diff --git a/backend/services/identity-service.js b/backend/services/identity-service.js index b797958..546900f 100644 --- a/backend/services/identity-service.js +++ b/backend/services/identity-service.js @@ -5,6 +5,32 @@ const logger = require('../utils/logger'); * Сервис для работы с идентификаторами пользователей */ class IdentityService { + /** + * Нормализует значения идентификаторов (приводит к нижнему регистру где нужно) + * @param {string} provider - Тип идентификатора + * @param {string} providerId - Значение идентификатора + * @returns {object} - Нормализованные значения + */ + normalizeIdentity(provider, providerId) { + if (!provider || !providerId) { + return { provider, providerId }; + } + + // Приводим провайдер к нижнему регистру + const normalizedProvider = provider.toLowerCase(); + + // Для email и wallet приводим значение к нижнему регистру + let normalizedProviderId = providerId; + if (normalizedProvider === 'wallet' || normalizedProvider === 'email') { + normalizedProviderId = providerId.toLowerCase(); + } + + return { + provider: normalizedProvider, + providerId: normalizedProviderId + }; + } + /** * Сохраняет идентификатор пользователя в базу данных * @param {number} userId - ID пользователя @@ -23,20 +49,18 @@ class IdentityService { }; } - // Приводим provider и providerId к нужному формату - provider = provider.toLowerCase(); - if (provider === 'wallet' || provider === 'email') { - providerId = providerId.toLowerCase(); - } + // Нормализуем значения + const { provider: normalizedProvider, providerId: normalizedProviderId } = + this.normalizeIdentity(provider, providerId); // Проверяем тип провайдера и перенаправляем гостевые идентификаторы в guest_user_mapping - if (provider === 'guest') { - logger.info(`[IdentityService] Converting guest identity for user ${userId} to guest_user_mapping: ${providerId}`); + if (normalizedProvider === 'guest') { + logger.info(`[IdentityService] Converting guest identity for user ${userId} to guest_user_mapping: ${normalizedProviderId}`); try { await db.query( 'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1', - [userId, providerId] + [userId, normalizedProviderId] ); return { success: true }; } catch (guestError) { @@ -47,20 +71,20 @@ class IdentityService { // Проверяем, разрешен ли такой тип провайдера const allowedProviders = ['email', 'wallet', 'telegram', 'username']; - if (!allowedProviders.includes(provider)) { - logger.warn(`[IdentityService] Invalid provider type: ${provider}`); + if (!allowedProviders.includes(normalizedProvider)) { + logger.warn(`[IdentityService] Invalid provider type: ${normalizedProvider}`); return { success: false, error: `Invalid provider type. Allowed types: ${allowedProviders.join(', ')}` }; } - logger.info(`[IdentityService] Saving identity for user ${userId}: ${provider}:${providerId}`); + logger.info(`[IdentityService] Saving identity for user ${userId}: ${normalizedProvider}:${normalizedProviderId}`); // Проверяем, существует ли уже такой идентификатор const existingResult = await db.query( `SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, - [provider, providerId] + [normalizedProvider, normalizedProviderId] ); if (existingResult.rows.length > 0) { @@ -68,10 +92,10 @@ class IdentityService { // Если идентификатор уже принадлежит этому пользователю, ничего не делаем if (existingUserId === userId) { - logger.info(`[IdentityService] Identity ${provider}:${providerId} already exists for user ${userId}`); + logger.info(`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already exists for user ${userId}`); } else { // Если идентификатор принадлежит другому пользователю, логируем это - logger.warn(`[IdentityService] Identity ${provider}:${providerId} already belongs to user ${existingUserId}, not user ${userId}`); + logger.warn(`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already belongs to user ${existingUserId}, not user ${userId}`); return { success: false, error: `Identity already belongs to another user (${existingUserId})` @@ -82,9 +106,9 @@ class IdentityService { await db.query( `INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)`, - [userId, provider, providerId] + [userId, normalizedProvider, normalizedProviderId] ); - logger.info(`[IdentityService] Created new identity ${provider}:${providerId} for user ${userId}`); + logger.info(`[IdentityService] Created new identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`); } return { success: true }; @@ -158,19 +182,23 @@ class IdentityService { return null; } + // Нормализуем значения + const { provider: normalizedProvider, providerId: normalizedProviderId } = + this.normalizeIdentity(provider, providerId); + const result = await db.query( `SELECT u.id, u.role FROM users u JOIN user_identities ui ON u.id = ui.user_id WHERE ui.provider = $1 AND ui.provider_id = $2`, - [provider, providerId] + [normalizedProvider, normalizedProviderId] ); if (result.rows.length === 0) { - logger.info(`[IdentityService] No user found with identity ${provider}:${providerId}`); + logger.info(`[IdentityService] No user found with identity ${normalizedProvider}:${normalizedProviderId}`); return null; } - logger.info(`[IdentityService] Found user ${result.rows[0].id} with identity ${provider}:${providerId}`); + logger.info(`[IdentityService] Found user ${result.rows[0].id} with identity ${normalizedProvider}:${normalizedProviderId}`); return result.rows[0]; } catch (error) { logger.error(`[IdentityService] Error finding user by identity ${provider}:${providerId}:`, error); @@ -195,12 +223,12 @@ class IdentityService { // Сохраняем все постоянные идентификаторы из сессии if (session.email) { - const emailResult = await this.saveIdentity(userId, 'email', session.email.toLowerCase(), true); + const emailResult = await this.saveIdentity(userId, 'email', session.email, true); results.push({ type: 'email', result: emailResult }); } if (session.address) { - const walletResult = await this.saveIdentity(userId, 'wallet', session.address.toLowerCase(), true); + const walletResult = await this.saveIdentity(userId, 'wallet', session.address, true); results.push({ type: 'wallet', result: walletResult }); } diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 5083c80..e223fa2 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -39,6 +39,7 @@ async function getBot() { const verification = codeResult.rows[0]; const providerId = verification.provider_id; + const linkedUserId = verification.user_id; // Получаем связанный userId если он есть let userId; // Отмечаем код как использованный @@ -62,33 +63,72 @@ async function getBot() { userId = existingTelegramUser.rows[0].user_id; logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`); } else { - // Создаем нового пользователя, если нет существующего с этим Telegram ID - const userResult = await db.query( - 'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id', - ['user'] - ); - userId = userResult.rows[0].id; - - // Связываем Telegram с новым пользователем - await db.query( - `INSERT INTO user_identities - (user_id, provider, provider_id, created_at) - VALUES ($1, $2, $3, NOW())`, - [userId, 'telegram', ctx.from.id.toString()] - ); - - // Если был гостевой ID, связываем его с новым пользователем - if (providerId) { + // Если код верификации был связан с существующим пользователем, используем его + if (linkedUserId) { + // Используем userId из кода верификации + userId = linkedUserId; + // Связываем Telegram с этим пользователем await db.query( `INSERT INTO user_identities (user_id, provider, provider_id, created_at) - VALUES ($1, $2, $3, NOW()) - ON CONFLICT (provider, provider_id) DO NOTHING`, - [userId, 'guest', providerId] + VALUES ($1, $2, $3, NOW())`, + [userId, 'telegram', ctx.from.id.toString()] ); + logger.info(`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`); + } else { + // Проверяем, есть ли пользователь, связанный с гостевым идентификатором + let existingUserWithGuestId = null; + if (providerId) { + const guestUserResult = await db.query( + `SELECT user_id FROM guest_user_mapping WHERE guest_id = $1`, + [providerId] + ); + if (guestUserResult.rows.length > 0) { + existingUserWithGuestId = guestUserResult.rows[0].user_id; + logger.info(`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`); + } + } + + if (existingUserWithGuestId) { + // Используем существующего пользователя и добавляем ему Telegram идентификатор + userId = existingUserWithGuestId; + await db.query( + `INSERT INTO user_identities + (user_id, provider, provider_id, created_at) + VALUES ($1, $2, $3, NOW())`, + [userId, 'telegram', ctx.from.id.toString()] + ); + logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`); + } else { + // Создаем нового пользователя, если не нашли существующего + const userResult = await db.query( + 'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id', + ['user'] + ); + userId = userResult.rows[0].id; + + // Связываем Telegram с новым пользователем + await db.query( + `INSERT INTO user_identities + (user_id, provider, provider_id, created_at) + VALUES ($1, $2, $3, NOW())`, + [userId, 'telegram', ctx.from.id.toString()] + ); + + // Если был гостевой ID, связываем его с новым пользователем + if (providerId) { + await db.query( + `INSERT INTO guest_user_mapping + (user_id, guest_id) + VALUES ($1, $2) + ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`, + [userId, providerId] + ); + } + + logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`); + } } - - logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`); } // Обновляем сессию в базе данных @@ -151,14 +191,30 @@ async function initTelegramAuth(session) { // Реальный пользователь будет создан или найден при проверке кода через бота const tempId = crypto.randomBytes(16).toString('hex'); - // Создаем код через сервис верификации с временным идентификатором + // Если пользователь уже авторизован, сохраняем его userId в guest_user_mapping + // чтобы потом при авторизации через бота этот пользователь был найден + if (session && session.authenticated && session.userId) { + const guestId = session.guestId || tempId; + + // Связываем гостевой ID с текущим пользователем + await db.query( + `INSERT INTO guest_user_mapping (user_id, guest_id) + VALUES ($1, $2) + ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`, + [session.userId, guestId] + ); + + logger.info(`[initTelegramAuth] Linked guestId ${guestId} to authenticated user ${session.userId}`); + } + + // Создаем код через сервис верификации с идентификатором const code = await verificationService.createVerificationCode( 'telegram', session.guestId || tempId, - null // Не привязываем к конкретному userId на этом этапе + session.authenticated ? session.userId : null ); - logger.info(`[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}`); + logger.info(`[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}${session.authenticated ? `, userId: ${session.userId}` : ''}`); return { verificationCode: code,