diff --git a/backend/db/migrations/014_identity_system_refactor.sql b/backend/db/migrations/014_identity_system_refactor.sql new file mode 100644 index 0000000..e89bc37 --- /dev/null +++ b/backend/db/migrations/014_identity_system_refactor.sql @@ -0,0 +1,190 @@ +-- Комплексная миграция для реструктуризации системы идентификации пользователей +-- Объединяет изменения из миграций 014-018 в одну идемпотентную миграцию + +-- 1. Создание таблицы guest_user_mapping, если она ещё не существует +CREATE TABLE IF NOT EXISTS guest_user_mapping ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + guest_id VARCHAR(255) NOT NULL, + processed BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(guest_id) +); + +-- 2. Создание индексов для guest_user_mapping +CREATE INDEX IF NOT EXISTS idx_guest_user_mapping_guest_id ON guest_user_mapping(guest_id); +CREATE INDEX IF NOT EXISTS idx_guest_user_mapping_user_id ON guest_user_mapping(user_id); + +-- 3. Перенос гостевых идентификаторов из user_identities в guest_user_mapping +DO $$ +BEGIN + -- Выполняем только если есть гостевые идентификаторы в user_identities + IF EXISTS (SELECT 1 FROM user_identities WHERE provider = 'guest') THEN + INSERT INTO guest_user_mapping (user_id, guest_id, processed) + SELECT user_id, provider_id, true + FROM user_identities + WHERE provider = 'guest' + ON CONFLICT (guest_id) DO NOTHING; + + -- Удаляем перенесенные идентификаторы + DELETE FROM user_identities WHERE provider = 'guest'; + END IF; +END $$; + +-- 4. Добавление/обновление поля user_id в таблице messages +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'messages' AND column_name = 'user_id' + ) THEN + ALTER TABLE messages ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE; + + -- Создаем индекс + CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id); + + -- Заполняем поле user_id из таблицы conversations + UPDATE messages m + SET user_id = c.user_id + FROM conversations c + WHERE m.conversation_id = c.id AND m.user_id IS NULL; + END IF; +END $$; + +-- 5. Создаем триггерную функцию для автоматического заполнения user_id +CREATE OR REPLACE FUNCTION set_message_user_id() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.user_id IS NULL THEN + SELECT user_id INTO NEW.user_id + FROM conversations + WHERE id = NEW.conversation_id; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- 6. Создаем триггер для автоматического заполнения user_id +DROP TRIGGER IF EXISTS trg_set_message_user_id ON messages; +CREATE TRIGGER trg_set_message_user_id +BEFORE INSERT ON messages +FOR EACH ROW +EXECUTE FUNCTION set_message_user_id(); + +-- 7. Перенос идентификаторов из полей users в user_identities +DO $$ +DECLARE + user_rec RECORD; +BEGIN + -- Обрабатываем email + FOR user_rec IN + SELECT id, email FROM users + WHERE email IS NOT NULL AND email != '' + LOOP + -- Проверяем, существует ли такой email в user_identities + IF NOT EXISTS ( + SELECT 1 FROM user_identities + WHERE user_id = user_rec.id AND provider = 'email' AND provider_id = user_rec.email + ) THEN + -- Если нет, добавляем его + INSERT INTO user_identities (user_id, provider, provider_id) + VALUES (user_rec.id, 'email', LOWER(user_rec.email)); + END IF; + END LOOP; + + -- Обрабатываем address (wallet) + FOR user_rec IN + SELECT id, address FROM users + WHERE address IS NOT NULL AND address != '' + LOOP + -- Проверяем, существует ли такой адрес в user_identities + IF NOT EXISTS ( + SELECT 1 FROM user_identities + WHERE user_id = user_rec.id AND provider = 'wallet' AND provider_id = LOWER(user_rec.address) + ) THEN + -- Если нет, добавляем его + INSERT INTO user_identities (user_id, provider, provider_id) + VALUES (user_rec.id, 'wallet', LOWER(user_rec.address)); + END IF; + END LOOP; +END $$; + +-- 8. Очистка устаревших полей в таблице users +UPDATE users +SET + email = NULL, + address = NULL, + username = NULL +WHERE + email IS NOT NULL OR address IS NOT NULL OR username IS NOT NULL; + +-- 9. Нормализация регистра для email и wallet идентификаторов +UPDATE user_identities +SET provider_id = LOWER(provider_id) +WHERE (provider = 'wallet' OR provider = 'email') AND provider_id != LOWER(provider_id); + +-- 10. Ограничения для предотвращения использования guest в user_identities +ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_not_guest; +ALTER TABLE user_identities ADD CONSTRAINT check_provider_not_guest + CHECK (provider != 'guest'); + +-- 11. Ограничение на допустимые типы идентификаторов +ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_allowed; +ALTER TABLE user_identities ADD CONSTRAINT check_provider_allowed + CHECK (provider IN ('email', 'wallet', 'telegram')); + +-- 12. Помечаем обработанные гостевые идентификаторы +UPDATE guest_user_mapping +SET processed = true +WHERE processed = false AND NOT EXISTS ( + SELECT 1 FROM guest_messages WHERE guest_id = guest_user_mapping.guest_id +); + +-- 13. Добавляем комментарии к таблицам и полям +COMMENT ON TABLE users IS 'Основная таблица пользователей системы'; +COMMENT ON TABLE user_identities IS 'Таблица идентификаторов пользователей (email, wallet, telegram)'; +COMMENT ON TABLE guest_user_mapping IS 'Таблица связи гостевых идентификаторов с пользователями'; +COMMENT ON TABLE conversations IS 'Диалоги пользователей с системой'; +COMMENT ON TABLE messages IS 'Сообщения пользователей и системы'; +COMMENT ON TABLE guest_messages IS 'Временное хранилище сообщений от неавторизованных пользователей'; + +COMMENT ON COLUMN users.id IS 'Уникальный идентификатор пользователя'; +COMMENT ON COLUMN users.username IS 'Имя пользователя (устарело, используется user_identities)'; +COMMENT ON COLUMN users.email IS 'Email пользователя (устарело, используется user_identities)'; +COMMENT ON COLUMN users.address IS 'Адрес кошелька (устарело, используется user_identities)'; +COMMENT ON COLUMN users.status IS 'Статус пользователя (active, blocked)'; +COMMENT ON COLUMN users.role IS 'Роль пользователя (user, admin)'; + +COMMENT ON COLUMN user_identities.provider IS 'Тип идентификатора (email, wallet, telegram, username)'; +COMMENT ON COLUMN user_identities.provider_id IS 'Значение идентификатора'; + +COMMENT ON COLUMN guest_user_mapping.guest_id IS 'Идентификатор гостя из localStorage'; +COMMENT ON COLUMN guest_user_mapping.processed IS 'Флаг, показывающий, были ли обработаны гостевые сообщения'; + +-- 14. Создаем диагностическую функцию +CREATE OR REPLACE FUNCTION verify_identity_system() +RETURNS TABLE ( + users_with_address INTEGER, + users_with_email INTEGER, + wallet_identities INTEGER, + email_identities INTEGER, + telegram_identities INTEGER, + guest_mapping_count INTEGER, + guest_messages_count INTEGER, + duplicate_provider_ids INTEGER +) AS $$ +BEGIN + RETURN QUERY + SELECT + (SELECT COUNT(*) FROM users WHERE address IS NOT NULL), + (SELECT COUNT(*) FROM users WHERE email IS NOT NULL), + (SELECT COUNT(*) FROM user_identities WHERE provider = 'wallet'), + (SELECT COUNT(*) FROM user_identities WHERE provider = 'email'), + (SELECT COUNT(*) FROM user_identities WHERE provider = 'telegram'), + (SELECT COUNT(*) FROM guest_user_mapping), + (SELECT COUNT(*) FROM guest_messages), + (SELECT COUNT(*) FROM + (SELECT provider, provider_id, COUNT(*) FROM user_identities + GROUP BY provider, provider_id HAVING COUNT(*) > 1) AS dups); +END; +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/backend/docs/identity_architecture.md b/backend/docs/identity_architecture.md new file mode 100644 index 0000000..89e01b4 --- /dev/null +++ b/backend/docs/identity_architecture.md @@ -0,0 +1,130 @@ +# Архитектура идентификаторов пользователей + +## Общая структура + +### Таблицы для хранения данных пользователей + +Система идентификации пользователей построена на следующих таблицах: + +1. **users** - Основная таблица пользователей + - `id SERIAL PRIMARY KEY` - Основной идентификатор пользователя + - `status` - Статус пользователя (active, blocked) + - `role` - Роль пользователя (user, admin) + - `created_at`, `updated_at` - Временные метки + - Поля `username`, `email` и `address` являются устаревшими и должны быть NULL + +2. **user_identities** - Таблица идентификаторов пользователей + - `id SERIAL PRIMARY KEY` - Идентификатор записи + - `user_id INTEGER REFERENCES users(id)` - Ссылка на пользователя + - `provider VARCHAR(50)` - Тип идентификатора (email, wallet, telegram, username) + - `provider_id VARCHAR(255)` - Значение идентификатора (должно быть в нижнем регистре для email и wallet) + - Уникальный составной ключ `(provider, provider_id)` + - Ограничение `CHECK (provider IN ('email', 'wallet', 'telegram', 'username'))` - запрещает тип 'guest' + +3. **guest_user_mapping** - Таблица связи гостевых идентификаторов с пользователями + - `id SERIAL PRIMARY KEY` - Идентификатор записи + - `user_id INTEGER REFERENCES users(id)` - Ссылка на пользователя + - `guest_id VARCHAR(255)` - Гостевой идентификатор + - `processed BOOLEAN` - Флаг обработки гостевых сообщений + - Уникальный ключ `guest_id` + +4. **messages** - Таблица сообщений + - `id SERIAL PRIMARY KEY` - Идентификатор сообщения + - `conversation_id INTEGER REFERENCES conversations(id)` - Ссылка на диалог + - `user_id INTEGER REFERENCES users(id)` - Прямая ссылка на пользователя + - `content TEXT` - Содержание сообщения + - `sender_type`, `role` - Тип отправителя и роль (user/assistant) + +5. **guest_messages** - Таблица гостевых сообщений + - `id SERIAL PRIMARY KEY` - Идентификатор гостевого сообщения + - `guest_id VARCHAR(255)` - Идентификатор гостя + - `content TEXT` - Содержание сообщения + - `is_ai BOOLEAN` - Флаг, указывающий на сообщение от AI + +## Процесс аутентификации и работа с гостевыми сообщениями + +### Гостевой доступ + +1. Гость (неаутентифицированный пользователь) начинает взаимодействие с системой +2. Для гостя генерируется уникальный `guest_id`, который сохраняется в localStorage браузера +3. Гостевые сообщения сохраняются в таблице `guest_messages` с привязкой к `guest_id` +4. Локально сообщения также хранятся в localStorage + +### Аутентификация пользователя + +1. Когда гость аутентифицируется (через email, wallet или telegram): + - Создается запись в таблице `users` + - Создается запись в таблице `user_identities` с соответствующим провайдером + - Гостевой ID сохраняется в таблице `guest_user_mapping` (не в user_identities) + +2. После аутентификации система автоматически обрабатывает гостевые сообщения: + - Вызывается метод `linkGuestMessages` + - Создается новый диалог для гостевых сообщений + - Гостевые сообщения переносятся в таблицу `messages` с привязкой к пользователю + - Обработанные гостевые сообщения удаляются из `guest_messages` + - Запись в `guest_user_mapping` помечается как `processed = true` + +### Объединение пользователей + +Если пользователь аутентифицируется разными способами, система может объединить его данные: + +1. Система проверяет связанных пользователей через `user_identities` +2. Если находятся связанные пользователи, вызывается метод `migrateUserData` +3. Данные от вторичных аккаунтов мигрируют к основному: + - Идентификаторы в таблице `user_identities` + - Гостевые связи в таблице `guest_user_mapping` + - Сообщения с прямым указанием `user_id` + - Диалоги + - Настройки + +## Ограничения и правила + +1. Тип провайдера `guest` запрещен в таблице `user_identities` (проверяется ограничением CHECK) +2. Гостевые идентификаторы хранятся только в таблице `guest_user_mapping` +3. Все идентификаторы email и wallet должны храниться в нижнем регистре +4. Таблица `messages` имеет прямую связь с пользователем через поле `user_id` +5. Сообщения всегда связаны с конкретным пользователем и диалогом +6. В таблице `users` поля `username`, `email` и `address` должны быть NULL + +## Обработка ошибок + +1. Если возникает ошибка при обработке гостевых сообщений, система: + - Логирует ошибку + - Продолжает попытки обработки при следующих авторизациях + - Не удаляет гостевые сообщения до успешной обработки + +2. Если гостевые сообщения уже обработаны, повторная обработка пропускается + +## Оптимизации + +1. Индексы созданы для всех полей, используемых в запросах: + - `user_identities(user_id)` + - `user_identities(provider, provider_id)` + - `guest_user_mapping(guest_id)` + - `guest_user_mapping(user_id)` + - `messages(user_id)` + - `messages(conversation_id)` + +2. Триггеры автоматически поддерживают целостность данных: + - Автоматическое заполнение `user_id` в таблице `messages` + - Очистка неиспользуемых полей в таблице `users` + +3. Ограничения предотвращают некорректные данные: + - Запрет на использование провайдера `guest` в таблице `user_identities` + - Уникальность `guest_id` в таблице `guest_user_mapping` + - Ограничение допустимых значений для поля `provider` + +## Функции для диагностики + +1. **verify_migration_017()** - проверяет состояние гостевых идентификаторов + - `guest_identities_count` - количество гостевых идентификаторов в таблице user_identities + - `guest_mapping_count` - количество записей в таблице guest_user_mapping + - `missing_mappings` - количество гостевых ID, которые отсутствуют в guest_user_mapping + +2. **verify_identity_data()** - проверяет общее состояние данных идентификаторов + - `users_with_address` - количество пользователей с заполненным полем address + - `users_with_email` - количество пользователей с заполненным полем email + - `wallet_identities` - количество идентификаторов wallet + - `email_identities` - количество идентификаторов email + - `telegram_identities` - количество идентификаторов telegram + - `duplicate_provider_ids` - количество дублирующихся идентификаторов \ No newline at end of file diff --git a/backend/docs/migration_guide.md b/backend/docs/migration_guide.md new file mode 100644 index 0000000..50bf770 --- /dev/null +++ b/backend/docs/migration_guide.md @@ -0,0 +1,74 @@ +# Руководство по миграциям базы данных + +## Общая информация + +Система миграций базы данных предназначена для поддержания структуры базы данных в актуальном состоянии и обеспечения возможности обновления между версиями приложения. + +## Структура миграций + +Миграции размещены в папке `backend/db/migrations/` и именуются по схеме `XXX_descriptive_name.sql`, где XXX - порядковый номер миграции. + +### Категории миграций + +1. **Основные структурные миграции** (001-013) - создание базовых таблиц и первоначальной структуры +2. **Функциональные миграции** - изменения, связанные с конкретными функциями +3. **Рефакторинг и оптимизация** (019+) - улучшение существующей структуры + +## Важные миграции + +### 019_identity_system_refactor.sql + +Комплексная миграция, объединяющая несколько предыдущих миграций (014-018) для улучшения системы идентификации пользователей: + +- Создание таблицы `guest_user_mapping` для связи гостевых идентификаторов с пользователями +- Добавление прямой связи между сообщениями и пользователями через поле `user_id` +- Очистка дублирующихся данных между таблицами `users` и `user_identities` +- Нормализация формата идентификаторов (приведение к нижнему регистру) +- Добавление ограничений и триггеров для поддержания целостности данных + +## Применение миграций + +При развертывании новой версии приложения миграции применяются автоматически через скрипт `backend/db/run-migrations.js`. Порядок применения определяется порядковым номером в имени файла. + +## Создание новых миграций + +1. **Именование**: Используйте следующий свободный порядковый номер и описательное имя +2. **Идемпотентность**: Миграции должны быть безопасны для повторного выполнения +3. **Проверки**: Добавляйте проверки существования объектов перед их созданием +4. **Тестирование**: Проверяйте миграцию на тестовой базе данных перед применением + +Пример правильной идемпотентной миграции: + +```sql +-- Создание таблицы, если она не существует +CREATE TABLE IF NOT EXISTS example_table ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL +); + +-- Добавление колонки, если она отсутствует +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'example_table' AND column_name = 'new_column' + ) THEN + ALTER TABLE example_table ADD COLUMN new_column INTEGER; + END IF; +END $$; +``` + +## Архивация устаревших миграций + +Устаревшие миграции, объединенные в более новые версии, перемещаются в папку `backend/db/migrations/archive/`. Для архивации используйте скрипт `backend/scripts/cleanup_migrations.sh`. + +## Диагностические функции + +Для проверки состояния базы данных и корректности миграций созданы следующие диагностические функции SQL: + +- `verify_identity_system()` - проверка состояния системы идентификации пользователей + +Пример использования: +```sql +SELECT * FROM verify_identity_system(); +``` \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 62c4f1a..cd817c2 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -3,23 +3,20 @@ const router = express.Router(); const crypto = require('crypto'); const db = require('../db'); const logger = require('../utils/logger'); -const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const { checkRole, requireAuth, auth } = require('../middleware/auth'); -const { pool } = require('../db'); const authService = require('../services/auth-service'); -const { SiweMessage } = require('siwe'); const emailBot = require('../services/emailBot'); const { verificationCodes } = require('../services/telegramBot'); -const { checkTokensAndUpdateRole } = require('../services/auth-service'); const { ethers } = require('ethers'); const { initTelegramAuth } = require('../services/telegramBot'); const emailAuth = require('../services/emailAuth'); const verificationService = require('../services/verification-service'); -const { processGuestMessages } = require('./chat'); // Импортируем функцию обработки гостевых сообщений -const nonceStore = {}; +const { processGuestMessages } = require('./chat'); +const identityService = require('../services/identity-service'); +const sessionService = require('../services/session-service'); -// Создайте лимитер для попыток аутентификации +// Создаем лимитер для попыток аутентификации const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 минут max: 20, @@ -59,20 +56,15 @@ router.get('/nonce', async (req, res) => { ); } - console.log(`Nonce ${nonce} сохранен для адреса ${address}`); + logger.info(`Nonce ${nonce} сохранен для адреса ${address}`); res.json({ nonce }); } catch (error) { - console.error('Error generating nonce:', error); + logger.error('Error generating nonce:', error); res.status(500).json({ error: 'Failed to generate nonce' }); } }); -// Минимальный ABI для проверки баланса ERC20 -const ERC20_ABI = [ - "function balanceOf(address owner) view returns (uint256)" -]; - // Верификация подписи и создание сессии router.post('/verify', async (req, res) => { try { @@ -84,132 +76,61 @@ router.post('/verify', async (req, res) => { const guestId = req.session.guestId; const previousGuestId = req.session.previousGuestId; - logger.info(`[verify] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); - // Проверяем подпись const isValid = await authService.verifySignature(message, signature, address); if (!isValid) { - logger.warn(`[verify] Invalid signature for address: ${address}`); return res.status(401).json({ success: false, error: 'Invalid signature' }); } // Проверяем nonce const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [address.toLowerCase()]); if (nonceResult.rows.length === 0 || nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]) { - logger.warn(`[verify] Invalid nonce for address: ${address}`); return res.status(401).json({ success: false, error: 'Invalid nonce' }); } - logger.info(`[verify] Signature and nonce verified for address: ${address}`); - // Находим или создаем пользователя - let userId, isAdmin; + const { userId, isAdmin } = await authService.findOrCreateUser(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.toLowerCase()]); + // Сохраняем идентификаторы + await identityService.saveIdentity(userId, 'wallet', address.toLowerCase(), true); - if (userResult.rows.length > 0) { - // Пользователь найден по кошельку - userId = userResult.rows[0].id; - isAdmin = userResult.rows[0].role === 'admin'; - logger.info(`[verify] Found existing user: ID=${userId}, isAdmin=${isAdmin}`); - } else if (req.session.guestId) { - // Проверяем, есть ли пользователь с текущим guestId - const guestUserResult = await db.query(` - SELECT u.* FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider = 'guest' AND ui.provider_id = $1 - `, [req.session.guestId]); - - if (guestUserResult.rows.length > 0) { - // Используем существующего пользователя с guestId - userId = guestUserResult.rows[0].id; - isAdmin = guestUserResult.rows[0].role === 'admin'; - logger.info(`[verify] Found user by guestId: ID=${userId}, isAdmin=${isAdmin}`); - - // Добавляем идентификатор кошелька к существующему пользователю - await saveUserIdentity(userId, 'wallet', address.toLowerCase(), true); - logger.info(`[verify] Added wallet identity ${address.toLowerCase()} to existing user ${userId}`); - } else { - // Создаем нового пользователя - const newUserResult = await db.query( - 'INSERT INTO users (role) VALUES ($1) RETURNING id', - ['user'] - ); - - userId = newUserResult.rows[0].id; - isAdmin = false; - logger.info(`[verify] Created new user: ID=${userId}`); - - // Добавляем идентификатор кошелька - await saveUserIdentity(userId, 'wallet', address.toLowerCase(), true); - logger.info(`[verify] Added wallet identity ${address.toLowerCase()} to new user ${userId}`); - - // Добавляем идентификатор гостя - await saveUserIdentity(userId, 'guest', req.session.guestId, true); - logger.info(`[verify] Added guest identity ${req.session.guestId} to new user ${userId}`); - } - } else { - // Создаем нового пользователя без гостевого ID - const newUserResult = await db.query( - 'INSERT INTO users (role) VALUES ($1) RETURNING id', - ['user'] - ); - - userId = newUserResult.rows[0].id; - isAdmin = false; - logger.info(`[verify] Created new user without guest ID: ID=${userId}`); - - // Добавляем идентификатор кошелька - await saveUserIdentity(userId, 'wallet', address.toLowerCase(), true); - logger.info(`[verify] Added wallet identity ${address.toLowerCase()} to new user ${userId}`); + if (guestId) { + await identityService.saveIdentity(userId, 'guest', guestId, true); + } + + if (previousGuestId && previousGuestId !== guestId) { + await identityService.saveIdentity(userId, 'guest', previousGuestId, true); } // Проверяем наличие админских токенов const adminStatus = await authService.checkAdminTokens(address.toLowerCase()); + if (adminStatus) { - isAdmin = true; await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); - logger.info(`[verify] Updated user ${userId} to admin role based on token check`); } // Обновляем сессию req.session.userId = userId; req.session.authenticated = true; req.session.authType = 'wallet'; - req.session.isAdmin = isAdmin; + req.session.isAdmin = adminStatus || isAdmin; req.session.address = address.toLowerCase(); // Удаляем временный ID delete req.session.tempUserId; - // Сохраняем сессию перед связыванием сообщений - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('[verify] Error saving session:', err); - reject(err); - } else { - logger.info(`[verify] Session saved successfully for user ${userId}`); - resolve(); - } - }); - }); + // Сохраняем сессию + await sessionService.saveSession(req.session); // Связываем гостевые сообщения с пользователем - const linkResults = await linkGuestMessagesAfterAuth(req.session, userId); - logger.info(`[verify] Guest messages linking results:`, linkResults); + await sessionService.linkGuestMessages(req.session, userId); // Возвращаем успешный ответ return res.json({ success: true, userId, address, - isAdmin, + isAdmin: adminStatus || isAdmin, authenticated: true }); @@ -231,8 +152,6 @@ router.post('/telegram/verify', async (req, res) => { }); } - logger.info(`[telegram/verify] Verifying Telegram auth for ID: ${telegramId}`); - // Сохраняем гостевой ID из текущей сессии const guestId = req.session.guestId; @@ -273,32 +192,11 @@ router.post('/telegram/verify', async (req, res) => { } // Сохраняем сессию - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('[telegram/verify] Error saving session:', err); - reject(err); - } else { - logger.info(`[telegram/verify] Session saved for user ${verificationResult.userId}`); - resolve(); - } - }); - }); + await sessionService.saveSession(req.session); - // Связываем гостевые сообщения только один раз - исправлено дублирование + // Связываем гостевые сообщения только один раз if (guestId) { - // Создаем объект сессии для совместимости с другими методами аутентификации - const session = { - guestId: guestId, - save: async (callback) => { - if (typeof callback === 'function') { - callback(null); - } - return Promise.resolve(); - } - }; - const linkResults = await linkGuestMessagesAfterAuth(session, verificationResult.userId); - logger.info(`[telegram/verify] Guest messages linking results:`, linkResults); + await sessionService.linkGuestMessages(req.session, verificationResult.userId); } return res.json({ @@ -327,25 +225,11 @@ router.post('/email/request', authLimiter, async (req, res) => { return res.status(400).json({ error: 'Invalid email format' }); } - // Используем общую логику инициализации email аутентификации - const { verificationCode } = await emailAuth.initEmailAuth(req.session, email); + // Инициализация email аутентификации + const result = await emailAuth.initEmailAuth(req.session, email); // Сохраняем сессию после установки pendingEmail - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('Error saving session:', err); - reject(err); - } else { - logger.info(`Session saved successfully with pendingEmail: ${email}`); - resolve(); - } - }); - }); - - // Отправляем email с кодом подтверждения - const emailBot = new emailBot(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD); - const result = await emailBot.sendVerificationCode(email, req.session.tempUserId || req.session.userId); + await sessionService.saveSession(req.session); if (result.success) { res.json({ @@ -364,50 +248,23 @@ router.post('/email/request', authLimiter, async (req, res) => { } }); -// Функция для проверки кода email -async function verifyEmailCode(code, email) { - try { - logger.info(`[verifyEmailCode] Verifying code ${code} for email ${email}`); - - // Получаем код из базы данных - const result = await db.query( - `SELECT * FROM verification_codes - WHERE code = $1 AND provider_id = $2 AND provider = 'email' - AND used = false AND expires_at > NOW()`, - [code, email] - ); - - if (result.rows.length === 0) { - logger.warn(`[verifyEmailCode] No valid code found for ${email}`); - return { success: false, error: 'Неверный или истекший код' }; - } - - // Помечаем код как использованный - await db.query( - 'UPDATE verification_codes SET used = true WHERE code = $1 AND provider_id = $2 AND provider = \'email\'', - [code, email] - ); - - logger.info(`[verifyEmailCode] Code verified successfully for ${email}`); - return { success: true }; - } catch (error) { - logger.error(`[verifyEmailCode] Error:`, error); - throw error; - } -} - // Маршрут для верификации email -router.post('/email/verify', async (req, res) => { +router.post('/email/verify-code', async (req, res) => { try { - const { code } = req.body; + const { code, email } = req.body; if (!code) { - return res.status(400).json({ - success: false, - error: 'Код подтверждения обязателен' + return res.status(400).json({ + success: false, + error: 'Код подтверждения обязателен' }); } + // Если email передан в запросе, сохраняем его в сессии + if (email && !req.session.pendingEmail) { + req.session.pendingEmail = email.toLowerCase(); + } + if (!req.session.pendingEmail) { return res.status(400).json({ success: false, @@ -415,11 +272,16 @@ router.post('/email/verify', async (req, res) => { }); } - // Сохраняем гостевые ID до проверки + // Сохраняем гостевой ID до проверки const guestId = req.session.guestId; + const previousGuestId = req.session.previousGuestId; // Проверяем код через сервис верификации - const verificationResult = await verifyEmailCode(code, req.session.pendingEmail); + const verificationResult = await verificationService.verifyCode( + code, + 'email', + req.session.pendingEmail + ); if (!verificationResult.success) { return res.status(400).json({ @@ -428,41 +290,60 @@ router.post('/email/verify', async (req, res) => { }); } + // Получаем или создаем пользователя let userId; + let isNewAuth = false; - // Если пользователь уже аутентифицирован, добавляем email к существующему аккаунту + // Проверяем, авторизован ли пользователь if (req.session.authenticated && req.session.userId) { + // Связываем email с существующим пользователем userId = req.session.userId; + logger.info(`[email/verify-code] Linking email ${req.session.pendingEmail} to existing authenticated user ${userId}`); - // Проверяем не связан ли email с другим аккаунтом - const existingEmail = await db.query(` - SELECT user_id FROM user_identities - WHERE provider = 'email' AND provider_id = $1 - `, [req.session.pendingEmail]); + // Связываем email с текущим аккаунтом + const linkResult = await authService.linkIdentity( + userId, + 'email', + req.session.pendingEmail + ); - if (existingEmail.rows.length > 0) { - return res.status(400).json({ - success: false, - error: 'Этот email уже связан с другим пользователем' - }); - } + // Сохраняем email в сессии + req.session.email = req.session.pendingEmail; - // Добавляем email к существующему пользователю - await saveUserIdentity(userId, 'email', req.session.pendingEmail, true); - logger.info(`[email/verify] Added email identity ${req.session.pendingEmail} to existing user ${userId}`); + // Удаляем временные данные + delete req.session.pendingEmail; + // Сохраняем сессию + await sessionService.saveSession(req.session); + + return res.json({ + success: true, + userId, + email: req.session.email, + authenticated: true, + linked: true + }); } else { - // Ищем существующего пользователя по email - const existingUser = await db.query(` - SELECT u.* FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider = 'email' AND ui.provider_id = $1 - `, [req.session.pendingEmail]); + // Если пользователь не авторизован, ищем существующего пользователя или создаем нового - if (existingUser.rows.length > 0) { + // Ищем существующего пользователя по email + const existingUser = await identityService.findUserByIdentity( + 'email', + req.session.pendingEmail + ); + + if (existingUser) { // Используем существующего пользователя - userId = existingUser.rows[0].id; - logger.info(`[email/verify] Found existing user with ID ${userId} for email ${req.session.pendingEmail}`); + userId = existingUser.id; + logger.info(`[email/verify-code] Using existing user ${userId} with email ${req.session.pendingEmail}`); + } else if (req.session.userId) { + // Используем текущего пользователя + userId = req.session.userId; + logger.info(`[email/verify-code] Using current user ${userId} for email ${req.session.pendingEmail}`); + } else if (req.session.tempUserId) { + // Используем временного пользователя + userId = req.session.tempUserId; + logger.info(`[email/verify-code] Using temporary user ${userId} for email ${req.session.pendingEmail}`); } else { // Создаем нового пользователя const newUser = await db.query( @@ -470,817 +351,46 @@ router.post('/email/verify', async (req, res) => { ['user'] ); userId = newUser.rows[0].id; - - // Добавляем email идентификатор - await saveUserIdentity(userId, 'email', req.session.pendingEmail, true); - logger.info(`[email/verify] Created new user with ID ${userId} for email ${req.session.pendingEmail}`); - } - } - - // Если есть гостевые сообщения, переносим их - if (guestId && !req.session.processedGuestIds?.includes(guestId)) { - await processGuestMessages(userId, guestId); - // Сохраняем обработанный guestId чтобы избежать повторной обработки - if (!req.session.processedGuestIds) { - req.session.processedGuestIds = []; - } - req.session.processedGuestIds.push(guestId); - logger.info(`[email/verify] Processed guest messages for user ${userId} with guest ID ${guestId}`); - } - - // Создаем новую сессию - req.session.regenerate(async (err) => { - if (err) { - logger.error('Error regenerating session:', err); - return res.status(500).json({ success: false, error: 'Server error' }); + isNewAuth = true; + logger.info(`[email/verify-code] Created new user ${userId} for email ${req.session.pendingEmail}`); } - // Устанавливаем данные новой сессии - req.session.authenticated = true; + // Сохраняем email как идентификатор + await identityService.saveIdentity(userId, 'email', req.session.pendingEmail, true); + + // Сохраняем гостевые идентификаторы + if (guestId) { + await identityService.saveIdentity(userId, 'guest', guestId, true); + } + + if (previousGuestId && previousGuestId !== guestId) { + await identityService.saveIdentity(userId, 'guest', previousGuestId, true); + } + + // Устанавливаем сессию req.session.userId = userId; - req.session.email = req.session.pendingEmail; + req.session.authenticated = true; req.session.authType = 'email'; - - // Сохраняем список обработанных гостевых ID - if (req.session.processedGuestIds?.length > 0) { - req.session.processedGuestIds = [...req.session.processedGuestIds]; - } + req.session.email = req.session.pendingEmail; // Удаляем временные данные delete req.session.tempUserId; - delete req.session.guestId; delete req.session.pendingEmail; // Сохраняем сессию - req.session.save((err) => { - if (err) { - logger.error('Error saving session:', err); - return res.status(500).json({ success: false, error: 'Server error' }); - } - - res.json({ - success: true, - userId, - email: req.session.email, - authenticated: true, - authType: 'email' - }); - }); - }); - - } catch (error) { - logger.error('[email/verify] Error:', error); - res.status(500).json({ success: false, error: 'Server error' }); - } -}); - -// Связывание аккаунтов -router.post('/link-identity', async (req, res) => { - try { - if (!req.session || !req.session.userId) { - return res.status(401).json({ error: 'Требуется аутентификация' }); - } - - const { identityType, identityValue } = req.body; - - // Проверяем, не привязан ли уже этот идентификатор к другому пользователю - const existingUserId = await authService.getUserIdByIdentity(identityType, identityValue); - - if (existingUserId && existingUserId !== req.session.userId) { - return res.status(400).json({ error: 'Этот идентификатор уже привязан к другому аккаунту' }); - } - - // Добавляем новый идентификатор - if (!existingUserId) { - await db.query( - 'INSERT INTO user_identities (user_id, identity_type, identity_value, created_at) VALUES ($1, $2, $3, NOW())', - [req.session.userId, identityType, identityValue] - ); - } - - // Если добавлен кошелек, проверяем токены - if (identityType === 'wallet') { - await authService.checkTokensAndUpdateRole(identityValue); - } - - // Получаем все идентификаторы пользователя - const identitiesResult = await db.query(` - SELECT identity_type, identity_value - FROM user_identities - WHERE user_id = $1 - `, [req.session.userId]); - const identities = identitiesResult.rows; - - // Получаем текущую роль - const isAdmin = await authService.isAdmin(req.session.userId); - - res.json({ - success: true, - identities, - isAdmin - }); - } catch (error) { - logger.error(`Link identity error: ${error.message}`); - res.status(500).json({ error: 'Ошибка сервера' }); - } -}); - -// Проверка статуса аутентификации -router.get('/check', async (req, res) => { - try { - logger.info(`[session/check] Checking session: ${req.sessionID}`); - - const authenticated = req.session.authenticated || false; - const authType = req.session.authType || null; - - // Подробное логирование для отладки восстановления сессии - logger.info(`[session/check] Session state: authenticated=${authenticated}, authType=${authType}, userId=${req.session.userId || 'none'}`); - - // Проверяем наличие идентификаторов в сессии - const sessionIdentities = []; - if (req.session.userId) sessionIdentities.push(`userId:${req.session.userId}`); - if (req.session.email) sessionIdentities.push(`email:${req.session.email}`); - if (req.session.address) sessionIdentities.push(`address:${req.session.address}`); - if (req.session.telegramId) sessionIdentities.push(`telegramId:${req.session.telegramId}`); - - logger.info(`[session/check] Identities in session: ${sessionIdentities.join(', ')}`); - - let identities = []; - let isAdmin = false; - - if (authenticated && req.session.userId) { - // Если пользователь аутентифицирован, получаем его идентификаторы из БД - try { - const identitiesResult = await db.query( - `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, - [req.session.userId] - ); - - identities = identitiesResult.rows; - logger.info(`[session/check] Found ${identities.length} identities in database for user ${req.session.userId}`); - - // Проверяем роль пользователя - const roleResult = await db.query( - 'SELECT role FROM users WHERE id = $1', - [req.session.userId] - ); - - if (roleResult.rows.length > 0) { - isAdmin = roleResult.rows[0].role === 'admin'; - req.session.isAdmin = isAdmin; - } - } catch (error) { - logger.error(`[session/check] Error fetching identities: ${error.message}`); - } - } - - // Проверяем, нужно ли создать новый гостевой ID - if (!authenticated && !req.session.guestId) { - req.session.guestId = crypto.randomBytes(16).toString('hex'); - logger.info(`[session/check] Created new guest ID: ${req.session.guestId}`); + await sessionService.saveSession(req.session); - // Сохраняем сессию с новым гостевым ID - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('[session/check] Error saving session with new guest ID:', err); - reject(err); - } else { - logger.info('[session/check] Session with new guest ID saved successfully'); - resolve(); - } - }); - }); - } - - // Формируем ответ - const response = { - success: true, - authenticated, - userId: req.session.userId || null, - guestId: req.session.guestId || null, - authType, - identitiesCount: identities.length, - isAdmin: isAdmin || false - }; - - // Добавляем специфические поля в зависимости от типа аутентификации - if (authType === 'wallet') { - response.address = req.session.address || null; - } else if (authType === 'email') { - response.email = req.session.email || null; - } else if (authType === 'telegram') { - response.telegramId = req.session.telegramId || null; - if (req.session.telegramUsername) { - response.telegramUsername = req.session.telegramUsername; - } - if (req.session.telegramFirstName) { - response.telegramFirstName = req.session.telegramFirstName; - } - } - - logger.info(`[session/check] Session check complete: authenticated=${authenticated}, authType=${authType}`); - return res.json(response); - } catch (error) { - logger.error('[session/check] Error:', error); - return res.status(500).json({ - success: false, - error: 'Internal server error' - }); - } -}); - -// Выход из системы -router.post('/logout', async (req, res) => { - try { - // Очищаем все идентификаторы сессии - req.session.authenticated = false; - req.session.userId = null; - req.session.address = null; - req.session.telegramId = null; - req.session.email = null; - req.session.isAdmin = false; - req.session.guestId = null; - req.session.previousGuestId = null; - req.session.processedGuestIds = []; - req.session.pendingEmail = null; - req.session.authType = null; - - // Сохраняем изменения в сессии - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('[logout] Error saving session:', err); - reject(err); - } else { - logger.info('[logout] Session cleared successfully'); - resolve(); - } - }); - }); - - // Уничтожаем сессию полностью - req.session.destroy((err) => { - if (err) { - logger.error('[logout] Error destroying session:', err); - return res.status(500).json({ success: false, error: 'Error during logout' }); - } - res.clearCookie('connect.sid'); - res.json({ success: true, message: 'Logged out successfully' }); - }); - } catch (error) { - logger.error('[logout] Error:', error); - res.status(500).json({ success: false, error: 'Internal server error during logout' }); - } -}); - -// Маршрут для авторизации через Telegram -router.get('/telegram', (req, res) => { - // Генерируем случайный токен для авторизации - const token = crypto.randomBytes(32).toString('hex'); - - // Сохраняем токен в сессии - req.session.telegramToken = token; - - // Создаем URL для авторизации через Telegram - const botName = process.env.TELEGRAM_BOT_NAME || 'YourBotName'; - const authUrl = `https://t.me/${botName}?start=${token}`; - - res.json({ authUrl }); -}); - -// Маршрут для получения кода подтверждения Telegram -router.get('/telegram/code', authLimiter, async (req, res) => { - try { - // Создаем код через сервис телеграм авторизации - const authData = await initTelegramAuth(req.session); - - if (!authData.verificationCode) { - return res.status(500).json({ - success: false, - error: 'Failed to generate verification code' - }); - } - - res.json({ - success: true, - message: 'Отправьте этот код боту @' + process.env.TELEGRAM_BOT_USERNAME, - code: authData.verificationCode, - botUsername: process.env.TELEGRAM_BOT_USERNAME || 'YourDAppBot' - }); - } catch (error) { - logger.error(`Error in telegram code request: ${error.message}`); - res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); - } -}); - -// Функция для проверки кода Telegram -async function verifyTelegramCode(code) { - try { - // Используем глобальное хранилище кодов - const verificationCodes = global.verificationCodes; - - if (!verificationCodes) { - return { success: false, error: 'Система верификации не инициализирована' }; - } - - // Ищем chatId по коду - for (const [chatId, data] of verificationCodes.entries()) { - if (data.code === code) { - // Проверяем срок действия - if (Date.now() > data.expires) { - verificationCodes.delete(chatId); - return { success: false, error: 'Код истек' }; - } - - // Код верный и не истек - const telegramId = chatId; - verificationCodes.delete(chatId); - return { - success: true, - telegramId: telegramId - }; - } - } - return { success: false, error: 'Неверный код' }; - } catch (error) { - console.error('Error in verifyTelegramCode:', error); - throw error; - } -} - -// Функция для проверки баланса токенов -async function checkTokenBalance(address) { - try { - const authService = require('../services/auth-service'); - const isAdmin = await authService.checkTokensAndUpdateRole(address); - return isAdmin; - } catch (error) { - console.error('Error checking token balance:', error); - return false; - } -} - -// Маршрут для связывания разных идентификаторов -router.post('/link-identity', requireAuth, async (req, res) => { - try { - const { type, value } = req.body; - const userId = req.session.userId; - - // Проверяем валидность типа - if (!['wallet', 'email', 'telegram'].includes(type)) { - return res.status(400).json({ - success: false, - error: 'Неподдерживаемый тип идентификатора' - }); - } - - // Проверяем, не связан ли идентификатор с другим пользователем - const existingResult = await db.query(` - SELECT ui.user_id - FROM user_identities ui - WHERE ui.identity_type = $1 AND ui.identity_value = $2 - `, [type, value]); - - if (existingResult.rows.length > 0 && existingResult.rows[0].user_id !== userId) { - return res.status(400).json({ - success: false, - error: 'Этот идентификатор уже связан с другим аккаунтом' - }); - } - - // Добавляем или обновляем идентификатор - await db.query(` - INSERT INTO user_identities (user_id, identity_type, identity_value, created_at, verified) - VALUES ($1, $2, $3, NOW(), true) - ON CONFLICT (identity_type, identity_value) - DO UPDATE SET verified = true - `, [userId, type, value]); - - // Если связываем кошелек, обновляем также поле address в таблице users - if (type === 'wallet') { - await db.query('UPDATE users SET address = $1 WHERE id = $2', [value, userId]); + // Связываем гостевые сообщения + await sessionService.linkGuestMessages(req.session, userId); - // Проверяем наличие токенов для статуса админа - const isAdmin = await authService.checkAdminTokens(value); - if (isAdmin) { - await db.query('UPDATE users SET is_admin = true WHERE id = $1', [userId]); - req.session.isAdmin = true; - } - - req.session.address = value; - } - - // Если связываем email, обновляем сессию - if (type === 'email') { - req.session.email = value; - } - - // Если связываем telegram, обновляем сессию - if (type === 'telegram') { - req.session.telegramId = value; - } - - // Сохраняем сессию - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) reject(err); - else resolve(); - }); - }); - - res.json({ - success: true, - message: `Идентификатор успешно связан с вашим аккаунтом`, - isAdmin: req.session.isAdmin - }); - } catch (error) { - logger.error(`Error linking identity: ${error.message}`); - res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); - } -}); - -// Добавляем маршрут для проверки прав доступа -router.get('/check-access', requireAuth, async (req, res) => { - try { - const userId = req.session.userId; - const address = req.session.address; - - if (address) { - const isAdmin = await checkTokensAndUpdateRole(address); - - // Обновляем сессию - req.session.isAdmin = isAdmin; - - return res.json({ + return res.json({ success: true, - isAdmin, userId, - address + email: req.session.email, + authenticated: true, + isNewAuth }); } - - return res.json({ - success: true, - isAdmin: false, - userId, - address: null - }); - - } catch (error) { - logger.error('Error checking access:', error); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Обновление сессии -router.post('/refresh-session', async (req, res) => { - try { - const { address } = req.body; - - if (req.session && req.session.authenticated) { - console.log('Обновление сессии для пользователя:', req.session.userId); - - // Обновляем время жизни сессии - req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 дней - - // Сохраняем обновленную сессию - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - console.error('Ошибка при сохранении сессии:', err); - reject(err); - } else { - console.log('Сессия успешно обновлена'); - resolve(); - } - }); - }); - - return res.json({ success: true }); - } else if (address) { - // Если сессия не аутентифицирована, но есть адрес - try { - const { pool } = require('../db'); - const result = await pool.query('SELECT * FROM users WHERE address = $1', [address]); - - if (result.rows.length > 0) { - const user = result.rows[0]; - - // Обновляем сессию - req.session.authenticated = true; - req.session.userId = user.id; - req.session.address = address; - req.session.isAdmin = user.is_admin; - req.session.authType = 'wallet'; - - // Сохраняем обновленную сессию - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - console.error('Ошибка при сохранении сессии:', err); - reject(err); - } else { - console.log('Сессия успешно обновлена'); - resolve(); - } - }); - }); - - return res.json({ success: true }); - } - } catch (error) { - console.error('Ошибка при проверке пользователя:', error); - } - } - - // Если не удалось обновить сессию, возвращаем успех=false, но не ошибку - return res.json({ success: false }); - } catch (error) { - console.error('Ошибка при обновлении сессии:', error); - res.status(500).json({ error: 'Ошибка сервера' }); - } -}); - -// Маршрут для обновления статуса администратора -router.post('/update-admin-status', async (req, res) => { - try { - const { address, isAdmin } = req.body; - - if (!address) { - return res.status(400).json({ error: 'Address is required' }); - } - - console.log(`Запрос на обновление статуса администратора для адреса ${address} на ${isAdmin}`); - - // Проверяем, существует ли пользователь - const userResult = await db.query('SELECT * FROM users WHERE address = $1', [ - address.toLowerCase(), - ]); - - if (userResult.rows.length === 0) { - // Если пользователь не найден, создаем его - await db.query('INSERT INTO users (address, is_admin, created_at) VALUES ($1, $2, NOW())', [ - address.toLowerCase(), - isAdmin, - ]); - - console.log( - `Создан новый пользователь с адресом ${address} и статусом администратора ${isAdmin}` - ); - } else { - // Если пользователь найден, обновляем его статус - await db.query('UPDATE users SET is_admin = $1 WHERE address = $2', [ - isAdmin, - address.toLowerCase(), - ]); - - console.log( - `Создан новый пользователь с адресом ${address} и статусом администратора ${isAdmin}` - ); - } - - res.json({ success: true }); - } catch (error) { - console.error('Ошибка при обновлении статуса администратора:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Маршрут для создания токена авторизации через Email -router.post('/email/auth-token', async (req, res) => { - try { - const { email } = req.body; - - if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { - return res.status(400).json({ success: false, error: 'Неверный формат email' }); - } - - // Генерируем уникальный токен - const token = crypto.randomBytes(20).toString('hex'); - - // Получаем ID пользователя из сессии или создаем нового гостевого пользователя - let userId; - - if (req.session.authenticated && req.session.userId) { - // Если пользователь уже аутентифицирован, используем его ID - userId = req.session.userId; - } else { - // Создаем временного пользователя - const userResult = await db.query( - 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' - ); - userId = userResult.rows[0].id; - - // Сохраняем ID в сессии как временный - req.session.tempUserId = userId; - } - - // Сохраняем токен в базе данных - await db.query(` - INSERT INTO email_auth_tokens (user_id, token, created_at, expires_at) - VALUES ($1, $2, NOW(), NOW() + INTERVAL '15 minutes') - `, [userId, token]); - - // Отправляем email с кодом подтверждения через emailBot - const emailBot = require('../services/emailBot'); - const emailService = new emailBot(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD); - const sendResult = await emailService.sendVerificationCode(email, token); - - if (sendResult.success) { - res.json({ - success: true, - message: 'Код подтверждения отправлен на ваш email' - }); - } else { - res.status(500).json({ success: false, error: 'Ошибка отправки email' }); - } - } catch (error) { - logger.error(`Error creating Email auth token: ${error.message}`); - res.status(500).json({ success: false, error: 'Ошибка сервера' }); - } -}); - -// Маршрут для проверки статуса аутентификации через Email -router.get('/email/auth-status/:token', async (req, res) => { - try { - const { token } = req.params; - - // Проверяем статус токена - const tokenResult = await db.query(` - SELECT user_id, used FROM email_auth_tokens - WHERE token = $1 AND expires_at > NOW() - `, [token]); - - if (tokenResult.rows.length === 0) { - return res.json({ success: false, error: 'Токен не найден или истек' }); - } - - const userId = tokenResult.rows[0].user_id; - const isAuthenticated = tokenResult.rows[0].used; - - if (isAuthenticated) { - // Токен использован, email подключен - - // Получаем email пользователя - const emailResult = await db.query(` - SELECT ui.identity_value FROM user_identities ui - WHERE ui.user_id = $1 AND ui.identity_type = 'email' - `, [userId]); - - if (emailResult.rows.length > 0) { - // Устанавливаем полную аутентификацию в сессии - req.session.authenticated = true; - req.session.userId = userId; - req.session.email = emailResult.rows[0].identity_value; - req.session.authType = 'email'; - - // Если был временный ID, удаляем его - if (req.session.tempUserId) { - delete req.session.tempUserId; - } - - // Сохраняем сессию - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) reject(err); - else resolve(); - }); - }); - } - } - - res.json({ - success: true, - authenticated: isAuthenticated - }); - } catch (error) { - logger.error(`Error checking Email auth status: ${error.message}`); - res.status(500).json({ success: false, error: 'Ошибка сервера' }); - } -}); - -// Маршрут для проверки кода email -router.post('/email/verify-code', async (req, res) => { - try { - const { email, code } = req.body; - - if (!email || !code) { - return res.status(400).json({ - success: false, - error: 'Email и код подтверждения обязательны' - }); - } - - logger.info(`[email/verify-code] Verifying code for email: ${email}`); - - // Сохраняем гостевой ID до проверки - const guestId = req.session.guestId; - const previousGuestId = req.session.previousGuestId; - - logger.info(`[email/verify-code] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); - - // Проверяем существование пользователя с таким email - const userResult = await db.query( - `SELECT u.id FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider = $1 AND ui.provider_id = $2`, - ['email', email.toLowerCase()] - ); - - let userId; - let isNewUser = false; - - if (userResult.rows.length > 0) { - // Пользователь уже существует - userId = userResult.rows[0].id; - logger.info(`[email/verify-code] Found existing user with ID ${userId}`); - } else if (req.session.tempUserId) { - // Используем временный ID пользователя - userId = req.session.tempUserId; - logger.info(`[email/verify-code] Using tempUserId ${userId}`); - } else { - // Создаем нового пользователя - const newUserResult = await db.query( - 'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id', - ['user'] - ); - userId = newUserResult.rows[0].id; - isNewUser = true; - logger.info(`[email/verify-code] Created new user with ID ${userId} for email ${email}`); - } - - // Проверяем код верификации - const verification = await verificationService.verifyCode( - code.toUpperCase(), - 'email', - email.toLowerCase() - ); - - if (!verification.success) { - logger.warn(`[email/verify-code] Invalid verification code for ${email}: ${verification.error}`); - return res.status(400).json({ - success: false, - error: verification.error - }); - } - - logger.info(`[email/verify-code] Verification successful for email ${email}, user ${userId}`); - - // Сохраняем email как identity - await saveUserIdentity(userId, 'email', email.toLowerCase(), true); - logger.info(`[email/verify-code] Saved email identity ${email} for user ${userId}`); - - // Если есть гостевой ID, сохраняем его - if (guestId) { - await saveUserIdentity(userId, 'guest', guestId, true); - logger.info(`[email/verify-code] Saved guest ID ${guestId} for user ${userId}`); - } - - if (previousGuestId && previousGuestId !== guestId) { - await saveUserIdentity(userId, 'guest', previousGuestId, true); - logger.info(`[email/verify-code] Saved previous guest ID ${previousGuestId} for user ${userId}`); - } - - // Устанавливаем данные сессии - req.session.authenticated = true; - req.session.userId = userId; - req.session.authType = 'email'; - req.session.email = email.toLowerCase(); - - // Удаляем временный ID - delete req.session.tempUserId; - delete req.session.pendingEmail; - - // Сохраняем сессию перед связыванием сообщений - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('[email/verify-code] Error saving session:', err); - reject(err); - } else { - logger.info(`[email/verify-code] Session saved successfully for user ${userId}`); - resolve(); - } - }); - }); - - // Сначала сохраняем сессию с актуальным гостевым ID - if (guestId) { - req.session.guestId = guestId; - } - - // Связываем гостевые сообщения с пользователем - const linkResults = await linkGuestMessagesAfterAuth(req.session, userId); - logger.info(`[email/verify-code] Guest messages linking results:`, linkResults); - - return res.json({ - success: true, - userId, - email: email.toLowerCase(), - authenticated: true - }); - } catch (error) { logger.error('[email/verify-code] Error:', error); return res.status(500).json({ @@ -1290,23 +400,6 @@ router.post('/email/verify-code', async (req, res) => { } }); -// Маршрут для очистки сессии -router.post('/clear-session', async (req, res) => { - try { - // Очищаем все данные сессии - req.session.destroy((err) => { - if (err) { - console.error('Error destroying session:', err); - return res.status(500).json({ error: 'Internal server error' }); - } - res.json({ success: true }); - }); - } catch (error) { - console.error('Error clearing session:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - // Инициализация Telegram аутентификации router.post('/telegram/init', async (req, res) => { try { @@ -1350,80 +443,16 @@ router.post('/email/init', async (req, res) => { }); } - logger.info(`[email/init] Initializing email authentication for: ${email}`); + // Инициализация email аутентификации + const result = await emailAuth.initEmailAuth(req.session, email); - // Сохраняем гостевой ID до проверки - const guestId = req.session.guestId; - const previousGuestId = req.session.previousGuestId; - - logger.info(`[email/init] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); - - // Проверяем, существует ли пользователь с таким email - const existingUserResult = await db.query( - `SELECT u.id FROM users u - JOIN user_identities ui ON u.id = ui.user_id - WHERE ui.provider = $1 AND ui.provider_id = $2`, - ['email', email.toLowerCase()] - ); - - let userId; - if (existingUserResult.rows.length > 0) { - // Используем существующего пользователя - userId = existingUserResult.rows[0].id; - logger.info(`[email/init] Found existing user with ID ${userId} for email ${email}`); - } else if (req.session.authenticated && req.session.userId) { - // Используем текущего аутентифицированного пользователя - userId = req.session.userId; - logger.info(`[email/init] Using current authenticated user with ID ${userId}`); - } else { - // Создаем нового пользователя - const userResult = await db.query( - 'INSERT INTO users (role) VALUES ($1) RETURNING id', - ['user'] - ); - - userId = userResult.rows[0].id; - req.session.tempUserId = userId; - logger.info(`[email/init] Created new user with ID ${userId} for email ${email}`); - } - - // Сохраняем email в сессии - req.session.pendingEmail = email.toLowerCase(); - - // Генерируем код верификации - const code = await verificationService.createVerificationCode('email', email.toLowerCase(), userId); - - // Инициализируем верификацию через email бот - const result = await emailBot.initEmailVerification(email, userId, code); - - if (!result.success) { - return res.status(500).json({ - success: false, - error: 'Ошибка при отправке кода верификации' - }); - } - // Сохраняем сессию - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('Error saving session:', err); - reject(err); - } else { - logger.info(`Session saved successfully with pendingEmail: ${email}`); - resolve(); - } - }); - }); - - // Связываем гостевые сообщения с пользователем - await linkGuestMessagesAfterAuth(req.session, userId); - + await sessionService.saveSession(req.session); + return res.json({ success: true, message: 'Код верификации отправлен на email' }); - } catch (error) { logger.error('Error in email auth initialization:', error); res.status(500).json({ @@ -1433,282 +462,193 @@ router.post('/email/init', async (req, res) => { } }); -// Проверка кода подтверждения email -router.post('/email/verify', requireAuth, async (req, res) => { +// Проверка статуса аутентификации +router.get('/check', async (req, res) => { try { - const { code } = req.body; - if (!code) { - return res.status(400).json({ error: 'Verification code is required' }); - } - - const result = await emailAuth.checkEmailVerification(code); - res.json(result); - } catch (error) { - console.error('Error verifying email code:', error); - res.status(400).json({ error: error.message }); - } -}); - -// Проверка кода верификации email -router.post('/email/check-verification', async (req, res) => { - try { - const { code } = req.body; + const authenticated = req.session.authenticated || false; + const authType = req.session.authType || null; - if (!code) { - return res.status(400).json({ - success: false, - message: 'Код верификации не предоставлен' - }); - } + let identities = []; + let isAdmin = false; - // Сохраняем гостевой ID до проверки кода - const guestId = req.session.guestId; - const previousGuestId = req.session.previousGuestId; - - logger.info(`[email/check-verification] Checking verification with code ${code}, guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); - - // Проверяем код через сервис верификации - const result = await emailAuth.checkEmailVerification(code, req.session); - - if (!result.verified) { - logger.warn(`[email/check-verification] Invalid code: ${result.message}`); - // Преобразуем ответ для совместимости с фронтендом - return res.json({ - success: false, - message: result.message - }); - } - - logger.info(`[email/check-verification] Email verification successful for userId: ${result.userId}, email: ${result.email}`); - - // Код верный, обновляем сессию - req.session.authenticated = true; - req.session.userId = result.userId; - req.session.authType = 'email'; - req.session.email = result.email; - - // Восстанавливаем гостевой ID, если он был потерян в процессе верификации - if (!req.session.guestId && guestId) { - req.session.guestId = guestId; - logger.info(`[email/check-verification] Restored guestId ${guestId}`); - } - - if (!req.session.previousGuestId && previousGuestId) { - req.session.previousGuestId = previousGuestId; - logger.info(`[email/check-verification] Restored previous guest ID ${previousGuestId}`); - } - - // Получаем роль пользователя - const roleResult = await db.query( - 'SELECT role FROM users WHERE id = $1', - [result.userId] - ); - - if (roleResult.rows.length > 0) { - req.session.userRole = roleResult.rows[0].role || 'user'; - logger.info(`[email/check-verification] User role: ${req.session.userRole}`); - } else { - req.session.userRole = 'user'; - logger.info(`[email/check-verification] User role not found, setting default: user`); - } - - // Явно добавляем email в таблицу user_identities - await saveUserIdentity(result.userId, 'email', result.email.toLowerCase(), true); - logger.info(`[email/check-verification] Email identity ${result.email} saved for user ${result.userId}`); - - // Сохраняем сессию - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('[email/check-verification] Error saving session:', err); - reject(err); - } else { - logger.info(`[email/check-verification] Session saved successfully for email ${result.email}`); - resolve(); + if (authenticated && req.session.userId) { + // Если пользователь аутентифицирован, получаем его идентификаторы из БД + try { + identities = await identityService.getUserIdentities(req.session.userId); + + // Проверяем роль пользователя + const roleResult = await db.query( + 'SELECT role FROM users WHERE id = $1', + [req.session.userId] + ); + + if (roleResult.rows.length > 0) { + isAdmin = roleResult.rows[0].role === 'admin'; + req.session.isAdmin = isAdmin; } - }); - }); - - // Связываем гостевые сообщения с пользователем - const linkResults = await linkGuestMessagesAfterAuth(req.session, result.userId); - logger.info(`[email/check-verification] Guest messages linking results:`, linkResults); - - return res.json({ - success: true, - userId: result.userId, - email: result.email - }); - } catch (error) { - logger.error('[email/check-verification] Error:', error); - return res.status(500).json({ - success: false, - message: 'Ошибка при проверке кода верификации' - }); - } -}); - -// Маршрут для имитации отправки email -router.post('/email/send', async (req, res) => { - try { - const { code } = req.body; - - if (!code) { - return res.status(400).json({ error: 'Code is required' }); + } catch (error) { + logger.error(`[session/check] Error fetching identities: ${error.message}`); + } } - const result = await emailAuth.markEmailAsSent(code); - - if (result.success) { - return res.json({ success: true }); - } else { - return res.status(400).json({ error: result.message }); - } - } catch (error) { - console.error('Error marking email as sent:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// Обертка для функции processGuestMessages -async function processGuestMessagesWrapper(userId, guestId) { - try { - logger.info(`[processGuestMessagesWrapper] Processing messages: userId=${userId}, guestId=${guestId}`); - return await processGuestMessages(userId, guestId); - } catch (error) { - logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error); - throw error; - } -} - -// Функция для сохранения идентификатора пользователя в базу данных -async function saveUserIdentity(userId, provider, providerId, verified = true) { - try { - logger.info(`[saveUserIdentity] Saving identity for user ${userId}: ${provider}:${providerId}`); - - // Проверяем, существует ли уже такой идентификатор - const existingResult = await db.query( - `SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, - [provider, providerId] - ); - - if (existingResult.rows.length > 0) { - const existingUserId = existingResult.rows[0].user_id; + // Проверяем, нужно ли создать новый гостевой ID + if (!authenticated && !req.session.guestId) { + req.session.guestId = crypto.randomBytes(16).toString('hex'); - // Если идентификатор уже принадлежит этому пользователю, ничего не делаем - if (existingUserId === userId) { - logger.info(`[saveUserIdentity] Identity ${provider}:${providerId} already exists for user ${userId}`); - } else { - // Если идентификатор принадлежит другому пользователю, логируем это - logger.warn(`[saveUserIdentity] Identity ${provider}:${providerId} already belongs to user ${existingUserId}, not user ${userId}`); - return { - success: false, - error: `Identity already belongs to another user (${existingUserId})` - }; - } - } else { - // Создаем новую запись - await db.query( - `INSERT INTO user_identities (user_id, provider, provider_id) - VALUES ($1, $2, $3)`, - [userId, provider, providerId] - ); - logger.info(`[saveUserIdentity] Created new identity ${provider}:${providerId} for user ${userId}`); + // Сохраняем сессию с новым гостевым ID + await sessionService.saveSession(req.session); } - return { success: true }; - } catch (error) { - logger.error(`[saveUserIdentity] Error saving identity ${provider}:${providerId} for user ${userId}:`, error); - return { success: false, error: error.message }; - } -} - -// Функция для связывания гостевых сообщений после аутентификации -async function linkGuestMessagesAfterAuth(session, userId) { - try { - logger.info(`[linkGuestMessagesAfterAuth] Starting for user ${userId} with guestId=${session.guestId}, previousGuestId=${session.previousGuestId}`); - - // Инициализируем массив обработанных гостевых ID, если его нет - if (!session.processedGuestIds) { - session.processedGuestIds = []; - logger.info('[linkGuestMessagesAfterAuth] Initialized processedGuestIds array for session'); - } - - // Получаем все гостевые ID для текущего пользователя - const guestIdsResult = await db.query( - 'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2', - [userId, 'guest'] - ); - const userGuestIds = guestIdsResult.rows.map(row => row.provider_id); - - // Обрабатываем текущий гостевой ID - if (session.guestId && !session.processedGuestIds.includes(session.guestId)) { - logger.info(`[linkGuestMessagesAfterAuth] Processing current guest ID ${session.guestId} for user ${userId}`); - await processGuestMessagesWrapper(userId, session.guestId); - session.processedGuestIds.push(session.guestId); - } - - // Обрабатываем предыдущий гостевой ID - if (session.previousGuestId && !session.processedGuestIds.includes(session.previousGuestId)) { - logger.info(`[linkGuestMessagesAfterAuth] Processing previous guest ID ${session.previousGuestId} for user ${userId}`); - await processGuestMessagesWrapper(userId, session.previousGuestId); - session.processedGuestIds.push(session.previousGuestId); - } - - // Обрабатываем все гостевые ID пользователя - for (const guestId of userGuestIds) { - if (!session.processedGuestIds.includes(guestId)) { - logger.info(`[linkGuestMessagesAfterAuth] Processing user's guest ID ${guestId} for user ${userId}`); - await processGuestMessagesWrapper(userId, guestId); - session.processedGuestIds.push(guestId); - } - } - - // Сохраняем сессию - await new Promise((resolve, reject) => { - session.save(err => { - if (err) { - logger.error('[linkGuestMessagesAfterAuth] Error saving session:', err); - reject(err); - } else { - logger.info('[linkGuestMessagesAfterAuth] Session saved successfully after guest ID processing'); - resolve(); - } - }); - }); - - return { success: true }; - } catch (error) { - logger.error('[linkGuestMessagesAfterAuth] Error:', error); - return { success: false, error: error.message }; - } -} - -// Маршрут для получения всех идентификаторов пользователя -router.get('/identities', requireAuth, async (req, res) => { - try { - const { userId } = req.session; - - // Получаем все идентификаторы пользователя - const identitiesResult = await db.query( - `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, - [userId] - ); - - const identities = identitiesResult.rows; - - res.json({ + // Формируем ответ + const response = { success: true, - identities - }); + authenticated, + userId: req.session.userId || null, + guestId: req.session.guestId || null, + authType, + identitiesCount: identities.length, + isAdmin: isAdmin || false + }; + + // Добавляем специфические поля в зависимости от типа аутентификации + if (authType === 'wallet') { + response.address = req.session.address || null; + } else if (authType === 'email') { + response.email = req.session.email || null; + } else if (authType === 'telegram') { + response.telegramId = req.session.telegramId || null; + if (req.session.telegramUsername) { + response.telegramUsername = req.session.telegramUsername; + } + if (req.session.telegramFirstName) { + response.telegramFirstName = req.session.telegramFirstName; + } + } + + return res.json(response); } catch (error) { - logger.error('Error getting user identities:', error); - res.status(500).json({ + logger.error('[session/check] Error:', error); + return res.status(500).json({ success: false, error: 'Internal server error' }); } }); +// Выход из системы +router.post('/logout', async (req, res) => { + try { + // Очищаем все идентификаторы сессии + req.session.authenticated = false; + req.session.userId = null; + req.session.address = null; + req.session.telegramId = null; + req.session.email = null; + req.session.isAdmin = false; + req.session.guestId = null; + req.session.previousGuestId = null; + req.session.processedGuestIds = []; + req.session.pendingEmail = null; + req.session.authType = null; + + // Сохраняем изменения в сессии + await sessionService.saveSession(req.session); + + // Уничтожаем сессию полностью + req.session.destroy((err) => { + if (err) { + logger.error('[logout] Error destroying session:', err); + return res.status(500).json({ success: false, error: 'Error during logout' }); + } + res.clearCookie('connect.sid'); + res.json({ success: true, message: 'Logged out successfully' }); + }); + } catch (error) { + logger.error('[logout] Error:', error); + res.status(500).json({ success: false, error: 'Internal server error during logout' }); + } +}); + +// Маршрут для проверки и обновления статуса администратора +router.get('/check-access', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const address = req.session.address; + + if (address) { + const isAdmin = await authService.checkAdminTokens(address); + + // Обновляем сессию + req.session.isAdmin = isAdmin; + await sessionService.saveSession(req.session); + + return res.json({ + success: true, + isAdmin, + userId, + address + }); + } + + return res.json({ + success: true, + isAdmin: false, + userId, + address: null + }); + + } catch (error) { + logger.error('Error checking access:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Обновление сессии +router.post('/refresh-session', async (req, res) => { + try { + const { address } = req.body; + + if (req.session && req.session.authenticated) { + logger.info('Обновление сессии для пользователя:', req.session.userId); + + // Обновляем время жизни сессии + req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 дней + + // Сохраняем обновленную сессию + await sessionService.saveSession(req.session); + + return res.json({ success: true }); + } else if (address) { + // Если сессия не аутентифицирована, но есть адрес + try { + const user = await identityService.findUserByIdentity('wallet', address.toLowerCase()); + + if (user) { + // Обновляем сессию + req.session.authenticated = true; + req.session.userId = user.id; + req.session.address = address.toLowerCase(); + req.session.isAdmin = user.role === 'admin'; + req.session.authType = 'wallet'; + + // Сохраняем обновленную сессию + await sessionService.saveSession(req.session); + + return res.json({ success: true }); + } + } catch (error) { + logger.error('Ошибка при проверке пользователя:', error); + } + } + + // Если не удалось обновить сессию, возвращаем успех=false, но не ошибку + return res.json({ success: false }); + } catch (error) { + logger.error('Ошибка при обновлении сессии:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + // Аутентификация через wallet router.post('/wallet', async (req, res) => { try { @@ -1721,13 +661,9 @@ router.post('/wallet', async (req, res) => { }); } - logger.info(`[wallet] Authentication request for address: ${address}`); - // Сохраняем гостевые ID до аутентификации const guestId = req.session.guestId; const previousGuestId = req.session.previousGuestId; - - logger.info(`[wallet] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); // Формируем сообщение для проверки const message = `Sign this message to authenticate with HB3 DApp: ${nonce}`; @@ -1735,49 +671,35 @@ router.post('/wallet', async (req, res) => { // Проверяем подпись const validSignature = await authService.verifySignature(message, signature, address); if (!validSignature) { - logger.warn(`[wallet] Invalid signature for address: ${address}`); return res.status(401).json({ success: false, error: 'Invalid signature' }); } - logger.info(`[wallet] Valid signature from address: ${address}`); - // Получаем или создаем пользователя const { userId } = await authService.findOrCreateUser(address); - logger.info(`[wallet] User ID for address ${address}: ${userId}`); // Проверяем наличие админских токенов const isAdmin = await authService.checkAdminTokens(address); - logger.info(`[wallet] Admin status for ${address}: ${isAdmin}`); // Обновляем роль пользователя в базе данных, если нужно if (isAdmin) { - try { - await db.query( - 'UPDATE users SET role = $1 WHERE id = $2', - ['admin', userId] - ); - logger.info(`[wallet] Updated user ${userId} role to admin`); - } catch (updateError) { - logger.error(`[wallet] Error updating user role:`, updateError); - } + await db.query( + 'UPDATE users SET role = $1 WHERE id = $2', + ['admin', userId] + ); } - // Явно сохраняем wallet адрес как идентификатор пользователя - await saveUserIdentity(userId, 'wallet', address.toLowerCase(), true); - logger.info(`[wallet] Saved wallet identity ${address.toLowerCase()} for user ${userId}`); + // Сохраняем идентификаторы + await identityService.saveIdentity(userId, 'wallet', address.toLowerCase(), true); - // Если есть гостевые ID, сохраняем их if (guestId) { - await saveUserIdentity(userId, 'guest', guestId, true); - logger.info(`[wallet] Saved guest ID ${guestId} for user ${userId}`); + await identityService.saveIdentity(userId, 'guest', guestId, true); } if (previousGuestId && previousGuestId !== guestId) { - await saveUserIdentity(userId, 'guest', previousGuestId, true); - logger.info(`[wallet] Saved previous guest ID ${previousGuestId} for user ${userId}`); + await identityService.saveIdentity(userId, 'guest', previousGuestId, true); } // Устанавливаем сессию @@ -1787,22 +709,11 @@ router.post('/wallet', async (req, res) => { req.session.authenticated = true; req.session.isAdmin = isAdmin; - // Сохраняем сессию перед связыванием сообщений - await new Promise((resolve, reject) => { - req.session.save(err => { - if (err) { - logger.error('[wallet] Error saving session:', err); - reject(err); - } else { - logger.info(`[wallet] Session saved successfully for user ${userId}, address ${address}`); - resolve(); - } - }); - }); + // Сохраняем сессию + await sessionService.saveSession(req.session); // Связываем гостевые сообщения с пользователем - const linkResults = await linkGuestMessagesAfterAuth(req.session, userId); - logger.info(`[wallet] Guest messages linking results:`, linkResults); + await sessionService.linkGuestMessages(req.session, userId); // Возвращаем успешный ответ return res.json({ @@ -1822,38 +733,23 @@ router.post('/wallet', async (req, res) => { } }); -// Маршрут для обработки гостевых сообщений после аутентификации -router.post('/link-guest-messages', requireAuth, async (req, res) => { +// Маршрут для получения всех идентификаторов пользователя +router.get('/identities', requireAuth, async (req, res) => { try { - const userId = req.user.id; - const { currentGuestId } = req.body; - - if (!currentGuestId) { - return res.status(400).json({ - success: false, - error: 'Guest ID is required' - }); - } - - // Создаем временную сессию для совместимости - const tempSession = { - guestId: currentGuestId, - save: async (callback) => { - if (typeof callback === 'function') { - callback(null); - } - return Promise.resolve(); - } - }; - - const result = await linkGuestMessagesAfterAuth(tempSession, userId); + const { userId } = req.session; - res.json(result); + // Получаем все идентификаторы пользователя + const identities = await identityService.getUserIdentities(userId); + + res.json({ + success: true, + identities + }); } catch (error) { - logger.error('Error in /link-guest-messages:', error); - res.status(500).json({ - success: false, - error: 'Failed to process guest messages' + logger.error('Error getting user identities:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' }); } }); @@ -1864,8 +760,7 @@ router.get('/check-session', async (req, res) => { // Если у пользователя нет guestId, создаем его if (!req.session.guestId && !req.session.authenticated) { req.session.guestId = crypto.randomBytes(16).toString('hex'); - await req.session.save(); - logger.info('Created new guestId:', req.session.guestId); + await sessionService.saveSession(req.session); } res.json({ @@ -1887,37 +782,8 @@ router.get('/check-tokens/:address', async (req, res) => { try { const { address } = req.params; - // Проверяем баланс токенов на разных сетях - const balances = { - eth: '0', - bsc: '0', - arbitrum: '0', - polygon: '0' - }; - - try { - balances.eth = await authService.getTokenBalance(address, 'eth'); - } catch (error) { - logger.error(`Error checking ETH balance: ${error.message}`); - } - - try { - balances.bsc = await authService.getTokenBalance(address, 'bsc'); - } catch (error) { - logger.error(`Error checking BSC balance: ${error.message}`); - } - - try { - balances.arbitrum = await authService.getTokenBalance(address, 'arbitrum'); - } catch (error) { - logger.error(`Error checking Arbitrum balance: ${error.message}`); - } - - try { - balances.polygon = await authService.getTokenBalance(address, 'polygon'); - } catch (error) { - logger.error(`Error checking Polygon balance: ${error.message}`); - } + // Получаем балансы токенов на всех сетях + const balances = await authService.getTokenBalances(address); res.json({ success: true, @@ -1932,4 +798,4 @@ router.get('/check-tokens/:address', async (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router; \ No newline at end of file diff --git a/backend/routes/auth.js.bak b/backend/routes/auth.js.bak new file mode 100644 index 0000000..62c4f1a --- /dev/null +++ b/backend/routes/auth.js.bak @@ -0,0 +1,1935 @@ +const express = require('express'); +const router = express.Router(); +const crypto = require('crypto'); +const db = require('../db'); +const logger = require('../utils/logger'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); +const { checkRole, requireAuth, auth } = require('../middleware/auth'); +const { pool } = require('../db'); +const authService = require('../services/auth-service'); +const { SiweMessage } = require('siwe'); +const emailBot = require('../services/emailBot'); +const { verificationCodes } = require('../services/telegramBot'); +const { checkTokensAndUpdateRole } = require('../services/auth-service'); +const { ethers } = require('ethers'); +const { initTelegramAuth } = require('../services/telegramBot'); +const emailAuth = require('../services/emailAuth'); +const verificationService = require('../services/verification-service'); +const { processGuestMessages } = require('./chat'); // Импортируем функцию обработки гостевых сообщений +const nonceStore = {}; + +// Создайте лимитер для попыток аутентификации +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 минут + max: 20, + standardHeaders: true, + legacyHeaders: false, + message: { error: 'Слишком много попыток аутентификации. Попробуйте позже.' }, +}); + +// Получение nonce для аутентификации +router.get('/nonce', async (req, res) => { + try { + const { address } = req.query; + if (!address) { + return res.status(400).json({ error: 'Address is required' }); + } + + // Генерируем случайный nonce + const nonce = crypto.randomBytes(16).toString('hex'); + + // Проверяем, существует ли уже nonce для этого адреса + const existingNonce = await db.query( + 'SELECT id FROM nonces WHERE identity_value = $1', + [address.toLowerCase()] + ); + + if (existingNonce.rows.length > 0) { + // Обновляем существующий nonce + await db.query( + 'UPDATE nonces SET nonce = $1, expires_at = NOW() + INTERVAL \'15 minutes\' WHERE identity_value = $2', + [nonce, address.toLowerCase()] + ); + } else { + // Создаем новый nonce + await db.query( + 'INSERT INTO nonces (identity_value, nonce, expires_at) VALUES ($1, $2, NOW() + INTERVAL \'15 minutes\')', + [address.toLowerCase(), nonce] + ); + } + + console.log(`Nonce ${nonce} сохранен для адреса ${address}`); + + res.json({ nonce }); + } catch (error) { + console.error('Error generating nonce:', error); + res.status(500).json({ error: 'Failed to generate nonce' }); + } +}); + +// Минимальный ABI для проверки баланса ERC20 +const ERC20_ABI = [ + "function balanceOf(address owner) view returns (uint256)" +]; + +// Верификация подписи и создание сессии +router.post('/verify', async (req, res) => { + try { + const { address, message, signature } = req.body; + + logger.info(`[verify] Verifying signature for address: ${address}`); + + // Сохраняем гостевые ID до проверки + const guestId = req.session.guestId; + const previousGuestId = req.session.previousGuestId; + + logger.info(`[verify] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); + + // Проверяем подпись + const isValid = await authService.verifySignature(message, signature, address); + if (!isValid) { + logger.warn(`[verify] Invalid signature for address: ${address}`); + return res.status(401).json({ success: false, error: 'Invalid signature' }); + } + + // Проверяем nonce + const nonceResult = await db.query('SELECT nonce FROM nonces WHERE identity_value = $1', [address.toLowerCase()]); + if (nonceResult.rows.length === 0 || nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]) { + logger.warn(`[verify] Invalid nonce for address: ${address}`); + return res.status(401).json({ success: false, error: 'Invalid nonce' }); + } + + logger.info(`[verify] Signature and nonce verified for address: ${address}`); + + // Находим или создаем пользователя + let userId, isAdmin; + + // Ищем пользователя по адресу в таблице 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.toLowerCase()]); + + if (userResult.rows.length > 0) { + // Пользователь найден по кошельку + userId = userResult.rows[0].id; + isAdmin = userResult.rows[0].role === 'admin'; + logger.info(`[verify] Found existing user: ID=${userId}, isAdmin=${isAdmin}`); + } else if (req.session.guestId) { + // Проверяем, есть ли пользователь с текущим guestId + const guestUserResult = await db.query(` + SELECT u.* FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = 'guest' AND ui.provider_id = $1 + `, [req.session.guestId]); + + if (guestUserResult.rows.length > 0) { + // Используем существующего пользователя с guestId + userId = guestUserResult.rows[0].id; + isAdmin = guestUserResult.rows[0].role === 'admin'; + logger.info(`[verify] Found user by guestId: ID=${userId}, isAdmin=${isAdmin}`); + + // Добавляем идентификатор кошелька к существующему пользователю + await saveUserIdentity(userId, 'wallet', address.toLowerCase(), true); + logger.info(`[verify] Added wallet identity ${address.toLowerCase()} to existing user ${userId}`); + } else { + // Создаем нового пользователя + const newUserResult = await db.query( + 'INSERT INTO users (role) VALUES ($1) RETURNING id', + ['user'] + ); + + userId = newUserResult.rows[0].id; + isAdmin = false; + logger.info(`[verify] Created new user: ID=${userId}`); + + // Добавляем идентификатор кошелька + await saveUserIdentity(userId, 'wallet', address.toLowerCase(), true); + logger.info(`[verify] Added wallet identity ${address.toLowerCase()} to new user ${userId}`); + + // Добавляем идентификатор гостя + await saveUserIdentity(userId, 'guest', req.session.guestId, true); + logger.info(`[verify] Added guest identity ${req.session.guestId} to new user ${userId}`); + } + } else { + // Создаем нового пользователя без гостевого ID + const newUserResult = await db.query( + 'INSERT INTO users (role) VALUES ($1) RETURNING id', + ['user'] + ); + + userId = newUserResult.rows[0].id; + isAdmin = false; + logger.info(`[verify] Created new user without guest ID: ID=${userId}`); + + // Добавляем идентификатор кошелька + await saveUserIdentity(userId, 'wallet', address.toLowerCase(), true); + logger.info(`[verify] Added wallet identity ${address.toLowerCase()} to new user ${userId}`); + } + + // Проверяем наличие админских токенов + const adminStatus = await authService.checkAdminTokens(address.toLowerCase()); + if (adminStatus) { + isAdmin = true; + await db.query('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]); + logger.info(`[verify] Updated user ${userId} to admin role based on token check`); + } + + // Обновляем сессию + req.session.userId = userId; + req.session.authenticated = true; + req.session.authType = 'wallet'; + req.session.isAdmin = isAdmin; + req.session.address = address.toLowerCase(); + + // Удаляем временный ID + delete req.session.tempUserId; + + // Сохраняем сессию перед связыванием сообщений + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[verify] Error saving session:', err); + reject(err); + } else { + logger.info(`[verify] Session saved successfully for user ${userId}`); + resolve(); + } + }); + }); + + // Связываем гостевые сообщения с пользователем + const linkResults = await linkGuestMessagesAfterAuth(req.session, userId); + logger.info(`[verify] Guest messages linking results:`, linkResults); + + // Возвращаем успешный ответ + return res.json({ + success: true, + userId, + address, + isAdmin, + authenticated: true + }); + + } catch (error) { + logger.error('[verify] Error:', error); + res.status(500).json({ success: false, error: 'Server error' }); + } +}); + +// Аутентификация через Telegram +router.post('/telegram/verify', async (req, res) => { + try { + const { telegramId, verificationCode } = req.body; + + if (!telegramId || !verificationCode) { + return res.status(400).json({ + success: false, + error: 'Missing required fields' + }); + } + + logger.info(`[telegram/verify] Verifying Telegram auth for ID: ${telegramId}`); + + // Сохраняем гостевой ID из текущей сессии + const guestId = req.session.guestId; + + // Передаем сессию в метод верификации + const verificationResult = await authService.verifyTelegramAuth( + telegramId, + verificationCode, + req.session + ); + + if (!verificationResult.success) { + return res.status(400).json({ + success: false, + error: verificationResult.error || 'Verification failed' + }); + } + + // Создаем новую сессию для этого telegramId + req.session.regenerate(async (err) => { + if (err) { + logger.error('[telegram/verify] Error regenerating session:', err); + return res.status(500).json({ + success: false, + error: 'Session error' + }); + } + + // Устанавливаем данные в новой сессии + req.session.userId = verificationResult.userId; + req.session.telegramId = telegramId; + req.session.authType = 'telegram'; + req.session.authenticated = true; + req.session.role = verificationResult.role; + + // Восстанавливаем гостевой ID, если он был + if (guestId) { + req.session.guestId = guestId; + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[telegram/verify] Error saving session:', err); + reject(err); + } else { + logger.info(`[telegram/verify] Session saved for user ${verificationResult.userId}`); + resolve(); + } + }); + }); + + // Связываем гостевые сообщения только один раз - исправлено дублирование + if (guestId) { + // Создаем объект сессии для совместимости с другими методами аутентификации + const session = { + guestId: guestId, + save: async (callback) => { + if (typeof callback === 'function') { + callback(null); + } + return Promise.resolve(); + } + }; + const linkResults = await linkGuestMessagesAfterAuth(session, verificationResult.userId); + logger.info(`[telegram/verify] Guest messages linking results:`, linkResults); + } + + return res.json({ + success: true, + userId: verificationResult.userId, + role: verificationResult.role, + telegramId, + isNewUser: verificationResult.isNewUser + }); + }); + } catch (error) { + logger.error('[telegram/verify] Error:', error); + return res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + +// Маршрут для запроса кода подтверждения по email +router.post('/email/request', authLimiter, async (req, res) => { + try { + const { email } = req.body; + + if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + return res.status(400).json({ error: 'Invalid email format' }); + } + + // Используем общую логику инициализации email аутентификации + const { verificationCode } = await emailAuth.initEmailAuth(req.session, email); + + // Сохраняем сессию после установки pendingEmail + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('Error saving session:', err); + reject(err); + } else { + logger.info(`Session saved successfully with pendingEmail: ${email}`); + resolve(); + } + }); + }); + + // Отправляем email с кодом подтверждения + const emailBot = new emailBot(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD); + const result = await emailBot.sendVerificationCode(email, req.session.tempUserId || req.session.userId); + + if (result.success) { + res.json({ + success: true, + message: 'Код подтверждения отправлен на email' + }); + } else { + res.status(500).json({ + success: false, + error: result.error || 'Ошибка отправки кода' + }); + } + } catch (error) { + logger.error('Error requesting email code:', error); + res.status(500).json({ error: error.message || 'Ошибка сервера' }); + } +}); + +// Функция для проверки кода email +async function verifyEmailCode(code, email) { + try { + logger.info(`[verifyEmailCode] Verifying code ${code} for email ${email}`); + + // Получаем код из базы данных + const result = await db.query( + `SELECT * FROM verification_codes + WHERE code = $1 AND provider_id = $2 AND provider = 'email' + AND used = false AND expires_at > NOW()`, + [code, email] + ); + + if (result.rows.length === 0) { + logger.warn(`[verifyEmailCode] No valid code found for ${email}`); + return { success: false, error: 'Неверный или истекший код' }; + } + + // Помечаем код как использованный + await db.query( + 'UPDATE verification_codes SET used = true WHERE code = $1 AND provider_id = $2 AND provider = \'email\'', + [code, email] + ); + + logger.info(`[verifyEmailCode] Code verified successfully for ${email}`); + return { success: true }; + } catch (error) { + logger.error(`[verifyEmailCode] Error:`, error); + throw error; + } +} + +// Маршрут для верификации email +router.post('/email/verify', async (req, res) => { + try { + const { code } = req.body; + + if (!code) { + return res.status(400).json({ + success: false, + error: 'Код подтверждения обязателен' + }); + } + + if (!req.session.pendingEmail) { + return res.status(400).json({ + success: false, + error: 'Email не найден в сессии. Пожалуйста, запросите код подтверждения снова.' + }); + } + + // Сохраняем гостевые ID до проверки + const guestId = req.session.guestId; + + // Проверяем код через сервис верификации + const verificationResult = await verifyEmailCode(code, req.session.pendingEmail); + + if (!verificationResult.success) { + return res.status(400).json({ + success: false, + error: verificationResult.error || 'Неверный код подтверждения' + }); + } + + let userId; + + // Если пользователь уже аутентифицирован, добавляем email к существующему аккаунту + if (req.session.authenticated && req.session.userId) { + userId = req.session.userId; + + // Проверяем не связан ли email с другим аккаунтом + const existingEmail = await db.query(` + SELECT user_id FROM user_identities + WHERE provider = 'email' AND provider_id = $1 + `, [req.session.pendingEmail]); + + if (existingEmail.rows.length > 0) { + return res.status(400).json({ + success: false, + error: 'Этот email уже связан с другим пользователем' + }); + } + + // Добавляем email к существующему пользователю + await saveUserIdentity(userId, 'email', req.session.pendingEmail, true); + logger.info(`[email/verify] Added email identity ${req.session.pendingEmail} to existing user ${userId}`); + + } else { + // Ищем существующего пользователя по email + const existingUser = await db.query(` + SELECT u.* FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = 'email' AND ui.provider_id = $1 + `, [req.session.pendingEmail]); + + if (existingUser.rows.length > 0) { + // Используем существующего пользователя + userId = existingUser.rows[0].id; + logger.info(`[email/verify] Found existing user with ID ${userId} for email ${req.session.pendingEmail}`); + } else { + // Создаем нового пользователя + const newUser = await db.query( + 'INSERT INTO users (role) VALUES ($1) RETURNING id', + ['user'] + ); + userId = newUser.rows[0].id; + + // Добавляем email идентификатор + await saveUserIdentity(userId, 'email', req.session.pendingEmail, true); + logger.info(`[email/verify] Created new user with ID ${userId} for email ${req.session.pendingEmail}`); + } + } + + // Если есть гостевые сообщения, переносим их + if (guestId && !req.session.processedGuestIds?.includes(guestId)) { + await processGuestMessages(userId, guestId); + // Сохраняем обработанный guestId чтобы избежать повторной обработки + if (!req.session.processedGuestIds) { + req.session.processedGuestIds = []; + } + req.session.processedGuestIds.push(guestId); + logger.info(`[email/verify] Processed guest messages for user ${userId} with guest ID ${guestId}`); + } + + // Создаем новую сессию + req.session.regenerate(async (err) => { + if (err) { + logger.error('Error regenerating session:', err); + return res.status(500).json({ success: false, error: 'Server error' }); + } + + // Устанавливаем данные новой сессии + req.session.authenticated = true; + req.session.userId = userId; + req.session.email = req.session.pendingEmail; + req.session.authType = 'email'; + + // Сохраняем список обработанных гостевых ID + if (req.session.processedGuestIds?.length > 0) { + req.session.processedGuestIds = [...req.session.processedGuestIds]; + } + + // Удаляем временные данные + delete req.session.tempUserId; + delete req.session.guestId; + delete req.session.pendingEmail; + + // Сохраняем сессию + req.session.save((err) => { + if (err) { + logger.error('Error saving session:', err); + return res.status(500).json({ success: false, error: 'Server error' }); + } + + res.json({ + success: true, + userId, + email: req.session.email, + authenticated: true, + authType: 'email' + }); + }); + }); + + } catch (error) { + logger.error('[email/verify] Error:', error); + res.status(500).json({ success: false, error: 'Server error' }); + } +}); + +// Связывание аккаунтов +router.post('/link-identity', async (req, res) => { + try { + if (!req.session || !req.session.userId) { + return res.status(401).json({ error: 'Требуется аутентификация' }); + } + + const { identityType, identityValue } = req.body; + + // Проверяем, не привязан ли уже этот идентификатор к другому пользователю + const existingUserId = await authService.getUserIdByIdentity(identityType, identityValue); + + if (existingUserId && existingUserId !== req.session.userId) { + return res.status(400).json({ error: 'Этот идентификатор уже привязан к другому аккаунту' }); + } + + // Добавляем новый идентификатор + if (!existingUserId) { + await db.query( + 'INSERT INTO user_identities (user_id, identity_type, identity_value, created_at) VALUES ($1, $2, $3, NOW())', + [req.session.userId, identityType, identityValue] + ); + } + + // Если добавлен кошелек, проверяем токены + if (identityType === 'wallet') { + await authService.checkTokensAndUpdateRole(identityValue); + } + + // Получаем все идентификаторы пользователя + const identitiesResult = await db.query(` + SELECT identity_type, identity_value + FROM user_identities + WHERE user_id = $1 + `, [req.session.userId]); + const identities = identitiesResult.rows; + + // Получаем текущую роль + const isAdmin = await authService.isAdmin(req.session.userId); + + res.json({ + success: true, + identities, + isAdmin + }); + } catch (error) { + logger.error(`Link identity error: ${error.message}`); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Проверка статуса аутентификации +router.get('/check', async (req, res) => { + try { + logger.info(`[session/check] Checking session: ${req.sessionID}`); + + const authenticated = req.session.authenticated || false; + const authType = req.session.authType || null; + + // Подробное логирование для отладки восстановления сессии + logger.info(`[session/check] Session state: authenticated=${authenticated}, authType=${authType}, userId=${req.session.userId || 'none'}`); + + // Проверяем наличие идентификаторов в сессии + const sessionIdentities = []; + if (req.session.userId) sessionIdentities.push(`userId:${req.session.userId}`); + if (req.session.email) sessionIdentities.push(`email:${req.session.email}`); + if (req.session.address) sessionIdentities.push(`address:${req.session.address}`); + if (req.session.telegramId) sessionIdentities.push(`telegramId:${req.session.telegramId}`); + + logger.info(`[session/check] Identities in session: ${sessionIdentities.join(', ')}`); + + let identities = []; + let isAdmin = false; + + if (authenticated && req.session.userId) { + // Если пользователь аутентифицирован, получаем его идентификаторы из БД + try { + const identitiesResult = await db.query( + `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, + [req.session.userId] + ); + + identities = identitiesResult.rows; + logger.info(`[session/check] Found ${identities.length} identities in database for user ${req.session.userId}`); + + // Проверяем роль пользователя + const roleResult = await db.query( + 'SELECT role FROM users WHERE id = $1', + [req.session.userId] + ); + + if (roleResult.rows.length > 0) { + isAdmin = roleResult.rows[0].role === 'admin'; + req.session.isAdmin = isAdmin; + } + } catch (error) { + logger.error(`[session/check] Error fetching identities: ${error.message}`); + } + } + + // Проверяем, нужно ли создать новый гостевой ID + if (!authenticated && !req.session.guestId) { + req.session.guestId = crypto.randomBytes(16).toString('hex'); + logger.info(`[session/check] Created new guest ID: ${req.session.guestId}`); + + // Сохраняем сессию с новым гостевым ID + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[session/check] Error saving session with new guest ID:', err); + reject(err); + } else { + logger.info('[session/check] Session with new guest ID saved successfully'); + resolve(); + } + }); + }); + } + + // Формируем ответ + const response = { + success: true, + authenticated, + userId: req.session.userId || null, + guestId: req.session.guestId || null, + authType, + identitiesCount: identities.length, + isAdmin: isAdmin || false + }; + + // Добавляем специфические поля в зависимости от типа аутентификации + if (authType === 'wallet') { + response.address = req.session.address || null; + } else if (authType === 'email') { + response.email = req.session.email || null; + } else if (authType === 'telegram') { + response.telegramId = req.session.telegramId || null; + if (req.session.telegramUsername) { + response.telegramUsername = req.session.telegramUsername; + } + if (req.session.telegramFirstName) { + response.telegramFirstName = req.session.telegramFirstName; + } + } + + logger.info(`[session/check] Session check complete: authenticated=${authenticated}, authType=${authType}`); + return res.json(response); + } catch (error) { + logger.error('[session/check] Error:', error); + return res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + +// Выход из системы +router.post('/logout', async (req, res) => { + try { + // Очищаем все идентификаторы сессии + req.session.authenticated = false; + req.session.userId = null; + req.session.address = null; + req.session.telegramId = null; + req.session.email = null; + req.session.isAdmin = false; + req.session.guestId = null; + req.session.previousGuestId = null; + req.session.processedGuestIds = []; + req.session.pendingEmail = null; + req.session.authType = null; + + // Сохраняем изменения в сессии + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[logout] Error saving session:', err); + reject(err); + } else { + logger.info('[logout] Session cleared successfully'); + resolve(); + } + }); + }); + + // Уничтожаем сессию полностью + req.session.destroy((err) => { + if (err) { + logger.error('[logout] Error destroying session:', err); + return res.status(500).json({ success: false, error: 'Error during logout' }); + } + res.clearCookie('connect.sid'); + res.json({ success: true, message: 'Logged out successfully' }); + }); + } catch (error) { + logger.error('[logout] Error:', error); + res.status(500).json({ success: false, error: 'Internal server error during logout' }); + } +}); + +// Маршрут для авторизации через Telegram +router.get('/telegram', (req, res) => { + // Генерируем случайный токен для авторизации + const token = crypto.randomBytes(32).toString('hex'); + + // Сохраняем токен в сессии + req.session.telegramToken = token; + + // Создаем URL для авторизации через Telegram + const botName = process.env.TELEGRAM_BOT_NAME || 'YourBotName'; + const authUrl = `https://t.me/${botName}?start=${token}`; + + res.json({ authUrl }); +}); + +// Маршрут для получения кода подтверждения Telegram +router.get('/telegram/code', authLimiter, async (req, res) => { + try { + // Создаем код через сервис телеграм авторизации + const authData = await initTelegramAuth(req.session); + + if (!authData.verificationCode) { + return res.status(500).json({ + success: false, + error: 'Failed to generate verification code' + }); + } + + res.json({ + success: true, + message: 'Отправьте этот код боту @' + process.env.TELEGRAM_BOT_USERNAME, + code: authData.verificationCode, + botUsername: process.env.TELEGRAM_BOT_USERNAME || 'YourDAppBot' + }); + } catch (error) { + logger.error(`Error in telegram code request: ${error.message}`); + res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); + } +}); + +// Функция для проверки кода Telegram +async function verifyTelegramCode(code) { + try { + // Используем глобальное хранилище кодов + const verificationCodes = global.verificationCodes; + + if (!verificationCodes) { + return { success: false, error: 'Система верификации не инициализирована' }; + } + + // Ищем chatId по коду + for (const [chatId, data] of verificationCodes.entries()) { + if (data.code === code) { + // Проверяем срок действия + if (Date.now() > data.expires) { + verificationCodes.delete(chatId); + return { success: false, error: 'Код истек' }; + } + + // Код верный и не истек + const telegramId = chatId; + verificationCodes.delete(chatId); + return { + success: true, + telegramId: telegramId + }; + } + } + return { success: false, error: 'Неверный код' }; + } catch (error) { + console.error('Error in verifyTelegramCode:', error); + throw error; + } +} + +// Функция для проверки баланса токенов +async function checkTokenBalance(address) { + try { + const authService = require('../services/auth-service'); + const isAdmin = await authService.checkTokensAndUpdateRole(address); + return isAdmin; + } catch (error) { + console.error('Error checking token balance:', error); + return false; + } +} + +// Маршрут для связывания разных идентификаторов +router.post('/link-identity', requireAuth, async (req, res) => { + try { + const { type, value } = req.body; + const userId = req.session.userId; + + // Проверяем валидность типа + if (!['wallet', 'email', 'telegram'].includes(type)) { + return res.status(400).json({ + success: false, + error: 'Неподдерживаемый тип идентификатора' + }); + } + + // Проверяем, не связан ли идентификатор с другим пользователем + const existingResult = await db.query(` + SELECT ui.user_id + FROM user_identities ui + WHERE ui.identity_type = $1 AND ui.identity_value = $2 + `, [type, value]); + + if (existingResult.rows.length > 0 && existingResult.rows[0].user_id !== userId) { + return res.status(400).json({ + success: false, + error: 'Этот идентификатор уже связан с другим аккаунтом' + }); + } + + // Добавляем или обновляем идентификатор + await db.query(` + INSERT INTO user_identities (user_id, identity_type, identity_value, created_at, verified) + VALUES ($1, $2, $3, NOW(), true) + ON CONFLICT (identity_type, identity_value) + DO UPDATE SET verified = true + `, [userId, type, value]); + + // Если связываем кошелек, обновляем также поле address в таблице users + if (type === 'wallet') { + await db.query('UPDATE users SET address = $1 WHERE id = $2', [value, userId]); + + // Проверяем наличие токенов для статуса админа + const isAdmin = await authService.checkAdminTokens(value); + if (isAdmin) { + await db.query('UPDATE users SET is_admin = true WHERE id = $1', [userId]); + req.session.isAdmin = true; + } + + req.session.address = value; + } + + // Если связываем email, обновляем сессию + if (type === 'email') { + req.session.email = value; + } + + // Если связываем telegram, обновляем сессию + if (type === 'telegram') { + req.session.telegramId = value; + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ + success: true, + message: `Идентификатор успешно связан с вашим аккаунтом`, + isAdmin: req.session.isAdmin + }); + } catch (error) { + logger.error(`Error linking identity: ${error.message}`); + res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); + } +}); + +// Добавляем маршрут для проверки прав доступа +router.get('/check-access', requireAuth, async (req, res) => { + try { + const userId = req.session.userId; + const address = req.session.address; + + if (address) { + const isAdmin = await checkTokensAndUpdateRole(address); + + // Обновляем сессию + req.session.isAdmin = isAdmin; + + return res.json({ + success: true, + isAdmin, + userId, + address + }); + } + + return res.json({ + success: true, + isAdmin: false, + userId, + address: null + }); + + } catch (error) { + logger.error('Error checking access:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Обновление сессии +router.post('/refresh-session', async (req, res) => { + try { + const { address } = req.body; + + if (req.session && req.session.authenticated) { + console.log('Обновление сессии для пользователя:', req.session.userId); + + // Обновляем время жизни сессии + req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 дней + + // Сохраняем обновленную сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + console.error('Ошибка при сохранении сессии:', err); + reject(err); + } else { + console.log('Сессия успешно обновлена'); + resolve(); + } + }); + }); + + return res.json({ success: true }); + } else if (address) { + // Если сессия не аутентифицирована, но есть адрес + try { + const { pool } = require('../db'); + const result = await pool.query('SELECT * FROM users WHERE address = $1', [address]); + + if (result.rows.length > 0) { + const user = result.rows[0]; + + // Обновляем сессию + req.session.authenticated = true; + req.session.userId = user.id; + req.session.address = address; + req.session.isAdmin = user.is_admin; + req.session.authType = 'wallet'; + + // Сохраняем обновленную сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + console.error('Ошибка при сохранении сессии:', err); + reject(err); + } else { + console.log('Сессия успешно обновлена'); + resolve(); + } + }); + }); + + return res.json({ success: true }); + } + } catch (error) { + console.error('Ошибка при проверке пользователя:', error); + } + } + + // Если не удалось обновить сессию, возвращаем успех=false, но не ошибку + return res.json({ success: false }); + } catch (error) { + console.error('Ошибка при обновлении сессии:', error); + res.status(500).json({ error: 'Ошибка сервера' }); + } +}); + +// Маршрут для обновления статуса администратора +router.post('/update-admin-status', async (req, res) => { + try { + const { address, isAdmin } = req.body; + + if (!address) { + return res.status(400).json({ error: 'Address is required' }); + } + + console.log(`Запрос на обновление статуса администратора для адреса ${address} на ${isAdmin}`); + + // Проверяем, существует ли пользователь + const userResult = await db.query('SELECT * FROM users WHERE address = $1', [ + address.toLowerCase(), + ]); + + if (userResult.rows.length === 0) { + // Если пользователь не найден, создаем его + await db.query('INSERT INTO users (address, is_admin, created_at) VALUES ($1, $2, NOW())', [ + address.toLowerCase(), + isAdmin, + ]); + + console.log( + `Создан новый пользователь с адресом ${address} и статусом администратора ${isAdmin}` + ); + } else { + // Если пользователь найден, обновляем его статус + await db.query('UPDATE users SET is_admin = $1 WHERE address = $2', [ + isAdmin, + address.toLowerCase(), + ]); + + console.log( + `Создан новый пользователь с адресом ${address} и статусом администратора ${isAdmin}` + ); + } + + res.json({ success: true }); + } catch (error) { + console.error('Ошибка при обновлении статуса администратора:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Маршрут для создания токена авторизации через Email +router.post('/email/auth-token', async (req, res) => { + try { + const { email } = req.body; + + if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + return res.status(400).json({ success: false, error: 'Неверный формат email' }); + } + + // Генерируем уникальный токен + const token = crypto.randomBytes(20).toString('hex'); + + // Получаем ID пользователя из сессии или создаем нового гостевого пользователя + let userId; + + if (req.session.authenticated && req.session.userId) { + // Если пользователь уже аутентифицирован, используем его ID + userId = req.session.userId; + } else { + // Создаем временного пользователя + const userResult = await db.query( + 'INSERT INTO users (created_at) VALUES (NOW()) RETURNING id' + ); + userId = userResult.rows[0].id; + + // Сохраняем ID в сессии как временный + req.session.tempUserId = userId; + } + + // Сохраняем токен в базе данных + await db.query(` + INSERT INTO email_auth_tokens (user_id, token, created_at, expires_at) + VALUES ($1, $2, NOW(), NOW() + INTERVAL '15 minutes') + `, [userId, token]); + + // Отправляем email с кодом подтверждения через emailBot + const emailBot = require('../services/emailBot'); + const emailService = new emailBot(process.env.EMAIL_USER, process.env.EMAIL_PASSWORD); + const sendResult = await emailService.sendVerificationCode(email, token); + + if (sendResult.success) { + res.json({ + success: true, + message: 'Код подтверждения отправлен на ваш email' + }); + } else { + res.status(500).json({ success: false, error: 'Ошибка отправки email' }); + } + } catch (error) { + logger.error(`Error creating Email auth token: ${error.message}`); + res.status(500).json({ success: false, error: 'Ошибка сервера' }); + } +}); + +// Маршрут для проверки статуса аутентификации через Email +router.get('/email/auth-status/:token', async (req, res) => { + try { + const { token } = req.params; + + // Проверяем статус токена + const tokenResult = await db.query(` + SELECT user_id, used FROM email_auth_tokens + WHERE token = $1 AND expires_at > NOW() + `, [token]); + + if (tokenResult.rows.length === 0) { + return res.json({ success: false, error: 'Токен не найден или истек' }); + } + + const userId = tokenResult.rows[0].user_id; + const isAuthenticated = tokenResult.rows[0].used; + + if (isAuthenticated) { + // Токен использован, email подключен + + // Получаем email пользователя + const emailResult = await db.query(` + SELECT ui.identity_value FROM user_identities ui + WHERE ui.user_id = $1 AND ui.identity_type = 'email' + `, [userId]); + + if (emailResult.rows.length > 0) { + // Устанавливаем полную аутентификацию в сессии + req.session.authenticated = true; + req.session.userId = userId; + req.session.email = emailResult.rows[0].identity_value; + req.session.authType = 'email'; + + // Если был временный ID, удаляем его + if (req.session.tempUserId) { + delete req.session.tempUserId; + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) reject(err); + else resolve(); + }); + }); + } + } + + res.json({ + success: true, + authenticated: isAuthenticated + }); + } catch (error) { + logger.error(`Error checking Email auth status: ${error.message}`); + res.status(500).json({ success: false, error: 'Ошибка сервера' }); + } +}); + +// Маршрут для проверки кода email +router.post('/email/verify-code', async (req, res) => { + try { + const { email, code } = req.body; + + if (!email || !code) { + return res.status(400).json({ + success: false, + error: 'Email и код подтверждения обязательны' + }); + } + + logger.info(`[email/verify-code] Verifying code for email: ${email}`); + + // Сохраняем гостевой ID до проверки + const guestId = req.session.guestId; + const previousGuestId = req.session.previousGuestId; + + logger.info(`[email/verify-code] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); + + // Проверяем существование пользователя с таким email + const userResult = await db.query( + `SELECT u.id FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = $1 AND ui.provider_id = $2`, + ['email', email.toLowerCase()] + ); + + let userId; + let isNewUser = false; + + if (userResult.rows.length > 0) { + // Пользователь уже существует + userId = userResult.rows[0].id; + logger.info(`[email/verify-code] Found existing user with ID ${userId}`); + } else if (req.session.tempUserId) { + // Используем временный ID пользователя + userId = req.session.tempUserId; + logger.info(`[email/verify-code] Using tempUserId ${userId}`); + } else { + // Создаем нового пользователя + const newUserResult = await db.query( + 'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id', + ['user'] + ); + userId = newUserResult.rows[0].id; + isNewUser = true; + logger.info(`[email/verify-code] Created new user with ID ${userId} for email ${email}`); + } + + // Проверяем код верификации + const verification = await verificationService.verifyCode( + code.toUpperCase(), + 'email', + email.toLowerCase() + ); + + if (!verification.success) { + logger.warn(`[email/verify-code] Invalid verification code for ${email}: ${verification.error}`); + return res.status(400).json({ + success: false, + error: verification.error + }); + } + + logger.info(`[email/verify-code] Verification successful for email ${email}, user ${userId}`); + + // Сохраняем email как identity + await saveUserIdentity(userId, 'email', email.toLowerCase(), true); + logger.info(`[email/verify-code] Saved email identity ${email} for user ${userId}`); + + // Если есть гостевой ID, сохраняем его + if (guestId) { + await saveUserIdentity(userId, 'guest', guestId, true); + logger.info(`[email/verify-code] Saved guest ID ${guestId} for user ${userId}`); + } + + if (previousGuestId && previousGuestId !== guestId) { + await saveUserIdentity(userId, 'guest', previousGuestId, true); + logger.info(`[email/verify-code] Saved previous guest ID ${previousGuestId} for user ${userId}`); + } + + // Устанавливаем данные сессии + req.session.authenticated = true; + req.session.userId = userId; + req.session.authType = 'email'; + req.session.email = email.toLowerCase(); + + // Удаляем временный ID + delete req.session.tempUserId; + delete req.session.pendingEmail; + + // Сохраняем сессию перед связыванием сообщений + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[email/verify-code] Error saving session:', err); + reject(err); + } else { + logger.info(`[email/verify-code] Session saved successfully for user ${userId}`); + resolve(); + } + }); + }); + + // Сначала сохраняем сессию с актуальным гостевым ID + if (guestId) { + req.session.guestId = guestId; + } + + // Связываем гостевые сообщения с пользователем + const linkResults = await linkGuestMessagesAfterAuth(req.session, userId); + logger.info(`[email/verify-code] Guest messages linking results:`, linkResults); + + return res.json({ + success: true, + userId, + email: email.toLowerCase(), + authenticated: true + }); + + } catch (error) { + logger.error('[email/verify-code] Error:', error); + return res.status(500).json({ + success: false, + error: 'Ошибка сервера' + }); + } +}); + +// Маршрут для очистки сессии +router.post('/clear-session', async (req, res) => { + try { + // Очищаем все данные сессии + req.session.destroy((err) => { + if (err) { + console.error('Error destroying session:', err); + return res.status(500).json({ error: 'Internal server error' }); + } + res.json({ success: true }); + }); + } catch (error) { + console.error('Error clearing session:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Инициализация Telegram аутентификации +router.post('/telegram/init', async (req, res) => { + try { + const { verificationCode, botLink } = await initTelegramAuth(req.session); + + if (!verificationCode || !botLink) { + throw new Error('Failed to generate verification code'); + } + + res.json({ + success: true, + verificationCode, + botLink + }); + } catch (error) { + logger.error('Error initializing Telegram auth:', error); + + if (error.message === 'Telegram уже привязан к этому аккаунту') { + return res.status(400).json({ + success: false, + error: error.message + }); + } + + res.status(500).json({ + success: false, + error: 'Failed to initialize Telegram auth' + }); + } +}); + +// Инициализация email аутентификации +router.post('/email/init', async (req, res) => { + try { + const { email } = req.body; + + if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { + return res.status(400).json({ + success: false, + error: 'Некорректный формат email' + }); + } + + logger.info(`[email/init] Initializing email authentication for: ${email}`); + + // Сохраняем гостевой ID до проверки + const guestId = req.session.guestId; + const previousGuestId = req.session.previousGuestId; + + logger.info(`[email/init] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); + + // Проверяем, существует ли пользователь с таким email + const existingUserResult = await db.query( + `SELECT u.id FROM users u + JOIN user_identities ui ON u.id = ui.user_id + WHERE ui.provider = $1 AND ui.provider_id = $2`, + ['email', email.toLowerCase()] + ); + + let userId; + if (existingUserResult.rows.length > 0) { + // Используем существующего пользователя + userId = existingUserResult.rows[0].id; + logger.info(`[email/init] Found existing user with ID ${userId} for email ${email}`); + } else if (req.session.authenticated && req.session.userId) { + // Используем текущего аутентифицированного пользователя + userId = req.session.userId; + logger.info(`[email/init] Using current authenticated user with ID ${userId}`); + } else { + // Создаем нового пользователя + const userResult = await db.query( + 'INSERT INTO users (role) VALUES ($1) RETURNING id', + ['user'] + ); + + userId = userResult.rows[0].id; + req.session.tempUserId = userId; + logger.info(`[email/init] Created new user with ID ${userId} for email ${email}`); + } + + // Сохраняем email в сессии + req.session.pendingEmail = email.toLowerCase(); + + // Генерируем код верификации + const code = await verificationService.createVerificationCode('email', email.toLowerCase(), userId); + + // Инициализируем верификацию через email бот + const result = await emailBot.initEmailVerification(email, userId, code); + + if (!result.success) { + return res.status(500).json({ + success: false, + error: 'Ошибка при отправке кода верификации' + }); + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('Error saving session:', err); + reject(err); + } else { + logger.info(`Session saved successfully with pendingEmail: ${email}`); + resolve(); + } + }); + }); + + // Связываем гостевые сообщения с пользователем + await linkGuestMessagesAfterAuth(req.session, userId); + + return res.json({ + success: true, + message: 'Код верификации отправлен на email' + }); + + } catch (error) { + logger.error('Error in email auth initialization:', error); + res.status(500).json({ + success: false, + error: 'Внутренняя ошибка сервера' + }); + } +}); + +// Проверка кода подтверждения email +router.post('/email/verify', requireAuth, async (req, res) => { + try { + const { code } = req.body; + if (!code) { + return res.status(400).json({ error: 'Verification code is required' }); + } + + const result = await emailAuth.checkEmailVerification(code); + res.json(result); + } catch (error) { + console.error('Error verifying email code:', error); + res.status(400).json({ error: error.message }); + } +}); + +// Проверка кода верификации email +router.post('/email/check-verification', async (req, res) => { + try { + const { code } = req.body; + + if (!code) { + return res.status(400).json({ + success: false, + message: 'Код верификации не предоставлен' + }); + } + + // Сохраняем гостевой ID до проверки кода + const guestId = req.session.guestId; + const previousGuestId = req.session.previousGuestId; + + logger.info(`[email/check-verification] Checking verification with code ${code}, guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); + + // Проверяем код через сервис верификации + const result = await emailAuth.checkEmailVerification(code, req.session); + + if (!result.verified) { + logger.warn(`[email/check-verification] Invalid code: ${result.message}`); + // Преобразуем ответ для совместимости с фронтендом + return res.json({ + success: false, + message: result.message + }); + } + + logger.info(`[email/check-verification] Email verification successful for userId: ${result.userId}, email: ${result.email}`); + + // Код верный, обновляем сессию + req.session.authenticated = true; + req.session.userId = result.userId; + req.session.authType = 'email'; + req.session.email = result.email; + + // Восстанавливаем гостевой ID, если он был потерян в процессе верификации + if (!req.session.guestId && guestId) { + req.session.guestId = guestId; + logger.info(`[email/check-verification] Restored guestId ${guestId}`); + } + + if (!req.session.previousGuestId && previousGuestId) { + req.session.previousGuestId = previousGuestId; + logger.info(`[email/check-verification] Restored previous guest ID ${previousGuestId}`); + } + + // Получаем роль пользователя + const roleResult = await db.query( + 'SELECT role FROM users WHERE id = $1', + [result.userId] + ); + + if (roleResult.rows.length > 0) { + req.session.userRole = roleResult.rows[0].role || 'user'; + logger.info(`[email/check-verification] User role: ${req.session.userRole}`); + } else { + req.session.userRole = 'user'; + logger.info(`[email/check-verification] User role not found, setting default: user`); + } + + // Явно добавляем email в таблицу user_identities + await saveUserIdentity(result.userId, 'email', result.email.toLowerCase(), true); + logger.info(`[email/check-verification] Email identity ${result.email} saved for user ${result.userId}`); + + // Сохраняем сессию + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[email/check-verification] Error saving session:', err); + reject(err); + } else { + logger.info(`[email/check-verification] Session saved successfully for email ${result.email}`); + resolve(); + } + }); + }); + + // Связываем гостевые сообщения с пользователем + const linkResults = await linkGuestMessagesAfterAuth(req.session, result.userId); + logger.info(`[email/check-verification] Guest messages linking results:`, linkResults); + + return res.json({ + success: true, + userId: result.userId, + email: result.email + }); + } catch (error) { + logger.error('[email/check-verification] Error:', error); + return res.status(500).json({ + success: false, + message: 'Ошибка при проверке кода верификации' + }); + } +}); + +// Маршрут для имитации отправки email +router.post('/email/send', async (req, res) => { + try { + const { code } = req.body; + + if (!code) { + return res.status(400).json({ error: 'Code is required' }); + } + + const result = await emailAuth.markEmailAsSent(code); + + if (result.success) { + return res.json({ success: true }); + } else { + return res.status(400).json({ error: result.message }); + } + } catch (error) { + console.error('Error marking email as sent:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Обертка для функции processGuestMessages +async function processGuestMessagesWrapper(userId, guestId) { + try { + logger.info(`[processGuestMessagesWrapper] Processing messages: userId=${userId}, guestId=${guestId}`); + return await processGuestMessages(userId, guestId); + } catch (error) { + logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error); + throw error; + } +} + +// Функция для сохранения идентификатора пользователя в базу данных +async function saveUserIdentity(userId, provider, providerId, verified = true) { + try { + logger.info(`[saveUserIdentity] Saving identity for user ${userId}: ${provider}:${providerId}`); + + // Проверяем, существует ли уже такой идентификатор + const existingResult = await db.query( + `SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, + [provider, providerId] + ); + + if (existingResult.rows.length > 0) { + const existingUserId = existingResult.rows[0].user_id; + + // Если идентификатор уже принадлежит этому пользователю, ничего не делаем + if (existingUserId === userId) { + logger.info(`[saveUserIdentity] Identity ${provider}:${providerId} already exists for user ${userId}`); + } else { + // Если идентификатор принадлежит другому пользователю, логируем это + logger.warn(`[saveUserIdentity] Identity ${provider}:${providerId} already belongs to user ${existingUserId}, not user ${userId}`); + return { + success: false, + error: `Identity already belongs to another user (${existingUserId})` + }; + } + } else { + // Создаем новую запись + await db.query( + `INSERT INTO user_identities (user_id, provider, provider_id) + VALUES ($1, $2, $3)`, + [userId, provider, providerId] + ); + logger.info(`[saveUserIdentity] Created new identity ${provider}:${providerId} for user ${userId}`); + } + + return { success: true }; + } catch (error) { + logger.error(`[saveUserIdentity] Error saving identity ${provider}:${providerId} for user ${userId}:`, error); + return { success: false, error: error.message }; + } +} + +// Функция для связывания гостевых сообщений после аутентификации +async function linkGuestMessagesAfterAuth(session, userId) { + try { + logger.info(`[linkGuestMessagesAfterAuth] Starting for user ${userId} with guestId=${session.guestId}, previousGuestId=${session.previousGuestId}`); + + // Инициализируем массив обработанных гостевых ID, если его нет + if (!session.processedGuestIds) { + session.processedGuestIds = []; + logger.info('[linkGuestMessagesAfterAuth] Initialized processedGuestIds array for session'); + } + + // Получаем все гостевые ID для текущего пользователя + const guestIdsResult = await db.query( + 'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2', + [userId, 'guest'] + ); + const userGuestIds = guestIdsResult.rows.map(row => row.provider_id); + + // Обрабатываем текущий гостевой ID + if (session.guestId && !session.processedGuestIds.includes(session.guestId)) { + logger.info(`[linkGuestMessagesAfterAuth] Processing current guest ID ${session.guestId} for user ${userId}`); + await processGuestMessagesWrapper(userId, session.guestId); + session.processedGuestIds.push(session.guestId); + } + + // Обрабатываем предыдущий гостевой ID + if (session.previousGuestId && !session.processedGuestIds.includes(session.previousGuestId)) { + logger.info(`[linkGuestMessagesAfterAuth] Processing previous guest ID ${session.previousGuestId} for user ${userId}`); + await processGuestMessagesWrapper(userId, session.previousGuestId); + session.processedGuestIds.push(session.previousGuestId); + } + + // Обрабатываем все гостевые ID пользователя + for (const guestId of userGuestIds) { + if (!session.processedGuestIds.includes(guestId)) { + logger.info(`[linkGuestMessagesAfterAuth] Processing user's guest ID ${guestId} for user ${userId}`); + await processGuestMessagesWrapper(userId, guestId); + session.processedGuestIds.push(guestId); + } + } + + // Сохраняем сессию + await new Promise((resolve, reject) => { + session.save(err => { + if (err) { + logger.error('[linkGuestMessagesAfterAuth] Error saving session:', err); + reject(err); + } else { + logger.info('[linkGuestMessagesAfterAuth] Session saved successfully after guest ID processing'); + resolve(); + } + }); + }); + + return { success: true }; + } catch (error) { + logger.error('[linkGuestMessagesAfterAuth] Error:', error); + return { success: false, error: error.message }; + } +} + +// Маршрут для получения всех идентификаторов пользователя +router.get('/identities', requireAuth, async (req, res) => { + try { + const { userId } = req.session; + + // Получаем все идентификаторы пользователя + const identitiesResult = await db.query( + `SELECT provider, provider_id FROM user_identities WHERE user_id = $1`, + [userId] + ); + + const identities = identitiesResult.rows; + + res.json({ + success: true, + identities + }); + } catch (error) { + logger.error('Error getting user identities:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + +// Аутентификация через wallet +router.post('/wallet', async (req, res) => { + try { + const { address, nonce, signature } = req.body; + + if (!address || !nonce || !signature) { + return res.status(400).json({ + success: false, + error: 'Missing required fields' + }); + } + + logger.info(`[wallet] Authentication request for address: ${address}`); + + // Сохраняем гостевые ID до аутентификации + const guestId = req.session.guestId; + const previousGuestId = req.session.previousGuestId; + + logger.info(`[wallet] Guest context: guestId=${guestId}, previousGuestId=${previousGuestId}`); + + // Формируем сообщение для проверки + const message = `Sign this message to authenticate with HB3 DApp: ${nonce}`; + + // Проверяем подпись + const validSignature = await authService.verifySignature(message, signature, address); + if (!validSignature) { + logger.warn(`[wallet] Invalid signature for address: ${address}`); + return res.status(401).json({ + success: false, + error: 'Invalid signature' + }); + } + + logger.info(`[wallet] Valid signature from address: ${address}`); + + // Получаем или создаем пользователя + const { userId } = await authService.findOrCreateUser(address); + logger.info(`[wallet] User ID for address ${address}: ${userId}`); + + // Проверяем наличие админских токенов + const isAdmin = await authService.checkAdminTokens(address); + logger.info(`[wallet] Admin status for ${address}: ${isAdmin}`); + + // Обновляем роль пользователя в базе данных, если нужно + if (isAdmin) { + try { + await db.query( + 'UPDATE users SET role = $1 WHERE id = $2', + ['admin', userId] + ); + logger.info(`[wallet] Updated user ${userId} role to admin`); + } catch (updateError) { + logger.error(`[wallet] Error updating user role:`, updateError); + } + } + + // Явно сохраняем wallet адрес как идентификатор пользователя + await saveUserIdentity(userId, 'wallet', address.toLowerCase(), true); + logger.info(`[wallet] Saved wallet identity ${address.toLowerCase()} for user ${userId}`); + + // Если есть гостевые ID, сохраняем их + if (guestId) { + await saveUserIdentity(userId, 'guest', guestId, true); + logger.info(`[wallet] Saved guest ID ${guestId} for user ${userId}`); + } + + if (previousGuestId && previousGuestId !== guestId) { + await saveUserIdentity(userId, 'guest', previousGuestId, true); + logger.info(`[wallet] Saved previous guest ID ${previousGuestId} for user ${userId}`); + } + + // Устанавливаем сессию + req.session.userId = userId; + req.session.address = address.toLowerCase(); + req.session.authType = 'wallet'; + req.session.authenticated = true; + req.session.isAdmin = isAdmin; + + // Сохраняем сессию перед связыванием сообщений + await new Promise((resolve, reject) => { + req.session.save(err => { + if (err) { + logger.error('[wallet] Error saving session:', err); + reject(err); + } else { + logger.info(`[wallet] Session saved successfully for user ${userId}, address ${address}`); + resolve(); + } + }); + }); + + // Связываем гостевые сообщения с пользователем + const linkResults = await linkGuestMessagesAfterAuth(req.session, userId); + logger.info(`[wallet] Guest messages linking results:`, linkResults); + + // Возвращаем успешный ответ + return res.json({ + success: true, + userId, + address, + isAdmin, + authenticated: true + }); + + } catch (error) { + logger.error('[wallet] Error:', error); + res.status(500).json({ + success: false, + error: 'Server error during wallet authentication' + }); + } +}); + +// Маршрут для обработки гостевых сообщений после аутентификации +router.post('/link-guest-messages', requireAuth, async (req, res) => { + try { + const userId = req.user.id; + const { currentGuestId } = req.body; + + if (!currentGuestId) { + return res.status(400).json({ + success: false, + error: 'Guest ID is required' + }); + } + + // Создаем временную сессию для совместимости + const tempSession = { + guestId: currentGuestId, + save: async (callback) => { + if (typeof callback === 'function') { + callback(null); + } + return Promise.resolve(); + } + }; + + const result = await linkGuestMessagesAfterAuth(tempSession, userId); + + res.json(result); + } catch (error) { + logger.error('Error in /link-guest-messages:', error); + res.status(500).json({ + success: false, + error: 'Failed to process guest messages' + }); + } +}); + +// Маршрут для проверки и инициализации сессии гостя +router.get('/check-session', async (req, res) => { + try { + // Если у пользователя нет guestId, создаем его + if (!req.session.guestId && !req.session.authenticated) { + req.session.guestId = crypto.randomBytes(16).toString('hex'); + await req.session.save(); + logger.info('Created new guestId:', req.session.guestId); + } + + res.json({ + success: true, + guestId: req.session.guestId, + isAuthenticated: req.session.authenticated || false + }); + } catch (error) { + logger.error('Error checking session:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + +// Маршрут для проверки баланса токенов +router.get('/check-tokens/:address', async (req, res) => { + try { + const { address } = req.params; + + // Проверяем баланс токенов на разных сетях + const balances = { + eth: '0', + bsc: '0', + arbitrum: '0', + polygon: '0' + }; + + try { + balances.eth = await authService.getTokenBalance(address, 'eth'); + } catch (error) { + logger.error(`Error checking ETH balance: ${error.message}`); + } + + try { + balances.bsc = await authService.getTokenBalance(address, 'bsc'); + } catch (error) { + logger.error(`Error checking BSC balance: ${error.message}`); + } + + try { + balances.arbitrum = await authService.getTokenBalance(address, 'arbitrum'); + } catch (error) { + logger.error(`Error checking Arbitrum balance: ${error.message}`); + } + + try { + balances.polygon = await authService.getTokenBalance(address, 'polygon'); + } catch (error) { + logger.error(`Error checking Polygon balance: ${error.message}`); + } + + res.json({ + success: true, + balances + }); + } catch (error) { + logger.error('Error checking token balances:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/routes/chat.js b/backend/routes/chat.js index a03416f..f0a5fd6 100644 --- a/backend/routes/chat.js +++ b/backend/routes/chat.js @@ -13,6 +13,27 @@ async function processGuestMessages(userId, guestId) { try { console.log(`Processing guest messages for user ${userId} with guest ID ${guestId}`); + // Проверяем, обрабатывались ли уже эти сообщения + const mappingCheck = await db.query( + 'SELECT processed FROM guest_user_mapping WHERE guest_id = $1', + [guestId] + ); + + // Если сообщения уже обработаны, пропускаем + if (mappingCheck.rows.length > 0 && mappingCheck.rows[0].processed) { + console.log(`Guest messages for guest ID ${guestId} were already processed.`); + return { success: true, message: 'Guest messages already processed' }; + } + + // Проверяем наличие mapping записи и создаем если нет + if (mappingCheck.rows.length === 0) { + 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, guestId] + ); + console.log(`Created mapping for guest ID ${guestId} to user ${userId}`); + } + // Получаем все гостевые сообщения const guestMessagesResult = await db.query( 'SELECT * FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC', @@ -21,6 +42,13 @@ async function processGuestMessages(userId, guestId) { if (guestMessagesResult.rows.length === 0) { console.log('No guest messages found'); + + // Помечаем как обработанные, даже если сообщений нет + await db.query( + 'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', + [guestId] + ); + return { success: true, message: 'No guest messages found' }; } @@ -52,9 +80,9 @@ async function processGuestMessages(userId, guestId) { // Сохраняем сообщение пользователя const userMessageResult = await db.query( `INSERT INTO messages - (conversation_id, content, sender_type, role, channel, created_at) + (conversation_id, content, sender_type, role, channel, created_at, user_id) VALUES - ($1, $2, $3, $4, $5, $6) + ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, [ conversation.id, @@ -62,7 +90,8 @@ async function processGuestMessages(userId, guestId) { 'user', 'user', 'web', - guestMessage.created_at + guestMessage.created_at, + userId // Добавляем userId в сообщение для прямой связи ] ); @@ -79,9 +108,9 @@ async function processGuestMessages(userId, guestId) { // Сохраняем ответ от ИИ const aiMessageResult = await db.query( `INSERT INTO messages - (conversation_id, content, sender_type, role, channel, created_at) + (conversation_id, content, sender_type, role, channel, created_at, user_id) VALUES - ($1, $2, $3, $4, $5, $6) + ($1, $2, $3, $4, $5, $6, $7) RETURNING *`, [ conversation.id, @@ -89,7 +118,8 @@ async function processGuestMessages(userId, guestId) { 'assistant', 'assistant', 'web', - new Date() + new Date(), + userId // Добавляем userId в сообщение для прямой связи ] ); @@ -105,6 +135,12 @@ async function processGuestMessages(userId, guestId) { if (savedMessageIds.length > 0) { await db.query('DELETE FROM guest_messages WHERE id = ANY($1)', [savedMessageIds]); console.log(`Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}`); + + // Помечаем гостевой ID как обработанный + await db.query( + 'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', + [guestId] + ); } else { console.log('No guest messages were successfully processed, skipping deletion'); } diff --git a/backend/routes/identities.js b/backend/routes/identities.js index ebaa02e..f152fc8 100644 --- a/backend/routes/identities.js +++ b/backend/routes/identities.js @@ -59,6 +59,9 @@ router.get('/token-balances', requireAuth, async (req, res) => { return res.status(404).json({ error: 'No wallet linked' }); } + // Здесь логирование инициирования получения баланса может быть полезно + logger.info(`Fetching token balances for user ${userId} with wallet ${wallet}`); + // Получаем балансы токенов const balances = await authService.getTokenBalances(wallet); diff --git a/backend/routes/tokens.js b/backend/routes/tokens.js index 72b621b..54d743b 100644 --- a/backend/routes/tokens.js +++ b/backend/routes/tokens.js @@ -18,7 +18,6 @@ router.get('/balances', requireAuth, async (req, res) => { logger.info(`Fetching token balances for address: ${address}`); const balances = await authService.getTokenBalances(address); - logger.info(`Token balances fetched for ${address}:`, balances); res.json(balances); } catch (error) { logger.error('Error fetching token balances:', error); diff --git a/backend/services/auth-service.js b/backend/services/auth-service.js index d3b9c84..dfc5ed5 100644 --- a/backend/services/auth-service.js +++ b/backend/services/auth-service.js @@ -397,6 +397,28 @@ class AuthService { try { logger.info(`[verifyTelegramAuth] Starting for telegramId: ${telegramId}`); + let userId; + let isNewUser = false; + + // Проверяем наличие аутентифицированного пользователя в сессии + if (session && session.authenticated && session.userId) { + // Если есть авторизованный пользователь в сессии, связываем Telegram с ним + userId = session.userId; + logger.info(`[verifyTelegramAuth] Using existing authenticated user ${userId} from session`); + + // Связываем Telegram с текущим пользователем + await this.linkIdentity(userId, 'telegram', telegramId); + + return { + success: true, + userId, + role: session.isAdmin ? 'admin' : 'user', + telegramId, + isNewUser: false + }; + } + + // Если в сессии нет авторизованного пользователя, проверяем существующие идентификаторы // Проверяем, существует ли уже пользователь с таким Telegram ID const existingUserResult = await db.query( `SELECT u.*, ui.provider, ui.provider_id @@ -406,9 +428,6 @@ class AuthService { [telegramId] ); - let userId; - let isNewUser = false; - // Если пользователь существует с таким telegramId, используем его if (existingUserResult.rows.length > 0) { const existingUser = existingUserResult.rows[0]; @@ -458,9 +477,8 @@ class AuthService { async checkAdminTokens(address) { if (!address) return false; - console.log(`Checking admin tokens for address: ${address}`); + logger.info(`Checking admin tokens for address: ${address}`); const isAdmin = await this.checkAdminRole(address); - console.log(`Admin token check result for ${address}: ${isAdmin}`); // Обновляем роль пользователя в базе данных, если есть админские токены if (isAdmin) { @@ -480,10 +498,10 @@ class AuthService { 'UPDATE users SET role = $1 WHERE id = $2', ['admin', userId] ); - console.log(`Updated user ${userId} role to admin based on token holdings`); + logger.info(`Updated user ${userId} role to admin based on token holdings`); } } catch (error) { - console.error('Error updating user role:', error); + logger.error('Error updating user role:', error); } } @@ -564,6 +582,79 @@ class AuthService { } } + /** + * Связывает новый идентификатор с существующим пользователем + * @param {number} userId - ID пользователя + * @param {string} provider - Тип идентификатора (wallet, email, telegram) + * @param {string} providerId - Значение идентификатора + * @returns {Promise} - Результат операции + */ + async linkIdentity(userId, provider, providerId) { + try { + if (!userId || !provider || !providerId) { + logger.warn(`[AuthService] Missing parameters for linkIdentity: userId=${userId}, provider=${provider}, providerId=${providerId}`); + throw new Error('Missing parameters'); + } + + // Нормализуем значение идентификатора + if (provider === 'wallet' && providerId) { + providerId = providerId.toLowerCase(); + } else if (provider === 'email' && providerId) { + providerId = providerId.toLowerCase(); + } + + logger.info(`[AuthService] Linking identity ${provider}:${providerId} to user ${userId}`); + + // Проверяем, существует ли уже такой идентификатор + const existingResult = await db.query( + `SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`, + [provider, providerId] + ); + + if (existingResult.rows.length > 0) { + const existingUserId = existingResult.rows[0].user_id; + + // Если идентификатор уже принадлежит этому пользователю, ничего не делаем + if (existingUserId === userId) { + logger.info(`[AuthService] Identity ${provider}:${providerId} 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}`); + throw new Error(`Identity already belongs to another user (${existingUserId})`); + } + } + + // Добавляем новый идентификатор для пользователя + await db.query( + `INSERT INTO user_identities (user_id, provider, provider_id) + VALUES ($1, $2, $3)`, + [userId, provider, providerId] + ); + + // Проверяем и обновляем роль администратора, если это идентификатор кошелька + let isAdmin = false; + if (provider === 'wallet') { + isAdmin = await this.checkAdminTokens(providerId); + + // Обновляем роль пользователя в базе данных, если нужно + if (isAdmin) { + await db.query( + 'UPDATE users SET role = $1 WHERE id = $2', + ['admin', userId] + ); + logger.info(`[AuthService] Updated user ${userId} role to admin based on token holdings`); + } + } + + logger.info(`[AuthService] Identity ${provider}:${providerId} successfully linked to user ${userId}`); + return { success: true, isAdmin }; + } catch (error) { + logger.error(`[AuthService] Error linking identity ${provider}:${providerId} to user ${userId}:`, error); + throw error; + } + } + /** * Обработка гостевых сообщений после аутентификации * ПРИМЕЧАНИЕ: Эта функция оставлена для обратной совместимости. diff --git a/backend/services/emailAuth.js b/backend/services/emailAuth.js index 4ad9778..2a8d1ce 100644 --- a/backend/services/emailAuth.js +++ b/backend/services/emailAuth.js @@ -16,18 +16,34 @@ class EmailAuth { throw new Error('Некорректный формат email'); } + // Проверяем, существует ли пользователь с таким email + const existingEmailUser = await db.query( + `SELECT u.id FROM users u + JOIN user_identities i ON u.id = i.user_id + WHERE i.provider = 'email' AND i.provider_id = $1`, + [email.toLowerCase()] + ); + // Создаем или получаем ID пользователя let userId; if (session.authenticated && session.userId) { + // Если пользователь уже аутентифицирован, используем его ID userId = session.userId; + logger.info(`[initEmailAuth] Using existing authenticated user ${userId} for email ${email}`); + } else if (existingEmailUser.rows.length > 0) { + // Если найден пользователь с таким email, используем его ID + userId = existingEmailUser.rows[0].id; + logger.info(`[initEmailAuth] Found existing user ${userId} with email ${email}`); } else { + // Создаем временного пользователя, если нужно будет создать нового const userResult = await db.query( 'INSERT INTO users (role) VALUES ($1) RETURNING id', ['user'] ); userId = userResult.rows[0].id; session.tempUserId = userId; + logger.info(`[initEmailAuth] Created temporary user ${userId} for email ${email}`); } // Сохраняем email в сессии @@ -73,7 +89,25 @@ class EmailAuth { const email = session.pendingEmail.toLowerCase(); let finalUserId; - // Ищем всех пользователей с похожими идентификаторами + // Если пользователь уже авторизован, используем его ID + if (session.authenticated && session.userId) { + finalUserId = session.userId; + logger.info(`[checkEmailVerification] Using existing authenticated user ${finalUserId}`); + + // Связываем email с существующим пользователем + await authService.linkIdentity(finalUserId, 'email', email); + + // Очищаем временные данные + delete session.pendingEmail; + + return { + verified: true, + userId: finalUserId, + email: email + }; + } + + // Если пользователь не авторизован, ищем всех пользователей с похожими идентификаторами const identities = { email: email, guest: session.guestId diff --git a/backend/services/identity-service.js b/backend/services/identity-service.js index 67f97f7..b797958 100644 --- a/backend/services/identity-service.js +++ b/backend/services/identity-service.js @@ -8,7 +8,7 @@ class IdentityService { /** * Сохраняет идентификатор пользователя в базу данных * @param {number} userId - ID пользователя - * @param {string} provider - Тип идентификатора (wallet, email, telegram, guest) + * @param {string} provider - Тип идентификатора (wallet, email, telegram) * @param {string} providerId - Значение идентификатора * @param {boolean} verified - Флаг верификации идентификатора * @returns {Promise} - Результат операции @@ -23,6 +23,38 @@ class IdentityService { }; } + // Приводим provider и providerId к нужному формату + provider = provider.toLowerCase(); + if (provider === 'wallet' || provider === 'email') { + providerId = providerId.toLowerCase(); + } + + // Проверяем тип провайдера и перенаправляем гостевые идентификаторы в guest_user_mapping + if (provider === 'guest') { + logger.info(`[IdentityService] Converting guest identity for user ${userId} to guest_user_mapping: ${providerId}`); + + 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] + ); + return { success: true }; + } catch (guestError) { + logger.error(`[IdentityService] Error saving guest identity for user ${userId}:`, guestError); + return { success: false, error: guestError.message }; + } + } + + // Проверяем, разрешен ли такой тип провайдера + const allowedProviders = ['email', 'wallet', 'telegram', 'username']; + if (!allowedProviders.includes(provider)) { + logger.warn(`[IdentityService] Invalid provider type: ${provider}`); + return { + success: false, + error: `Invalid provider type. Allowed types: ${allowedProviders.join(', ')}` + }; + } + logger.info(`[IdentityService] Saving identity for user ${userId}: ${provider}:${providerId}`); // Проверяем, существует ли уже такой идентификатор @@ -177,15 +209,31 @@ class IdentityService { results.push({ type: 'telegram', result: telegramResult }); } - // Сохраняем гостевые идентификаторы + // Сохраняем гостевые идентификаторы в guest_user_mapping if (session.guestId) { - const guestResult = await this.saveIdentity(userId, 'guest', session.guestId, true); - results.push({ type: 'guest', result: guestResult }); + 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, session.guestId] + ); + results.push({ type: 'guest', result: { success: true } }); + } catch (error) { + logger.error(`[IdentityService] Error saving guest ID for user ${userId}:`, error); + results.push({ type: 'guest', result: { success: false, error: error.message } }); + } } if (session.previousGuestId && session.previousGuestId !== session.guestId) { - const prevGuestResult = await this.saveIdentity(userId, 'guest', session.previousGuestId, true); - results.push({ type: 'previousGuest', result: prevGuestResult }); + 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, session.previousGuestId] + ); + results.push({ type: 'previousGuest', result: { success: true } }); + } catch (error) { + logger.error(`[IdentityService] Error saving previous guest ID for user ${userId}:`, error); + results.push({ type: 'previousGuest', result: { success: false, error: error.message } }); + } } logger.info(`[IdentityService] Saved ${results.length} identities from session for user ${userId}`); @@ -223,12 +271,42 @@ class IdentityService { // Переносим каждый идентификатор for (const identity of identitiesResult.rows) { await client.query( - `UPDATE user_identities - SET user_id = $1 - WHERE user_id = $2 AND provider = $3 AND provider_id = $4`, - [toUserId, fromUserId, identity.provider, identity.provider_id] + `INSERT INTO user_identities (user_id, provider, provider_id) + VALUES ($1, $2, $3) + ON CONFLICT (provider, provider_id) DO NOTHING`, + [toUserId, identity.provider, identity.provider_id] + ); + + // Удаляем старый идентификатор + await client.query( + `DELETE FROM user_identities + WHERE user_id = $1 AND provider = $2 AND provider_id = $3`, + [fromUserId, identity.provider, identity.provider_id] ); } + + // Мигрируем гостевые идентификаторы из новой таблицы guest_user_mapping + const guestMappingsResult = await client.query( + `SELECT guest_id, processed FROM guest_user_mapping WHERE user_id = $1`, + [fromUserId] + ); + + // Переносим каждый гостевой идентификатор + for (const mapping of guestMappingsResult.rows) { + await client.query( + `INSERT INTO guest_user_mapping (user_id, guest_id, processed) + VALUES ($1, $2, $3) + ON CONFLICT (guest_id) DO UPDATE + SET user_id = $1, processed = EXCLUDED.processed OR guest_user_mapping.processed`, + [toUserId, mapping.guest_id, mapping.processed] + ); + } + + // Удаляем старые гостевые маппинги + await client.query( + `DELETE FROM guest_user_mapping WHERE user_id = $1`, + [fromUserId] + ); // Переносим все сообщения await client.query( @@ -245,28 +323,29 @@ class IdentityService { WHERE user_id = $2`, [toUserId, fromUserId] ); - - // Удаляем исходного пользователя + + // Переносим настройки пользователя await client.query( - `DELETE FROM users WHERE id = $1`, - [fromUserId] + `UPDATE user_preferences + SET user_id = $1 + WHERE user_id = $2`, + [toUserId, fromUserId] ); + // Завершаем транзакцию await client.query('COMMIT'); - logger.info(`[IdentityService] Successfully migrated data from user ${fromUserId} to user ${toUserId}`); - return { - success: true, - migratedIdentities: identitiesResult.rows.length - }; + logger.info(`[IdentityService] Successfully migrated data from user ${fromUserId} to ${toUserId}`); + return { success: true }; } catch (error) { await client.query('ROLLBACK'); - throw error; + logger.error(`[IdentityService] Transaction error:`, error); + return { success: false, error: error.message }; } finally { client.release(); } } catch (error) { - logger.error(`[IdentityService] Error migrating data from user ${fromUserId} to user ${toUserId}:`, error); + logger.error(`[IdentityService] Error migrating user data:`, error); return { success: false, error: error.message }; } } diff --git a/backend/services/session-service.js b/backend/services/session-service.js index ca19836..4ad2aeb 100644 --- a/backend/services/session-service.js +++ b/backend/services/session-service.js @@ -1,40 +1,167 @@ const logger = require('../utils/logger'); const db = require('../db'); +const { processGuestMessages } = require('../routes/chat'); /** * Сервис для работы с сессиями пользователей */ class SessionService { /** - * Сохраняет сессию с обработкой ошибок - * @param {object} session - Объект сессии - * @param {string} context - Контекст для логирования - * @returns {Promise} - Результат операции + * Сохраняет сессию, обрабатывая ошибки и логируя результат + * @param {Object} session - Объект сессии Express + * @returns {Promise} - Успешно ли сохранена сессия */ - async saveSession(session, context = '') { - if (!session) { - logger.warn(`[SessionService${context ? '/' + context : ''}] Cannot save null session`); - return false; - } - + async saveSession(session) { try { - return await new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { session.save(err => { if (err) { - logger.error(`[SessionService${context ? '/' + context : ''}] Error saving session:`, err); + logger.error('Error saving session:', err); reject(err); } else { - logger.info(`[SessionService${context ? '/' + context : ''}] Session saved successfully`); + logger.info('Session saved successfully'); resolve(true); } }); }); } catch (error) { - logger.error(`[SessionService${context ? '/' + context : ''}] Error in saveSession:`, error); - return false; + logger.error(`[saveSession] Error:`, error); + throw error; } } - + + /** + * Связывает гостевые сообщения с пользователем после аутентификации + * @param {Object} session - Объект сессии Express + * @param {number} userId - ID пользователя + * @returns {Promise} - Результат операции + */ + async linkGuestMessages(session, userId) { + try { + logger.info(`[linkGuestMessages] Starting for user ${userId} with guestId=${session.guestId}, previousGuestId=${session.previousGuestId}`); + + // Инициализируем массив обработанных гостевых ID, если его нет + if (!session.processedGuestIds) { + session.processedGuestIds = []; + } + + // Получаем все гостевые ID для текущего пользователя из новой таблицы + const guestIdsResult = await db.query( + 'SELECT guest_id FROM guest_user_mapping WHERE user_id = $1', + [userId] + ); + const userGuestIds = guestIdsResult.rows.map(row => row.guest_id); + + // Собираем все гостевые ID, которые нужно обработать + const guestIdsToProcess = new Set(); + + // Добавляем текущий гостевой ID + if (session.guestId && !session.processedGuestIds.includes(session.guestId)) { + guestIdsToProcess.add(session.guestId); + + // Записываем связь с пользователем в новую таблицу + 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, session.guestId] + ); + } + + // Добавляем предыдущий гостевой ID + if (session.previousGuestId && !session.processedGuestIds.includes(session.previousGuestId)) { + guestIdsToProcess.add(session.previousGuestId); + + // Записываем связь с пользователем в новую таблицу + 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, session.previousGuestId] + ); + } + + // Добавляем все гостевые ID пользователя из таблицы + for (const guestId of userGuestIds) { + if (!session.processedGuestIds.includes(guestId)) { + guestIdsToProcess.add(guestId); + } + } + + // Обрабатываем все собранные гостевые ID + for (const guestId of guestIdsToProcess) { + await this.processGuestMessagesWrapper(userId, guestId); + session.processedGuestIds.push(guestId); + + // Помечаем guestId как обработанный в базе данных + await db.query( + 'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', + [guestId] + ); + } + + // Сохраняем сессию + await this.saveSession(session); + + return { success: true }; + } catch (error) { + logger.error('[linkGuestMessages] Error:', error); + return { success: false, error: error.message }; + } + } + + /** + * Обертка для функции processGuestMessages + * @param {number} userId - ID пользователя + * @param {string} guestId - ID гостя + * @returns {Promise} - Результат операции + */ + async processGuestMessagesWrapper(userId, guestId) { + try { + logger.info(`[processGuestMessagesWrapper] Processing messages: userId=${userId}, guestId=${guestId}`); + return await processGuestMessages(userId, guestId); + } catch (error) { + logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error); + throw error; + } + } + + /** + * Получает сессию из хранилища по ID + * @param {string} sessionId - ID сессии + * @returns {Promise} - Объект сессии или null + */ + async getSession(sessionId) { + try { + // Реализация будет зависеть от используемого хранилища сессий + // Этот метод будет полезен, если нужно получить сессию не из текущего запроса + return null; // Временная заглушка + } catch (error) { + logger.error(`[getSession] Error getting session ${sessionId}:`, error); + return null; + } + } + + /** + * Удаляет сессию + * @param {Object} session - Объект сессии Express + * @returns {Promise} - Успешно ли удалена сессия + */ + async destroySession(session) { + try { + return new Promise((resolve, reject) => { + session.destroy(err => { + if (err) { + logger.error('Error destroying session:', err); + reject(err); + } else { + logger.info('Session destroyed successfully'); + resolve(true); + } + }); + }); + } catch (error) { + logger.error(`[destroySession] Error:`, error); + throw error; + } + } + /** * Восстанавливает сессию из базы данных по ID * @param {string} sessionId - ID сессии @@ -166,4 +293,5 @@ class SessionService { } } -module.exports = new SessionService(); \ No newline at end of file +const sessionService = new SessionService(); +module.exports = sessionService; \ No newline at end of file diff --git a/frontend/src/composables/useAuth.js b/frontend/src/composables/useAuth.js index 357b914..887c760 100644 --- a/frontend/src/composables/useAuth.js +++ b/frontend/src/composables/useAuth.js @@ -385,6 +385,52 @@ export function useAuth() { stopIdentitiesPolling(); }); + /** + * Связывает новый идентификатор с текущим аккаунтом пользователя + * @param {string} provider - Тип идентификатора (wallet, email, telegram) + * @param {string} providerId - Значение идентификатора + * @returns {Promise} - Результат операции + */ + const linkIdentity = async (provider, providerId) => { + try { + if (!isAuthenticated.value) { + console.error('Невозможно связать идентификатор: пользователь не аутентифицирован'); + return { success: false, error: 'Пользователь не аутентифицирован' }; + } + + const response = await axios.post('/api/auth/identities/link', { + type: provider, + value: providerId + }); + + if (response.data.success) { + // Обновляем локальные данные при необходимости + if (provider === 'wallet') { + address.value = providerId; + isAdmin.value = response.data.isAdmin || false; + } else if (provider === 'telegram') { + telegramId.value = providerId; + } else if (provider === 'email') { + email.value = providerId; + } + + // Обновляем список идентификаторов + await updateIdentities(); + + console.log(`Идентификатор ${provider} успешно связан с аккаунтом`); + return { success: true }; + } + + return response.data; + } catch (error) { + console.error('Ошибка при связывании идентификатора:', error); + return { + success: false, + error: error.response?.data?.error || error.message + }; + } + }; + return { isAuthenticated, authType, @@ -402,6 +448,7 @@ export function useAuth() { linkMessages, updateIdentities, updateProcessedGuestIds, - updateConnectionDisplay + updateConnectionDisplay, + linkIdentity }; } \ No newline at end of file diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 1b38584..35aab69 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -918,7 +918,7 @@ const setupMessagePolling = (initialCount) => { * Обрабатывает аутентификацию через кошелек */ const handleWalletAuth = async () => { - if (isConnecting.value || isAuthenticated.value) return; + if (isConnecting.value) return; isConnecting.value = true; try { @@ -926,17 +926,45 @@ const handleWalletAuth = async () => { console.log('Результат подключения кошелька:', result); if (result.success) { - // Обновляем состояние авторизации - const authResponse = await auth.checkAuth(); - - if (authResponse.authenticated && authResponse.authType === 'wallet') { - console.log('Кошелёк успешно подключен и аутентифицирован'); + if (auth.isAuthenticated.value) { + // Если пользователь уже авторизован, связываем кошелек с существующим аккаунтом + console.log('Связывание кошелька с существующим аккаунтом:', result.address); + const linkResult = await auth.linkIdentity('wallet', result.address); - // Загружаем сообщения после аутентификации - await loadMessages({ authType: 'wallet' }); + if (linkResult.success) { + notifications.value.successMessage = 'Кошелек успешно подключен к вашему аккаунту!'; + notifications.value.showSuccess = true; + + // Скрываем сообщение через 3 секунды + setTimeout(() => { + notifications.value.showSuccess = false; + }, 3000); + + // Обновляем данные авторизации и балансы + await auth.checkAuth(); + startBalanceUpdates(); + } else { + notifications.value.errorMessage = linkResult.error || 'Не удалось подключить кошелек'; + notifications.value.showError = true; + + // Скрываем сообщение через 3 секунды + setTimeout(() => { + notifications.value.showError = false; + }, 3000); + } + } else { + // Если пользователь не авторизован, выполняем обычную авторизацию через кошелек + const authResponse = await auth.checkAuth(); - // Запускаем обновление балансов - startBalanceUpdates(); + if (authResponse.authenticated && authResponse.authType === 'wallet') { + console.log('Кошелёк успешно подключен и аутентифицирован'); + + // Загружаем сообщения после аутентификации + await loadMessages({ authType: 'wallet' }); + + // Запускаем обновление балансов + startBalanceUpdates(); + } } // Небольшая задержка перед сбросом состояния @@ -959,6 +987,7 @@ const handleWalletAuth = async () => { */ const handleTelegramAuth = async () => { try { + // Показываем окно верификации telegramAuth.value.showVerification = true; telegramAuth.value.error = ''; @@ -973,14 +1002,43 @@ const handleTelegramAuth = async () => { telegramAuth.value.checkInterval = setInterval(async () => { try { const checkResponse = await auth.checkAuth(); - if (checkResponse.authenticated && checkResponse.authType === 'telegram') { - console.log('Telegram аутентификация успешна'); + + // Получаем Telegram ID из проверки аутентификации + const telegramId = checkResponse.telegramId; + + if (auth.isAuthenticated.value && telegramId) { + if (auth.authType.value !== 'telegram') { + // Если пользователь авторизован не через Telegram, связываем идентификаторы + console.log('Связывание Telegram с существующим аккаунтом:', telegramId); + const linkResult = await auth.linkIdentity('telegram', telegramId); + + if (linkResult.success) { + notifications.value.successMessage = 'Telegram успешно подключен к вашему аккаунту!'; + notifications.value.showSuccess = true; + + setTimeout(() => { + notifications.value.showSuccess = false; + }, 3000); + } else { + notifications.value.errorMessage = linkResult.error || 'Не удалось подключить Telegram'; + notifications.value.showError = true; + + setTimeout(() => { + notifications.value.showError = false; + }, 3000); + } + } else { + // Если новая аутентификация через Telegram + console.log('Telegram аутентификация успешна'); + + // Загружаем сообщения после аутентификации + await loadMessages({ authType: 'telegram' }); + } + + // Очищаем интервал и скрываем окно верификации clearTelegramInterval(); telegramAuth.value.showVerification = false; telegramAuth.value.verificationCode = ''; - - // Загружаем сообщения после аутентификации - await loadMessages({ authType: 'telegram' }); } } catch (error) { console.error('Ошибка при проверке аутентификации:', error); @@ -1114,20 +1172,55 @@ const verifyEmailCode = async () => { emailAuth.value.showForm = false; emailAuth.value.showVerification = false; - // Показываем сообщение об успехе - notifications.value.successMessage = `Email ${emailAuth.value.verificationEmail} успешно подтвержден!`; - notifications.value.showSuccess = true; - - // Скрываем сообщение через 3 секунды - setTimeout(() => { - notifications.value.showSuccess = false; - }, 3000); - - // Обновляем состояние аутентификации + // Получаем текущее состояние аутентификации const authResponse = await auth.checkAuth(); - if (authResponse.authenticated && authResponse.authType === 'email') { - console.log('Email успешно подтвержден и аутентифицирован'); + if (auth.isAuthenticated.value && emailAuth.value.verificationEmail) { + // Если пользователь уже авторизован, связываем email + if (auth.authType.value !== 'email') { + console.log('Связывание Email с существующим аккаунтом:', emailAuth.value.verificationEmail); + const linkResult = await auth.linkIdentity('email', emailAuth.value.verificationEmail); + + if (linkResult.success) { + // Показываем сообщение об успехе + notifications.value.successMessage = `Email ${emailAuth.value.verificationEmail} успешно подключен к вашему аккаунту!`; + notifications.value.showSuccess = true; + + // Скрываем сообщение через 3 секунды + setTimeout(() => { + notifications.value.showSuccess = false; + }, 3000); + } else { + notifications.value.errorMessage = linkResult.error || 'Не удалось подключить Email'; + notifications.value.showError = true; + + setTimeout(() => { + notifications.value.showError = false; + }, 3000); + } + } else { + // Показываем сообщение об успехе + notifications.value.successMessage = `Email ${emailAuth.value.verificationEmail} успешно подтвержден!`; + notifications.value.showSuccess = true; + + // Скрываем сообщение через 3 секунды + setTimeout(() => { + notifications.value.showSuccess = false; + }, 3000); + + // Загружаем сообщения после аутентификации + await loadMessages({ authType: 'email' }); + } + } else { + // Если пользователь не был авторизован до этого + // Показываем сообщение об успехе + notifications.value.successMessage = `Email ${emailAuth.value.verificationEmail} успешно подтвержден!`; + notifications.value.showSuccess = true; + + // Скрываем сообщение через 3 секунды + setTimeout(() => { + notifications.value.showSuccess = false; + }, 3000); // Загружаем сообщения после аутентификации await loadMessages({ authType: 'email' });