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

This commit is contained in:
2025-04-15 19:30:56 +03:00
parent 3ceefd046e
commit 4330a1f2a0
14 changed files with 3234 additions and 1529 deletions

View File

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

View File

@@ -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` - количество дублирующихся идентификаторов

View File

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

File diff suppressed because it is too large Load Diff

1935
backend/routes/auth.js.bak Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -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<Object>} - Результат операции
*/
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;
}
}
/**
* Обработка гостевых сообщений после аутентификации
* ПРИМЕЧАНИЕ: Эта функция оставлена для обратной совместимости.

View File

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

View File

@@ -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<object>} - Результат операции
@@ -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,13 +271,43 @@ 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(
`UPDATE messages
@@ -246,27 +324,28 @@ class IdentityService {
[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 };
}
}

View File

@@ -1,37 +1,164 @@
const logger = require('../utils/logger');
const db = require('../db');
const { processGuestMessages } = require('../routes/chat');
/**
* Сервис для работы с сессиями пользователей
*/
class SessionService {
/**
* Сохраняет сессию с обработкой ошибок
* @param {object} session - Объект сессии
* @param {string} context - Контекст для логирования
* @returns {Promise<boolean>} - Результат операции
* Сохраняет сессию, обрабатывая ошибки и логируя результат
* @param {Object} session - Объект сессии Express
* @returns {Promise<boolean>} - Успешно ли сохранена сессия
*/
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<Object>} - Результат операции
*/
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<Object>} - Результат операции
*/
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<Object|null>} - Объект сессии или 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<boolean>} - Успешно ли удалена сессия
*/
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;
}
}
@@ -166,4 +293,5 @@ class SessionService {
}
}
module.exports = new SessionService();
const sessionService = new SessionService();
module.exports = sessionService;

View File

@@ -385,6 +385,52 @@ export function useAuth() {
stopIdentitiesPolling();
});
/**
* Связывает новый идентификатор с текущим аккаунтом пользователя
* @param {string} provider - Тип идентификатора (wallet, email, telegram)
* @param {string} providerId - Значение идентификатора
* @returns {Promise<Object>} - Результат операции
*/
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
};
}

View File

@@ -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,7 +926,34 @@ const handleWalletAuth = async () => {
console.log('Результат подключения кошелька:', result);
if (result.success) {
// Обновляем состояние авторизации
if (auth.isAuthenticated.value) {
// Если пользователь уже авторизован, связываем кошелек с существующим аккаунтом
console.log('Связывание кошелька с существующим аккаунтом:', result.address);
const linkResult = await auth.linkIdentity('wallet', result.address);
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();
if (authResponse.authenticated && authResponse.authType === 'wallet') {
@@ -938,6 +965,7 @@ const handleWalletAuth = async () => {
// Запускаем обновление балансов
startBalanceUpdates();
}
}
// Небольшая задержка перед сбросом состояния
setTimeout(() => {
@@ -959,6 +987,7 @@ const handleWalletAuth = async () => {
*/
const handleTelegramAuth = async () => {
try {
// Показываем окно верификации
telegramAuth.value.showVerification = true;
telegramAuth.value.error = '';
@@ -973,15 +1002,44 @@ const handleTelegramAuth = async () => {
telegramAuth.value.checkInterval = setInterval(async () => {
try {
const checkResponse = await auth.checkAuth();
if (checkResponse.authenticated && checkResponse.authType === '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 аутентификация успешна');
clearTelegramInterval();
telegramAuth.value.showVerification = false;
telegramAuth.value.verificationCode = '';
// Загружаем сообщения после аутентификации
await loadMessages({ authType: 'telegram' });
}
// Очищаем интервал и скрываем окно верификации
clearTelegramInterval();
telegramAuth.value.showVerification = false;
telegramAuth.value.verificationCode = '';
}
} catch (error) {
console.error('Ошибка при проверке аутентификации:', error);
}
@@ -1114,6 +1172,33 @@ const verifyEmailCode = async () => {
emailAuth.value.showForm = false;
emailAuth.value.showVerification = false;
// Получаем текущее состояние аутентификации
const authResponse = await auth.checkAuth();
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;
@@ -1123,11 +1208,19 @@ const verifyEmailCode = async () => {
notifications.value.showSuccess = false;
}, 3000);
// Обновляем состояние аутентификации
const authResponse = await auth.checkAuth();
// Загружаем сообщения после аутентификации
await loadMessages({ authType: 'email' });
}
} else {
// Если пользователь не был авторизован до этого
// Показываем сообщение об успехе
notifications.value.successMessage = `Email ${emailAuth.value.verificationEmail} успешно подтвержден!`;
notifications.value.showSuccess = true;
if (authResponse.authenticated && authResponse.authType === 'email') {
console.log('Email успешно подтвержден и аутентифицирован');
// Скрываем сообщение через 3 секунды
setTimeout(() => {
notifications.value.showSuccess = false;
}, 3000);
// Загружаем сообщения после аутентификации
await loadMessages({ authType: 'email' });