560 lines
22 KiB
JavaScript
560 lines
22 KiB
JavaScript
const db = require('../db');
|
||
const logger = require('../utils/logger');
|
||
const { getLinkedWallet } = require('./wallet-service');
|
||
|
||
/**
|
||
* Сервис для работы с идентификаторами пользователей
|
||
*/
|
||
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);
|
||
|
||
// Проверяем тип провайдера и перенаправляем гостевые идентификаторы в guest_user_mapping
|
||
if (normalizedProvider === 'guest') {
|
||
logger.info(
|
||
`[IdentityService] Converting guest identity for user ${userId} to guest_user_mapping: ${normalizedProviderId}`
|
||
);
|
||
|
||
try {
|
||
await db.getQuery()(
|
||
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
|
||
[userId, normalizedProviderId]
|
||
);
|
||
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. Allowed types: ${allowedProviders.join(', ')}`,
|
||
};
|
||
}
|
||
|
||
logger.info(
|
||
`[IdentityService] Saving identity for user ${userId}: ${normalizedProvider}:${normalizedProviderId}`
|
||
);
|
||
|
||
// Проверяем, существует ли уже такой идентификатор
|
||
const existingResult = await db.getQuery()(
|
||
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
|
||
[normalizedProvider, normalizedProviderId]
|
||
);
|
||
|
||
if (existingResult.rows.length > 0) {
|
||
const existingUserId = existingResult.rows[0].user_id;
|
||
|
||
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем
|
||
if (existingUserId === userId) {
|
||
logger.info(
|
||
`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already exists for user ${userId}`
|
||
);
|
||
} else {
|
||
// Если идентификатор принадлежит другому пользователю, логируем это
|
||
logger.warn(
|
||
`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already belongs to user ${existingUserId}, not user ${userId}`
|
||
);
|
||
return {
|
||
success: false,
|
||
error: `Identity already belongs to another user (${existingUserId})`,
|
||
};
|
||
}
|
||
} else {
|
||
// Создаем новую запись
|
||
await db.getQuery()(
|
||
`INSERT INTO user_identities (user_id, provider, provider_id)
|
||
VALUES ($1, $2, $3)`,
|
||
[userId, normalizedProvider, normalizedProviderId]
|
||
);
|
||
logger.info(
|
||
`[IdentityService] Created new identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`
|
||
);
|
||
}
|
||
|
||
return { success: true };
|
||
} catch (error) {
|
||
logger.error(
|
||
`[IdentityService] Error saving identity ${provider}:${providerId} for user ${userId}:`,
|
||
error
|
||
);
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получает все идентификаторы пользователя
|
||
* @param {number} userId - ID пользователя
|
||
* @returns {Promise<Array>} - Массив идентификаторов
|
||
*/
|
||
async getUserIdentities(userId) {
|
||
try {
|
||
if (!userId) {
|
||
logger.warn('[IdentityService] Missing userId parameter');
|
||
return [];
|
||
}
|
||
|
||
const result = await db.getQuery()(
|
||
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
|
||
[userId]
|
||
);
|
||
|
||
logger.info(`[IdentityService] Found ${result.rows.length} identities for user ${userId}`);
|
||
return result.rows;
|
||
} catch (error) {
|
||
logger.error(`[IdentityService] Error getting identities for user ${userId}:`, error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Получает все идентификаторы пользователя определенного типа
|
||
* @param {number} userId - ID пользователя
|
||
* @param {string} provider - Тип идентификатора
|
||
* @returns {Promise<Array>} - Массив идентификаторов
|
||
*/
|
||
async getUserIdentitiesByProvider(userId, provider) {
|
||
try {
|
||
if (!userId || !provider) {
|
||
logger.warn(`[IdentityService] Missing parameters: userId=${userId}, provider=${provider}`);
|
||
return [];
|
||
}
|
||
|
||
const result = await db.getQuery()(
|
||
`SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2`,
|
||
[userId, provider]
|
||
);
|
||
|
||
logger.info(
|
||
`[IdentityService] Found ${result.rows.length} ${provider} identities for user ${userId}`
|
||
);
|
||
return result.rows.map((row) => row.provider_id);
|
||
} catch (error) {
|
||
logger.error(
|
||
`[IdentityService] Error getting ${provider} identities for user ${userId}:`,
|
||
error
|
||
);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Находит пользователя по идентификатору
|
||
* @param {string} provider - Тип идентификатора
|
||
* @param {string} providerId - Значение идентификатора
|
||
* @returns {Promise<object|null>} - Информация о пользователе или null
|
||
*/
|
||
async findUserByIdentity(provider, providerId) {
|
||
try {
|
||
if (!provider || !providerId) {
|
||
logger.warn(
|
||
`[IdentityService] Missing parameters: provider=${provider}, providerId=${providerId}`
|
||
);
|
||
return null;
|
||
}
|
||
|
||
// Нормализуем значения
|
||
const { provider: normalizedProvider, providerId: normalizedProviderId } =
|
||
this.normalizeIdentity(provider, providerId);
|
||
|
||
const result = await db.getQuery()(
|
||
`SELECT u.id, u.role FROM users u
|
||
JOIN user_identities ui ON u.id = ui.user_id
|
||
WHERE ui.provider = $1 AND ui.provider_id = $2`,
|
||
[normalizedProvider, normalizedProviderId]
|
||
);
|
||
|
||
if (result.rows.length === 0) {
|
||
logger.info(
|
||
`[IdentityService] No user found with identity ${normalizedProvider}:${normalizedProviderId}`
|
||
);
|
||
return null;
|
||
}
|
||
|
||
logger.info(
|
||
`[IdentityService] Found user ${result.rows[0].id} with identity ${normalizedProvider}:${normalizedProviderId}`
|
||
);
|
||
return result.rows[0];
|
||
} catch (error) {
|
||
logger.error(
|
||
`[IdentityService] Error finding user by identity ${provider}:${providerId}:`,
|
||
error
|
||
);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Находит конкретный идентификатор пользователя по его типу.
|
||
* Возвращает первую найденную запись.
|
||
* @param {number} userId - ID пользователя
|
||
* @param {string} provider - Тип идентификатора (например, 'wallet', 'email')
|
||
* @returns {Promise<object|null>} - Объект идентификатора (содержит provider_id) или null
|
||
*/
|
||
async findIdentity(userId, provider) {
|
||
try {
|
||
if (!userId || !provider) {
|
||
logger.warn(`[IdentityService] Missing parameters for findIdentity: userId=${userId}, provider=${provider}`);
|
||
return null;
|
||
}
|
||
|
||
// Нормализуем провайдера
|
||
const normalizedProvider = provider.toLowerCase();
|
||
|
||
const result = await db.getQuery()(
|
||
`SELECT provider, provider_id, created_at, updated_at
|
||
FROM user_identities
|
||
WHERE user_id = $1 AND provider = $2
|
||
LIMIT 1`,
|
||
[userId, normalizedProvider]
|
||
);
|
||
|
||
if (result.rows.length === 0) {
|
||
logger.info(`[IdentityService] No ${normalizedProvider} identity found for user ${userId}`);
|
||
return null;
|
||
}
|
||
|
||
logger.info(
|
||
`[IdentityService] Found ${normalizedProvider} identity for user ${userId}: ${result.rows[0].provider_id}`
|
||
);
|
||
return result.rows[0]; // Возвращаем всю строку (включая provider_id)
|
||
} catch (error) {
|
||
logger.error(
|
||
`[IdentityService] Error finding ${provider} 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 });
|
||
}
|
||
|
||
// Сохраняем гостевые идентификаторы в guest_user_mapping
|
||
if (session.guestId) {
|
||
try {
|
||
await db.getQuery()(
|
||
'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) {
|
||
try {
|
||
await db.getQuery()(
|
||
'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}`
|
||
);
|
||
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 client = await db.pool.connect();
|
||
try {
|
||
await client.query('BEGIN');
|
||
|
||
// Получаем все идентификаторы исходного пользователя
|
||
const identitiesResult = await client.query(
|
||
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
|
||
[fromUserId]
|
||
);
|
||
|
||
// Переносим каждый идентификатор
|
||
for (const identity of identitiesResult.rows) {
|
||
await client.query(
|
||
`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
|
||
SET user_id = $1
|
||
WHERE user_id = $2`,
|
||
[toUserId, fromUserId]
|
||
);
|
||
|
||
// Переносим все диалоги
|
||
await client.query(
|
||
`UPDATE conversations
|
||
SET user_id = $1
|
||
WHERE user_id = $2`,
|
||
[toUserId, fromUserId]
|
||
);
|
||
|
||
// Переносим настройки пользователя
|
||
await client.query(
|
||
`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 ${toUserId}`
|
||
);
|
||
return { success: true };
|
||
} catch (error) {
|
||
await client.query('ROLLBACK');
|
||
logger.error(`[IdentityService] Transaction error:`, error);
|
||
return { success: false, error: error.message };
|
||
} finally {
|
||
client.release();
|
||
}
|
||
} 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 result = await db.getQuery()(
|
||
`SELECT DISTINCT user_id
|
||
FROM user_identities
|
||
WHERE provider = $1 AND provider_id = $2`,
|
||
[provider, providerId]
|
||
);
|
||
|
||
result.rows.forEach((row) => userIds.add(row.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 db.getQuery()(
|
||
`DELETE FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3`,
|
||
[userId, normalizedProvider, normalizedProviderId]
|
||
);
|
||
logger.info(`[IdentityService] Deleted identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`);
|
||
return { success: true, deleted: result.rowCount };
|
||
} 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 newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', ['user']);
|
||
const userId = newUserResult.rows[0].id;
|
||
await this.saveIdentity(userId, provider, providerId, true);
|
||
user = { id: userId, role: 'user' };
|
||
isNew = true;
|
||
}
|
||
// Проверяем связь с кошельком
|
||
const wallet = await getLinkedWallet(user.id);
|
||
let role = 'user';
|
||
if (wallet) {
|
||
const isAdmin = await authService.checkAdminRole(wallet);
|
||
role = isAdmin ? 'admin' : 'user';
|
||
// Обновляем роль в users, если изменилась
|
||
if (user.role !== role) {
|
||
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, user.id]);
|
||
}
|
||
}
|
||
return { userId: user.id, role, isNew };
|
||
}
|
||
}
|
||
|
||
module.exports = new IdentityService();
|