ваше сообщение коммита
This commit is contained in:
56
backend/db/migrations/015_users_table_refactor.sql
Normal file
56
backend/db/migrations/015_users_table_refactor.sql
Normal 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;
|
||||||
93
backend/db/migrations/016_fix_duplicate_identities.sql
Normal file
93
backend/db/migrations/016_fix_duplicate_identities.sql
Normal 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 $$;
|
||||||
40
backend/db/migrations/fix_duplicates_manual.sql
Normal file
40
backend/db/migrations/fix_duplicates_manual.sql
Normal 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);
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier --write \"**/*.{js,vue,json,md}\"",
|
"format": "prettier --write \"**/*.{js,vue,json,md}\"",
|
||||||
"format:check": "prettier --check \"**/*.{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": {
|
"dependencies": {
|
||||||
"@langchain/community": "^0.3.34",
|
"@langchain/community": "^0.3.34",
|
||||||
|
|||||||
@@ -82,18 +82,49 @@ router.post('/verify', async (req, res) => {
|
|||||||
return res.status(401).json({ success: false, error: 'Invalid signature' });
|
return res.status(401).json({ success: false, error: 'Invalid signature' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Нормализуем адрес для использования в запросах
|
||||||
|
const normalizedAddress = ethers.getAddress(address).toLowerCase();
|
||||||
|
|
||||||
// Проверяем nonce
|
// Проверяем 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]) {
|
if (nonceResult.rows.length === 0 || nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]) {
|
||||||
return res.status(401).json({ success: false, error: 'Invalid nonce' });
|
return res.status(401).json({ success: false, error: 'Invalid nonce' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Находим или создаем пользователя
|
let userId;
|
||||||
const { userId, isAdmin } = await authService.findOrCreateUser(address.toLowerCase());
|
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) {
|
if (guestId) {
|
||||||
await identityService.saveIdentity(userId, 'guest', guestId, true);
|
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) {
|
if (adminStatus) {
|
||||||
await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
|
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.authenticated = true;
|
||||||
req.session.authType = 'wallet';
|
req.session.authType = 'wallet';
|
||||||
req.session.isAdmin = adminStatus || isAdmin;
|
req.session.isAdmin = adminStatus || isAdmin;
|
||||||
req.session.address = address.toLowerCase();
|
req.session.address = normalizedAddress; // Всегда сохраняем нормализованный адрес
|
||||||
|
|
||||||
// Удаляем временный ID
|
// Удаляем временный ID
|
||||||
delete req.session.tempUserId;
|
delete req.session.tempUserId;
|
||||||
@@ -129,7 +161,7 @@ router.post('/verify', async (req, res) => {
|
|||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
userId,
|
userId,
|
||||||
address,
|
address: normalizedAddress, // Возвращаем нормализованный адрес
|
||||||
isAdmin: adminStatus || isAdmin,
|
isAdmin: adminStatus || isAdmin,
|
||||||
authenticated: true
|
authenticated: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const router = express.Router();
|
|||||||
const { requireAuth } = require('../middleware/auth');
|
const { requireAuth } = require('../middleware/auth');
|
||||||
const authService = require('../services/auth-service');
|
const authService = require('../services/auth-service');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
const db = require('../db');
|
||||||
|
|
||||||
// Получение всех идентификаторов пользователя
|
// Получение всех идентификаторов пользователя
|
||||||
router.get('/', requireAuth, async (req, res) => {
|
router.get('/', requireAuth, async (req, res) => {
|
||||||
@@ -22,7 +23,29 @@ router.post('/link', requireAuth, async (req, res) => {
|
|||||||
const { type, value } = req.body;
|
const { type, value } = req.body;
|
||||||
const userId = req.session.userId;
|
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') {
|
if (type === 'wallet') {
|
||||||
@@ -41,6 +64,15 @@ router.post('/link', requireAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error linking identity:', 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' });
|
res.status(500).json({ error: error.message || 'Internal server error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
module.exports = router;
|
||||||
|
|||||||
268
backend/scripts/fix-duplicate-identities.js
Normal file
268
backend/scripts/fix-duplicate-identities.js
Normal 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();
|
||||||
@@ -30,8 +30,15 @@ class AuthService {
|
|||||||
async verifySignature(message, signature, address) {
|
async verifySignature(message, signature, address) {
|
||||||
try {
|
try {
|
||||||
if (!message || !signature || !address) return false;
|
if (!message || !signature || !address) return false;
|
||||||
|
|
||||||
|
// Нормализуем входящий адрес
|
||||||
|
const normalizedAddress = ethers.getAddress(address).toLowerCase();
|
||||||
|
|
||||||
|
// Восстанавливаем адрес из подписи
|
||||||
const recoveredAddress = ethers.verifyMessage(message, signature);
|
const recoveredAddress = ethers.verifyMessage(message, signature);
|
||||||
return ethers.getAddress(recoveredAddress) === ethers.getAddress(address);
|
|
||||||
|
// Сравниваем нормализованные адреса
|
||||||
|
return ethers.getAddress(recoveredAddress).toLowerCase() === normalizedAddress;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error in signature verification:', error);
|
logger.error('Error in signature verification:', error);
|
||||||
return false;
|
return false;
|
||||||
@@ -45,15 +52,15 @@ class AuthService {
|
|||||||
*/
|
*/
|
||||||
async findOrCreateUser(address) {
|
async findOrCreateUser(address) {
|
||||||
try {
|
try {
|
||||||
// Нормализуем адрес
|
// Нормализуем адрес - всегда приводим к нижнему регистру
|
||||||
address = ethers.getAddress(address);
|
const normalizedAddress = ethers.getAddress(address).toLowerCase();
|
||||||
|
|
||||||
// Ищем пользователя по адресу в таблице user_identities
|
// Ищем пользователя по адресу в таблице user_identities
|
||||||
const userResult = await db.query(`
|
const userResult = await db.query(`
|
||||||
SELECT u.* FROM users u
|
SELECT u.* FROM users u
|
||||||
JOIN user_identities ui ON u.id = ui.user_id
|
JOIN user_identities ui ON u.id = ui.user_id
|
||||||
WHERE ui.provider = 'wallet' AND ui.provider_id = $1
|
WHERE ui.provider = 'wallet' AND ui.provider_id = $1
|
||||||
`, [address]);
|
`, [normalizedAddress]);
|
||||||
|
|
||||||
if (userResult.rows.length > 0) {
|
if (userResult.rows.length > 0) {
|
||||||
const user = userResult.rows[0];
|
const user = userResult.rows[0];
|
||||||
@@ -71,13 +78,22 @@ class AuthService {
|
|||||||
|
|
||||||
const userId = newUserResult.rows[0].id;
|
const userId = newUserResult.rows[0].id;
|
||||||
|
|
||||||
// Добавляем идентификатор кошелька
|
// Добавляем идентификатор кошелька (всегда в нижнем регистре)
|
||||||
await db.query(
|
await db.query(
|
||||||
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)',
|
'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) {
|
} catch (error) {
|
||||||
console.error('Error finding or creating user:', error);
|
console.error('Error finding or creating user:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -454,8 +470,8 @@ class AuthService {
|
|||||||
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя
|
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя
|
||||||
if (session.guestId && isNewUser) {
|
if (session.guestId && isNewUser) {
|
||||||
await db.query(
|
await db.query(
|
||||||
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING',
|
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
|
||||||
[userId, 'guest', session.guestId]
|
[userId, session.guestId]
|
||||||
);
|
);
|
||||||
logger.info(`[verifyTelegramAuth] Saved guest ID ${session.guestId} for user ${userId}`);
|
logger.info(`[verifyTelegramAuth] Saved guest ID ${session.guestId} for user ${userId}`);
|
||||||
}
|
}
|
||||||
@@ -597,18 +613,25 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Нормализуем значение идентификатора
|
// Нормализуем значение идентификатора
|
||||||
if (provider === 'wallet' && providerId) {
|
let normalizedProviderId = providerId;
|
||||||
providerId = providerId.toLowerCase();
|
if (provider === 'wallet') {
|
||||||
} else if (provider === 'email' && providerId) {
|
// Для кошельков используем ethers для валидации и нормализации
|
||||||
providerId = providerId.toLowerCase();
|
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(
|
const existingResult = await db.query(
|
||||||
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
|
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
|
||||||
[provider, providerId]
|
[provider, normalizedProviderId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingResult.rows.length > 0) {
|
if (existingResult.rows.length > 0) {
|
||||||
@@ -616,11 +639,11 @@ class AuthService {
|
|||||||
|
|
||||||
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем
|
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем
|
||||||
if (existingUserId === userId) {
|
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' };
|
return { success: true, message: 'Identity already exists' };
|
||||||
} else {
|
} 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})`);
|
throw new Error(`Identity already belongs to another user (${existingUserId})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,13 +652,13 @@ class AuthService {
|
|||||||
await db.query(
|
await db.query(
|
||||||
`INSERT INTO user_identities (user_id, provider, provider_id)
|
`INSERT INTO user_identities (user_id, provider, provider_id)
|
||||||
VALUES ($1, $2, $3)`,
|
VALUES ($1, $2, $3)`,
|
||||||
[userId, provider, providerId]
|
[userId, provider, normalizedProviderId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Проверяем и обновляем роль администратора, если это идентификатор кошелька
|
// Проверяем и обновляем роль администратора, если это идентификатор кошелька
|
||||||
let isAdmin = false;
|
let isAdmin = false;
|
||||||
if (provider === 'wallet') {
|
if (provider === 'wallet') {
|
||||||
isAdmin = await this.checkAdminTokens(providerId);
|
isAdmin = await this.checkAdminTokens(normalizedProviderId);
|
||||||
|
|
||||||
// Обновляем роль пользователя в базе данных, если нужно
|
// Обновляем роль пользователя в базе данных, если нужно
|
||||||
if (isAdmin) {
|
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 };
|
return { success: true, isAdmin };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[AuthService] Error linking identity ${provider}:${providerId} to user ${userId}:`, error);
|
logger.error(`[AuthService] Error linking identity ${provider}:${providerId} to user ${userId}:`, error);
|
||||||
|
|||||||
@@ -5,6 +5,32 @@ const logger = require('../utils/logger');
|
|||||||
* Сервис для работы с идентификаторами пользователей
|
* Сервис для работы с идентификаторами пользователей
|
||||||
*/
|
*/
|
||||||
class IdentityService {
|
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 пользователя
|
* @param {number} userId - ID пользователя
|
||||||
@@ -23,20 +49,18 @@ class IdentityService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Приводим provider и providerId к нужному формату
|
// Нормализуем значения
|
||||||
provider = provider.toLowerCase();
|
const { provider: normalizedProvider, providerId: normalizedProviderId } =
|
||||||
if (provider === 'wallet' || provider === 'email') {
|
this.normalizeIdentity(provider, providerId);
|
||||||
providerId = providerId.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем тип провайдера и перенаправляем гостевые идентификаторы в guest_user_mapping
|
// Проверяем тип провайдера и перенаправляем гостевые идентификаторы в guest_user_mapping
|
||||||
if (provider === 'guest') {
|
if (normalizedProvider === 'guest') {
|
||||||
logger.info(`[IdentityService] Converting guest identity for user ${userId} to guest_user_mapping: ${providerId}`);
|
logger.info(`[IdentityService] Converting guest identity for user ${userId} to guest_user_mapping: ${normalizedProviderId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.query(
|
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',
|
'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 };
|
return { success: true };
|
||||||
} catch (guestError) {
|
} catch (guestError) {
|
||||||
@@ -47,20 +71,20 @@ class IdentityService {
|
|||||||
|
|
||||||
// Проверяем, разрешен ли такой тип провайдера
|
// Проверяем, разрешен ли такой тип провайдера
|
||||||
const allowedProviders = ['email', 'wallet', 'telegram', 'username'];
|
const allowedProviders = ['email', 'wallet', 'telegram', 'username'];
|
||||||
if (!allowedProviders.includes(provider)) {
|
if (!allowedProviders.includes(normalizedProvider)) {
|
||||||
logger.warn(`[IdentityService] Invalid provider type: ${provider}`);
|
logger.warn(`[IdentityService] Invalid provider type: ${normalizedProvider}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Invalid provider type. Allowed types: ${allowedProviders.join(', ')}`
|
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(
|
const existingResult = await db.query(
|
||||||
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
|
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
|
||||||
[provider, providerId]
|
[normalizedProvider, normalizedProviderId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingResult.rows.length > 0) {
|
if (existingResult.rows.length > 0) {
|
||||||
@@ -68,10 +92,10 @@ class IdentityService {
|
|||||||
|
|
||||||
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем
|
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем
|
||||||
if (existingUserId === userId) {
|
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 {
|
} 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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Identity already belongs to another user (${existingUserId})`
|
error: `Identity already belongs to another user (${existingUserId})`
|
||||||
@@ -82,9 +106,9 @@ class IdentityService {
|
|||||||
await db.query(
|
await db.query(
|
||||||
`INSERT INTO user_identities (user_id, provider, provider_id)
|
`INSERT INTO user_identities (user_id, provider, provider_id)
|
||||||
VALUES ($1, $2, $3)`,
|
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 };
|
return { success: true };
|
||||||
@@ -158,19 +182,23 @@ class IdentityService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Нормализуем значения
|
||||||
|
const { provider: normalizedProvider, providerId: normalizedProviderId } =
|
||||||
|
this.normalizeIdentity(provider, providerId);
|
||||||
|
|
||||||
const result = await db.query(
|
const result = await db.query(
|
||||||
`SELECT u.id, u.role FROM users u
|
`SELECT u.id, u.role FROM users u
|
||||||
JOIN user_identities ui ON u.id = ui.user_id
|
JOIN user_identities ui ON u.id = ui.user_id
|
||||||
WHERE ui.provider = $1 AND ui.provider_id = $2`,
|
WHERE ui.provider = $1 AND ui.provider_id = $2`,
|
||||||
[provider, providerId]
|
[normalizedProvider, normalizedProviderId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
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;
|
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];
|
return result.rows[0];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[IdentityService] Error finding user by identity ${provider}:${providerId}:`, error);
|
logger.error(`[IdentityService] Error finding user by identity ${provider}:${providerId}:`, error);
|
||||||
@@ -195,12 +223,12 @@ class IdentityService {
|
|||||||
|
|
||||||
// Сохраняем все постоянные идентификаторы из сессии
|
// Сохраняем все постоянные идентификаторы из сессии
|
||||||
if (session.email) {
|
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 });
|
results.push({ type: 'email', result: emailResult });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.address) {
|
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 });
|
results.push({ type: 'wallet', result: walletResult });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ async function getBot() {
|
|||||||
|
|
||||||
const verification = codeResult.rows[0];
|
const verification = codeResult.rows[0];
|
||||||
const providerId = verification.provider_id;
|
const providerId = verification.provider_id;
|
||||||
|
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть
|
||||||
let userId;
|
let userId;
|
||||||
|
|
||||||
// Отмечаем код как использованный
|
// Отмечаем код как использованный
|
||||||
@@ -62,33 +63,72 @@ async function getBot() {
|
|||||||
userId = existingTelegramUser.rows[0].user_id;
|
userId = existingTelegramUser.rows[0].user_id;
|
||||||
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
|
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
|
||||||
} else {
|
} else {
|
||||||
// Создаем нового пользователя, если нет существующего с этим Telegram ID
|
// Если код верификации был связан с существующим пользователем, используем его
|
||||||
const userResult = await db.query(
|
if (linkedUserId) {
|
||||||
'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id',
|
// Используем userId из кода верификации
|
||||||
['user']
|
userId = linkedUserId;
|
||||||
);
|
// Связываем Telegram с этим пользователем
|
||||||
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(
|
await db.query(
|
||||||
`INSERT INTO user_identities
|
`INSERT INTO user_identities
|
||||||
(user_id, provider, provider_id, created_at)
|
(user_id, provider, provider_id, created_at)
|
||||||
VALUES ($1, $2, $3, NOW())
|
VALUES ($1, $2, $3, NOW())`,
|
||||||
ON CONFLICT (provider, provider_id) DO NOTHING`,
|
[userId, 'telegram', ctx.from.id.toString()]
|
||||||
[userId, 'guest', providerId]
|
|
||||||
);
|
);
|
||||||
|
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');
|
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(
|
const code = await verificationService.createVerificationCode(
|
||||||
'telegram',
|
'telegram',
|
||||||
session.guestId || tempId,
|
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 {
|
return {
|
||||||
verificationCode: code,
|
verificationCode: code,
|
||||||
|
|||||||
Reference in New Issue
Block a user