Files
DLE/backend/services/IdentityLinkService.js
2025-10-13 22:41:49 +03:00

298 lines
8.9 KiB
JavaScript
Raw 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
*/
const db = require('../db');
const logger = require('../utils/logger');
const encryptionUtils = require('../utils/encryptionUtils');
const crypto = require('crypto');
/**
* Сервис для создания и управления токенами связывания идентификаторов
* Используется для привязки Telegram/Email к Web3 кошелькам
*/
class IdentityLinkService {
constructor() {
this.DEFAULT_TTL_HOURS = 1; // Токен действителен 1 час
this.FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:5173';
}
/**
* Сгенерировать токен для связывания
* @param {string} provider - 'telegram', 'email'
* @param {string} identifier - ID пользователя (Telegram ID или email)
* @param {Object} options - Дополнительные опции
* @returns {Promise<Object>} - {token, linkUrl, expiresAt}
*/
async generateLinkToken(provider, identifier, options = {}) {
try {
if (!provider || !identifier) {
throw new Error('Provider and identifier are required');
}
if (!['telegram', 'email'].includes(provider)) {
throw new Error(`Invalid provider: ${provider}. Must be 'telegram' or 'email'`);
}
// Генерируем уникальный токен
const token = crypto.randomBytes(32).toString('hex');
// Вычисляем время истечения
const ttlHours = options.ttlHours || this.DEFAULT_TTL_HOURS;
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + ttlHours);
const encryptionKey = encryptionUtils.getEncryptionKey();
// Сохраняем токен в БД
await db.getQuery()(
`INSERT INTO identity_link_tokens (
token,
source_provider,
source_identifier_encrypted,
user_id,
expires_at,
created_at
) VALUES (
$1, $2,
encrypt_text($3, $4),
$5,
$6,
NOW()
)`,
[
token,
provider,
identifier,
encryptionKey,
options.userId || null,
expiresAt
]
);
const linkUrl = `${this.FRONTEND_URL}/connect-wallet?token=${token}`;
logger.info(`[IdentityLinkService] Создан токен связывания для ${provider}:${identifier}`);
return {
success: true,
token,
linkUrl,
expiresAt: expiresAt.toISOString(),
provider,
identifier
};
} catch (error) {
logger.error('[IdentityLinkService] Ошибка генерации токена:', error);
throw error;
}
}
/**
* Проверить токен и получить данные
* @param {string} token
* @returns {Promise<Object|null>}
*/
async verifyLinkToken(token) {
try {
if (!token) {
return null;
}
const encryptionKey = encryptionUtils.getEncryptionKey();
const { rows } = await db.getQuery()(
`SELECT
id,
source_provider,
decrypt_text(source_identifier_encrypted, $2) as source_identifier,
user_id,
is_used,
used_at,
linked_wallet,
expires_at,
created_at
FROM identity_link_tokens
WHERE token = $1`,
[token, encryptionKey]
);
if (rows.length === 0) {
logger.warn(`[IdentityLinkService] Токен не найден: ${token}`);
return null;
}
const tokenData = rows[0];
// Проверяем срок действия
if (new Date() > new Date(tokenData.expires_at)) {
logger.warn(`[IdentityLinkService] Токен истек: ${token}`);
return null;
}
// Проверяем использование
if (tokenData.is_used) {
logger.warn(`[IdentityLinkService] Токен уже использован: ${token}`);
return null;
}
return tokenData;
} catch (error) {
logger.error('[IdentityLinkService] Ошибка проверки токена:', error);
throw error;
}
}
/**
* Использовать токен (связать с кошельком)
* @param {string} token
* @param {string} walletAddress
* @returns {Promise<Object>}
*/
async useLinkToken(token, walletAddress) {
try {
// 1. Проверяем токен
const tokenData = await this.verifyLinkToken(token);
if (!tokenData) {
return {
success: false,
error: 'Токен недействителен или истек'
};
}
const encryptionKey = encryptionUtils.getEncryptionKey();
// 2. Создаем пользователя если нужно
let userId = tokenData.user_id;
if (!userId) {
// Создаем нового пользователя
const { rows: userRows } = await db.getQuery()(
`INSERT INTO users (role) VALUES ($1) RETURNING id`,
['user']
);
userId = userRows[0].id;
logger.info(`[IdentityLinkService] Создан новый пользователь: ${userId}`);
}
// 3. Сохраняем wallet идентификатор
const identityService = require('./identity-service');
await identityService.saveIdentity(userId, 'wallet', walletAddress);
// 4. Сохраняем Telegram/Email идентификатор
await identityService.saveIdentity(
userId,
tokenData.source_provider,
tokenData.source_identifier
);
// 5. Помечаем токен как использованный
await db.getQuery()(
`UPDATE identity_link_tokens
SET is_used = true,
used_at = NOW(),
user_id = $2,
linked_wallet = $3
WHERE token = $1`,
[token, userId, walletAddress]
);
// 6. Проверяем админские права
const authService = require('./auth-service');
const userAccessLevel = await authService.getUserAccessLevel(walletAddress);
if (userAccessLevel.hasAccess) {
await db.getQuery()(
`UPDATE users SET role = $1 WHERE id = $2`,
['editor', userId]
);
logger.info(`[IdentityLinkService] Пользователь ${userId} получил роль admin`);
}
// 7. Создаем identifier для миграции
const universalGuestService = require('./UniversalGuestService');
const identifier = universalGuestService.createIdentifier(
tokenData.source_provider,
tokenData.source_identifier
);
logger.info(`[IdentityLinkService] Токен успешно использован. UserId: ${userId}`);
return {
success: true,
userId,
identifier,
provider: tokenData.source_provider,
role: userAccessLevel.hasAccess ? 'admin' : 'user'
};
} catch (error) {
logger.error('[IdentityLinkService] Ошибка использования токена:', error);
return {
success: false,
error: error.message
};
}
}
/**
* Очистить истекшие токены
* @returns {Promise<number>} - Количество удаленных
*/
async cleanupExpiredTokens() {
try {
const { rowCount } = await db.getQuery()(
`DELETE FROM identity_link_tokens
WHERE expires_at < NOW()
OR (is_used = true AND used_at < NOW() - INTERVAL '7 days')`
);
logger.info(`[IdentityLinkService] Очищено истекших токенов: ${rowCount}`);
return rowCount;
} catch (error) {
logger.error('[IdentityLinkService] Ошибка очистки токенов:', error);
throw error;
}
}
/**
* Получить статистику по токенам
* @returns {Promise<Object>}
*/
async getStats() {
try {
const { rows } = await db.getQuery()(
`SELECT
COUNT(*) as total_tokens,
COUNT(*) FILTER (WHERE is_used = true) as used_tokens,
COUNT(*) FILTER (WHERE is_used = false AND expires_at > NOW()) as active_tokens,
COUNT(*) FILTER (WHERE expires_at < NOW()) as expired_tokens
FROM identity_link_tokens`
);
return rows[0];
} catch (error) {
logger.error('[IdentityLinkService] Ошибка получения статистики:', error);
throw error;
}
}
}
module.exports = new IdentityLinkService();