Files
DLE/backend/services/identity-service.js
2025-10-16 18:44:30 +03:00

542 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/HB3-ACCELERATOR
*/
// console.log('[identity-service] loaded');
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const logger = require('../utils/logger');
const { getLinkedWallet } = require('./wallet-service');
const { broadcastContactsUpdate } = require('../wsHub');
/**
* Сервис для работы с идентификаторами пользователей
*/
class IdentityService {
/**
* Нормализует значения идентификаторов (приводит к нижнему регистру где нужно)
* @param {string} provider - Тип идентификатора
* @param {string} providerId - Значение идентификатора
* @returns {object} - Нормализованные значения
*/
normalizeIdentity(provider, providerId) {
if (!provider || !providerId) {
return { provider, providerId };
}
// Приводим провайдер к нижнему регистру
const normalizedProvider = provider.toLowerCase();
// Для email и wallet приводим значение к нижнему регистру
let normalizedProviderId = providerId;
if (normalizedProvider === 'wallet' || normalizedProvider === 'email') {
normalizedProviderId = providerId.toLowerCase();
}
return {
provider: normalizedProvider,
providerId: normalizedProviderId,
};
}
/**
* Сохраняет идентификатор пользователя в базу данных
* @param {number} userId - ID пользователя
* @param {string} provider - Тип идентификатора (wallet, email, telegram)
* @param {string} providerId - Значение идентификатора
* @param {boolean} verified - Флаг верификации идентификатора (не используется в БД)
* @returns {Promise<object>} - Результат операции
*/
async saveIdentity(userId, provider, providerId, verified = true) {
try {
if (!userId || !provider || !providerId) {
logger.warn(
`[IdentityService] Missing required parameters: userId=${userId}, provider=${provider}, providerId=${providerId}`
);
return {
success: false,
error: 'Missing required parameters',
};
}
// Нормализуем значения
const { provider: normalizedProvider, providerId: normalizedProviderId } =
this.normalizeIdentity(provider, providerId);
// Проверяем тип провайдера и перенаправляем гостевые идентификаторы в unified_guest_mapping
if (normalizedProvider === 'guest') {
logger.info(
`[IdentityService] Converting guest identity for user ${userId} to unified_guest_mapping: ${normalizedProviderId}`
);
try {
const db = require('../db');
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
await db.getQuery()(
`INSERT INTO unified_guest_mapping (user_id, identifier_encrypted, channel, created_at)
VALUES ($1, encrypt_text($2, $4), $3, NOW())
ON CONFLICT (identifier_encrypted, channel) DO NOTHING`,
[userId, `web:${normalizedProviderId}`, 'web', encryptionKey]
);
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(normalizedProvider)) {
logger.warn(`[IdentityService] Invalid provider type: ${normalizedProvider}`);
return {
success: false,
error: `Invalid provider type: ${normalizedProvider}`,
};
}
// Проверяем, существует ли уже такой идентификатор
const existingIdentity = await this.findIdentity(userId, normalizedProvider);
if (existingIdentity) {
// Обновляем существующий идентификатор
await encryptedDb.saveData('user_identities', {
provider: normalizedProvider,
provider_id: normalizedProviderId
}, {
user_id: userId,
provider: normalizedProvider
});
logger.info(
`[IdentityService] Updated identity for user ${userId}: ${normalizedProvider}=${normalizedProviderId}`
);
} else {
// Создаем новый идентификатор
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: normalizedProvider,
provider_id: normalizedProviderId
});
logger.info(
`[IdentityService] Saved new identity for user ${userId}: ${normalizedProvider}=${normalizedProviderId}`
);
}
return { success: true };
} catch (error) {
logger.error(
`[IdentityService] Error saving identity for user ${userId}:`,
error
);
return { success: false, error: error.message };
}
}
/**
* Получает все идентификаторы пользователя
* @param {number} userId - ID пользователя
* @returns {Promise<Array>} - Массив идентификаторов
*/
async getUserIdentities(userId) {
try {
const identities = await encryptedDb.getData('user_identities', { user_id: userId });
logger.info(`[IdentityService] Found ${identities.length} identities for user ${userId}`);
// Данные уже расшифрованы encryptedDb, просто переименовываем поля
const formattedIdentities = identities.map(identity => ({
id: identity.id,
user_id: identity.user_id,
created_at: identity.created_at,
provider: identity.provider, // Уже расшифровано
provider_id: identity.provider_id // Уже расшифровано
}));
return formattedIdentities;
} catch (error) {
logger.error(`[IdentityService] Error getting identities for user ${userId}:`, error);
// Если не удалось получить данные через encryptedDb, пробуем через обычный запрос
// logger.info(`[IdentityService] Trying to get unencrypted data for user ${userId}`); // Убрано избыточное логирование
const { rows } = await db.getQuery()(`
SELECT provider, provider_id, provider_username, provider_avatar, created_at, updated_at
FROM user_identities
WHERE user_id = $1
ORDER BY created_at DESC
`, [userId]);
// logger.info(`[IdentityService] Found ${rows.length} unencrypted identities for user ${userId}`); // Убрано избыточное логирование
return rows;
}
}
/**
* Получает идентификаторы пользователя по типу провайдера
* @param {number} userId - ID пользователя
* @param {string} provider - Тип провайдера
* @returns {Promise<Array>} - Массив идентификаторов
*/
async getUserIdentitiesByProvider(userId, provider) {
try {
const identities = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: provider.toLowerCase()
});
return identities;
} catch (error) {
logger.error(`[IdentityService] Error getting identities by provider for user ${userId}:`, error);
return [];
}
}
/**
* Находит пользователя по идентификатору
* @param {string} provider - Тип провайдера
* @param {string} providerId - Значение идентификатора
* @returns {Promise<object|null>} - Пользователь или null
*/
async findUserByIdentity(provider, providerId) {
try {
const { provider: normalizedProvider, providerId: normalizedProviderId } =
this.normalizeIdentity(provider, providerId);
const identities = await encryptedDb.getData('user_identities', {
provider: normalizedProvider,
provider_id: normalizedProviderId
}, 1);
if (identities.length === 0) {
return null;
}
const userId = identities[0].user_id;
const users = await encryptedDb.getData('users', { id: userId }, 1);
return users.length > 0 ? users[0] : null;
} catch (error) {
logger.error(`[IdentityService] Error finding user by identity:`, error);
return null;
}
}
/**
* Находит конкретный идентификатор пользователя
* @param {number} userId - ID пользователя
* @param {string} provider - Тип провайдера
* @returns {Promise<object|null>} - Идентификатор или null
*/
async findIdentity(userId, provider) {
try {
const identities = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: provider.toLowerCase()
}, 1);
return identities.length > 0 ? identities[0] : null;
} catch (error) {
logger.error(`[IdentityService] Error finding identity for user ${userId}:`, error);
return null;
}
}
/**
* Сохраняет идентификаторы из сессии для пользователя
* @param {object} session - Объект сессии
* @param {number} userId - ID пользователя
* @returns {Promise<object>} - Результат операции
*/
async saveIdentitiesFromSession(session, userId) {
try {
if (!session || !userId) {
logger.warn(`[IdentityService] Missing parameters: session=${!!session}, userId=${userId}`);
return { success: false, error: 'Missing required parameters' };
}
const results = [];
// Сохраняем все постоянные идентификаторы из сессии
if (session.email) {
const emailResult = await this.saveIdentity(userId, 'email', session.email, true);
results.push({ type: 'email', result: emailResult });
}
if (session.address) {
const walletResult = await this.saveIdentity(userId, 'wallet', session.address, true);
results.push({ type: 'wallet', result: walletResult });
}
if (session.telegramId) {
const telegramResult = await this.saveIdentity(
userId,
'telegram',
session.telegramId,
true
);
results.push({ type: 'telegram', result: telegramResult });
}
// Сохраняем гостевые идентификаторы в unified_guest_mapping
if (session.guestId) {
try {
const db = require('../db');
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
await db.getQuery()(
`INSERT INTO unified_guest_mapping (user_id, identifier_encrypted, channel, created_at)
VALUES ($1, encrypt_text($2, $4), $3, NOW())
ON CONFLICT (identifier_encrypted, channel) DO NOTHING`,
[userId, `web:${session.guestId}`, 'web', encryptionKey]
);
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) {
try {
const db = require('../db');
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
await db.getQuery()(
`INSERT INTO unified_guest_mapping (user_id, identifier_encrypted, channel, created_at)
VALUES ($1, encrypt_text($2, $4), $3, NOW())
ON CONFLICT (identifier_encrypted, channel) DO NOTHING`,
[userId, `web:${session.previousGuestId}`, 'web', encryptionKey]
);
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}`
);
return { success: true, results };
} catch (error) {
logger.error(
`[IdentityService] Error saving identities from session for user ${userId}:`,
error
);
return { success: false, error: error.message };
}
}
/**
* Мигрирует все идентификаторы и сообщения от одного пользователя к другому
* @param {number} fromUserId - ID исходного пользователя
* @param {number} toUserId - ID целевого пользователя
* @returns {Promise<object>} - Результат операции
*/
async migrateUserData(fromUserId, toUserId) {
try {
if (!fromUserId || !toUserId) {
logger.warn(
`[IdentityService] Missing parameters: fromUserId=${fromUserId}, toUserId=${toUserId}`
);
return { success: false, error: 'Missing required parameters' };
}
// Получаем все идентификаторы исходного пользователя
const identities = await encryptedDb.getData('user_identities', { user_id: fromUserId });
// Переносим каждый идентификатор
for (const identity of identities) {
// Создаем новый идентификатор для целевого пользователя
await encryptedDb.saveData('user_identities', {
user_id: toUserId,
provider: identity.provider,
provider_id: identity.provider_id
});
// Удаляем старый идентификатор
await encryptedDb.deleteData('user_identities', {
user_id: fromUserId,
provider: identity.provider,
provider_id: identity.provider_id
});
}
// Мигрируем гостевые идентификаторы
const guestMappings = await encryptedDb.getData('unified_guest_mapping', { user_id: fromUserId });
// Переносим каждый гостевой идентификатор
for (const mapping of guestMappings) {
const db = require('../db');
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
await db.getQuery()(
`INSERT INTO unified_guest_mapping (user_id, identifier_encrypted, channel, processed, processed_at, created_at)
VALUES ($1, encrypt_text($2, $6), $3, $4, $5, NOW())
ON CONFLICT (identifier_encrypted, channel) DO UPDATE SET user_id = $1, processed = $4, processed_at = $5`,
[toUserId, mapping.identifier_encrypted, mapping.channel, mapping.processed, mapping.processed_at, encryptionKey]
);
}
// Удаляем старые гостевые маппинги
await encryptedDb.deleteData('unified_guest_mapping', { user_id: fromUserId });
// Переносим все сообщения
const messages = await encryptedDb.getData('messages', { user_id: fromUserId });
for (const message of messages) {
await encryptedDb.saveData('messages', {
...message,
user_id: toUserId
});
await encryptedDb.deleteData('messages', { id: message.id });
}
// Переносим все диалоги
const conversations = await encryptedDb.getData('conversations', { user_id: fromUserId });
for (const conversation of conversations) {
await encryptedDb.saveData('conversations', {
...conversation,
user_id: toUserId
});
await encryptedDb.deleteData('conversations', { id: conversation.id });
}
// Переносим настройки пользователя
const preferences = await encryptedDb.getData('user_preferences', { user_id: fromUserId });
for (const preference of preferences) {
await encryptedDb.saveData('user_preferences', {
...preference,
user_id: toUserId
});
await encryptedDb.deleteData('user_preferences', { id: preference.id });
}
logger.info(
`[IdentityService] Successfully migrated data from user ${fromUserId} to ${toUserId}`
);
return { success: true };
} catch (error) {
logger.error(`[IdentityService] Error migrating user data:`, error);
return { success: false, error: error.message };
}
}
/**
* Находит всех пользователей с похожими идентификаторами
* @param {object} identities - Объект с идентификаторами
* @returns {Promise<Array>} - Массив ID пользователей
*/
async findRelatedUsers(identities) {
try {
const userIds = new Set();
for (const [provider, providerId] of Object.entries(identities)) {
if (!providerId) continue;
const users = await encryptedDb.getData('user_identities', {
provider: provider,
provider_id: providerId
});
users.forEach((user) => userIds.add(user.user_id));
}
return Array.from(userIds);
} catch (error) {
logger.error(`[IdentityService] Error finding related users:`, error);
return [];
}
}
/**
* Удаляет идентификатор пользователя
* @param {number} userId - ID пользователя
* @param {string} provider - Тип идентификатора
* @param {string} providerId - Значение идентификатора
* @returns {Promise<object>} - Результат операции
*/
async deleteIdentity(userId, provider, providerId) {
try {
if (!userId || !provider || !providerId) {
logger.warn(`[IdentityService] Missing parameters for deleteIdentity: userId=${userId}, provider=${provider}, providerId=${providerId}`);
return { success: false, error: 'Missing required parameters' };
}
const { provider: normalizedProvider, providerId: normalizedProviderId } = this.normalizeIdentity(provider, providerId);
const result = await encryptedDb.deleteData('user_identities', {
user_id: userId,
provider: normalizedProvider,
provider_id: normalizedProviderId
});
logger.info(`[IdentityService] Deleted identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`);
return { success: true, deleted: result.length };
} catch (error) {
logger.error(`[IdentityService] Error deleting identity ${provider}:${providerId} for user ${userId}:`, error);
return { success: false, error: error.message };
}
}
/**
* Универсальная функция: найти или создать пользователя по идентификатору, привязать идентификатор, проверить роль
* @param {string} provider - Тип идентификатора ('email' | 'telegram')
* @param {string} providerId - Значение идентификатора
* @param {object} [options] - Дополнительные опции
* @returns {Promise<{userId: number, role: string, isNew: boolean}>}
*/
async findOrCreateUserWithRole(provider, providerId, options = {}) {
let user = await this.findUserByIdentity(provider, providerId);
let isNew = false;
if (!user) {
// Создаем пользователя с централизованной ролью
const { ROLES } = require('/app/shared/permissions');
const newUser = await encryptedDb.saveData('users', {
role: ROLES.USER
});
const userId = newUser.id;
await this.saveIdentity(userId, provider, providerId, true);
user = { id: userId, role: ROLES.USER };
isNew = true;
logger.info('[WS] broadcastContactsUpdate after new user created');
broadcastContactsUpdate();
}
// Проверяем связь с кошельком
const wallet = await getLinkedWallet(user.id);
const { ROLES } = require('/app/shared/permissions');
let role = ROLES.USER;
if (wallet) {
const userAccessLevel = await authService.getUserAccessLevel(wallet);
// Используем роль из userAccessLevel, которая уже правильно определена с учетом порогов
role = userAccessLevel.level;
// Обновляем роль в users, если изменилась
if (user.role !== role) {
await encryptedDb.saveData('users', {
role: role
}, {
id: user.id
});
}
}
return { userId: user.id, role, isNew };
}
}
module.exports = new IdentityService();