feat: новая функция
This commit is contained in:
297
backend/services/IdentityLinkService.js
Normal file
297
backend/services/IdentityLinkService.js
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* 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 { checkAdminRole } = require('./admin-role');
|
||||
const isAdmin = await checkAdminRole(walletAddress);
|
||||
|
||||
if (isAdmin) {
|
||||
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: isAdmin ? '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();
|
||||
|
||||
Reference in New Issue
Block a user