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

This commit is contained in:
2025-04-16 16:39:58 +03:00
parent 31189163af
commit f371521511
11 changed files with 777 additions and 76 deletions

View File

@@ -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;

View File

@@ -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 $$;

View File

@@ -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);

View File

@@ -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",

View File

@@ -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
});

View File

@@ -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' });
}
});

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);

View File

@@ -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 });
}

View File

@@ -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,7 +63,44 @@ async function getBot() {
userId = existingTelegramUser.rows[0].user_id;
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
} else {
// Создаем нового пользователя, если нет существующего с этим Telegram ID
// Если код верификации был связан с существующим пользователем, используем его
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())`,
[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']
@@ -80,16 +118,18 @@ async function getBot() {
// Если был гостевой ID, связываем его с новым пользователем
if (providerId) {
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]
`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}`);
}
}
}
// Обновляем сессию в базе данных
await db.query(
@@ -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,