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

This commit is contained in:
2025-07-27 03:30:13 +03:00
parent 057fe6254c
commit 1835632be9
141 changed files with 32514 additions and 6661 deletions

View File

@@ -12,6 +12,7 @@
const { ethers } = require('ethers');
const logger = require('../utils/logger');
const db = require('../db');
const authTokenService = require('./authTokenService');
const rpcProviderService = require('./rpcProviderService');
@@ -28,12 +29,41 @@ const ERC20_ABI = [
async function checkAdminRole(address) {
if (!address) return false;
logger.info(`Checking admin role for address: ${address}`);
try {
let foundTokens = false;
let errorCount = 0;
const balances = {};
// Получаем токены и RPC из базы
const tokens = await authTokenService.getAllAuthTokens();
const rpcProviders = await rpcProviderService.getAllRpcProviders();
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем токены и RPC из базы с расшифровкой
const tokensResult = await db.getQuery()(
'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
[encryptionKey]
);
const tokens = tokensResult.rows;
const rpcProvidersResult = await db.getQuery()(
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
[encryptionKey]
);
const rpcProviders = rpcProvidersResult.rows;
logger.info(`Retrieved ${tokens.length} tokens and ${rpcProviders.length} RPC providers`);
logger.info('Tokens:', JSON.stringify(tokens, null, 2));
logger.info('RPC Providers:', JSON.stringify(rpcProviders, null, 2));
const rpcMap = {};
for (const rpc of rpcProviders) {
rpcMap[rpc.network_id] = rpc.rpc_url;
@@ -109,6 +139,10 @@ async function checkAdminRole(address) {
}
logger.info(`Admin role denied - no tokens found for ${address}`);
return false;
} catch (error) {
logger.error(`Error in checkAdminRole for ${address}:`, error);
return false;
}
}
module.exports = { checkAdminRole };

View File

@@ -18,26 +18,84 @@ const { OpenAIEmbeddings } = require('@langchain/openai');
const logger = require('../utils/logger');
const fetch = require('node-fetch');
// Простой кэш для ответов
const responseCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 минут
class AIAssistant {
constructor() {
this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5';
this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5:7b';
this.isModelLoaded = false;
this.lastHealthCheck = 0;
this.healthCheckInterval = 30000; // 30 секунд
}
// Проверка здоровья модели
async checkModelHealth() {
const now = Date.now();
if (now - this.lastHealthCheck < this.healthCheckInterval) {
return this.isModelLoaded;
}
try {
const response = await fetch(`${this.baseUrl}/api/tags`, {
timeout: 5000
});
if (response.ok) {
const data = await response.json();
this.isModelLoaded = data.models?.some(m => m.name === this.defaultModel) || false;
} else {
this.isModelLoaded = false;
}
} catch (error) {
console.error('Model health check failed:', error);
this.isModelLoaded = false;
}
this.lastHealthCheck = now;
return this.isModelLoaded;
}
// Очистка старых записей кэша
cleanupCache() {
const now = Date.now();
for (const [key, value] of responseCache.entries()) {
if (now - value.timestamp > CACHE_TTL) {
responseCache.delete(key);
}
}
}
// Создание экземпляра ChatOllama с нужными параметрами
createChat(language = 'ru') {
const systemPrompt =
language === 'ru'
? 'Вы - полезный ассистент. Отвечайте на русском языке.'
: 'You are a helpful assistant. Respond in English.';
createChat(language = 'ru', customSystemPrompt = '') {
// Используем кастомный системный промпт, если он передан, иначе используем дефолтный
let systemPrompt = customSystemPrompt;
if (!systemPrompt) {
systemPrompt = language === 'ru'
? 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.'
: 'You are a helpful assistant. Respond in English briefly and to the point.';
}
return new ChatOllama({
baseUrl: this.baseUrl,
model: this.defaultModel,
system: systemPrompt,
temperature: 0.7,
maxTokens: 1000,
timeout: 30000, // 30 секунд таймаут
temperature: 0.3, // Уменьшаем для более предсказуемых ответов
maxTokens: 100, // Еще больше уменьшаем для быстрого ответа
timeout: 60000, // Увеличиваем таймаут до 60 секунд
options: {
num_ctx: 512, // Еще больше уменьшаем контекст для экономии памяти
num_thread: 12, // Увеличиваем количество потоков еще больше
num_gpu: 1,
num_gqa: 8,
rope_freq_base: 1000000,
rope_freq_scale: 0.5,
repeat_penalty: 1.1, // Добавляем штраф за повторения
top_k: 20, // Еще больше ограничиваем выбор токенов
top_p: 0.8, // Уменьшаем nucleus sampling
temperature: 0.1, // Еще больше уменьшаем для более предсказуемых ответов
}
});
}
@@ -52,6 +110,24 @@ class AIAssistant {
try {
console.log('getResponse called with:', { message, language, history, systemPrompt, rules });
// Очищаем старый кэш
this.cleanupCache();
// Проверяем здоровье модели
const isHealthy = await this.checkModelHealth();
if (!isHealthy) {
console.warn('Model is not healthy, returning fallback response');
return 'Извините, модель временно недоступна. Пожалуйста, попробуйте позже.';
}
// Создаем ключ кэша
const cacheKey = JSON.stringify({ message, language, systemPrompt, rules });
const cached = responseCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
console.log('Returning cached response');
return cached.response;
}
// Определяем язык, если не указан явно
const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language;
console.log('Detected language:', detectedLanguage);
@@ -77,28 +153,39 @@ class AIAssistant {
// Добавляем текущее сообщение пользователя
messages.push({ role: 'user', content: message });
let response = null;
// Пробуем прямой API запрос (OpenAI-совместимый endpoint)
try {
console.log('Trying direct API request...');
const response = await this.fallbackRequestOpenAI(messages, detectedLanguage);
response = await this.fallbackRequestOpenAI(messages, detectedLanguage, fullSystemPrompt);
console.log('Direct API response received:', response);
return response;
} catch (error) {
console.error('Error in direct API request:', error);
// Если прямой запрос не удался, пробуем через ChatOllama (склеиваем сообщения в текст)
const chat = this.createChat(detectedLanguage, fullSystemPrompt);
try {
const prompt = messages.map(m => `${m.role === 'user' ? 'Пользователь' : m.role === 'assistant' ? 'Ассистент' : 'Система'}: ${m.content}`).join('\n');
console.log('Sending request to ChatOllama...');
const chatResponse = await chat.invoke(prompt);
console.log('ChatOllama response:', chatResponse);
response = chatResponse.content;
} catch (chatError) {
console.error('Error using ChatOllama:', chatError);
throw chatError;
}
}
// Если прямой запрос не удался, пробуем через ChatOllama (склеиваем сообщения в текст)
const chat = this.createChat(detectedLanguage);
try {
const prompt = messages.map(m => `${m.role === 'user' ? 'Пользователь' : m.role === 'assistant' ? 'Ассистент' : 'Система'}: ${m.content}`).join('\n');
console.log('Sending request to ChatOllama...');
const response = await chat.invoke(prompt);
console.log('ChatOllama response:', response);
return response.content;
} catch (error) {
console.error('Error using ChatOllama:', error);
throw error;
// Кэшируем ответ
if (response) {
responseCache.set(cacheKey, {
response,
timestamp: Date.now()
});
}
return response;
} catch (error) {
console.error('Error in getResponse:', error);
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
@@ -106,10 +193,15 @@ class AIAssistant {
}
// Новый метод для OpenAI/Qwen2.5 совместимого endpoint
async fallbackRequestOpenAI(messages, language) {
async fallbackRequestOpenAI(messages, language, systemPrompt = '') {
try {
console.log('Using fallbackRequestOpenAI with:', { messages, language });
console.log('Using fallbackRequestOpenAI with:', { messages, language, systemPrompt });
const model = this.defaultModel;
// Создаем AbortController для таймаута
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // Увеличиваем до 60 секунд
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -118,11 +210,24 @@ class AIAssistant {
messages,
stream: false,
options: {
temperature: 0.7,
num_predict: 1000,
temperature: 0.3,
num_predict: 200, // Уменьшаем максимальную длину ответа
num_ctx: 1024, // Уменьшаем контекст для экономии памяти
num_thread: 8, // Увеличиваем количество потоков
num_gpu: 1, // Используем GPU если доступен
num_gqa: 8, // Оптимизация для qwen2.5
rope_freq_base: 1000000, // Оптимизация для qwen2.5
rope_freq_scale: 0.5, // Оптимизация для qwen2.5
repeat_penalty: 1.1, // Добавляем штраф за повторения
top_k: 40, // Ограничиваем выбор токенов
top_p: 0.9, // Используем nucleus sampling
},
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -134,6 +239,9 @@ class AIAssistant {
return data.response || '';
} catch (error) {
console.error('Error in fallbackRequestOpenAI:', error);
if (error.name === 'AbortError') {
throw new Error('Request timeout - модель не ответила в течение 60 секунд');
}
throw error;
}
}

View File

@@ -0,0 +1,377 @@
/**
* 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 Queue = require('better-queue');
const logger = require('../utils/logger');
class AIQueueService {
constructor() {
this.queue = null;
this.isInitialized = false;
this.userRequestTimes = new Map(); // Добавляем Map для отслеживания запросов пользователей
this.stats = {
totalProcessed: 0,
totalFailed: 0,
averageProcessingTime: 0,
currentQueueSize: 0,
lastProcessedAt: null
};
this.initQueue();
}
initQueue() {
try {
this.queue = new Queue(this.processTask.bind(this), {
// Ограничиваем количество одновременных запросов к Ollama
concurrent: 2,
// Максимальное время выполнения задачи
maxTimeout: 180000, // 3 минуты
// Задержка между задачами для предотвращения перегрузки
afterProcessDelay: 1000, // 1 секунда
// Максимальное количество повторных попыток
maxRetries: 2,
// Задержка между повторными попытками
retryDelay: 5000, // 5 секунд
// Функция определения приоритета
priority: this.getTaskPriority.bind(this),
// Функция фильтрации задач
filter: this.filterTask.bind(this),
// Функция слияния одинаковых задач
merge: this.mergeTasks.bind(this),
// ID задачи для предотвращения дублирования
id: 'requestId'
});
this.setupEventListeners();
this.isInitialized = true;
logger.info('[AIQueue] Queue initialized successfully');
} catch (error) {
logger.error('[AIQueue] Failed to initialize queue:', error);
this.isInitialized = false;
}
}
// Определение приоритета задачи
getTaskPriority(task, cb) {
try {
let priority = 1; // Базовый приоритет
// Высокий приоритет для администраторов
if (task.userRole === 'admin') {
priority += 10;
}
// Приоритет по типу запроса
switch (task.type) {
case 'urgent':
priority += 20;
break;
case 'chat':
priority += 5;
break;
case 'analysis':
priority += 3;
break;
case 'generation':
priority += 1;
break;
}
// Приоритет по размеру запроса (короткие запросы имеют больший приоритет)
if (task.message && task.message.length < 100) {
priority += 2;
}
// Приоритет по времени ожидания
const waitTime = Date.now() - task.timestamp;
if (waitTime > 30000) { // Более 30 секунд ожидания
priority += 5;
}
cb(null, priority);
} catch (error) {
cb(error, 1);
}
}
// Фильтрация задач
filterTask(task, cb) {
try {
// Проверяем обязательные поля
if (!task.message || typeof task.message !== 'string') {
return cb('Invalid message format');
}
if (!task.requestId) {
return cb('Missing request ID');
}
// Проверяем размер сообщения
if (task.message.length > 10000) {
return cb('Message too long (max 10000 characters)');
}
// Проверяем частоту запросов от пользователя
if (this.isUserRateLimited(task.userId)) {
return cb('User rate limit exceeded');
}
cb(null, task);
} catch (error) {
cb(error);
}
}
// Слияние одинаковых задач
mergeTasks(oldTask, newTask, cb) {
try {
// Если это тот же запрос от того же пользователя, обновляем метаданные
if (oldTask.message === newTask.message && oldTask.userId === newTask.userId) {
oldTask.timestamp = newTask.timestamp;
oldTask.retryCount = (oldTask.retryCount || 0) + 1;
cb(null, oldTask);
} else {
cb(null, newTask);
}
} catch (error) {
cb(error);
}
}
// Обработка задачи
async processTask(task, cb) {
const startTime = Date.now();
const taskId = task.requestId;
try {
logger.info(`[AIQueue] Processing task ${taskId} for user ${task.userId}`);
// Импортируем AI сервис
const aiAssistant = require('./ai-assistant');
const encryptedDb = require('./encryptedDatabaseService');
// Выполняем AI запрос
const result = await aiAssistant.getResponse(
task.message,
task.language || 'auto',
task.history || null,
task.systemPrompt || '',
task.rules || null
);
const processingTime = Date.now() - startTime;
// Сохраняем AI ответ в базу данных
if (task.conversationId && result) {
try {
const aiMessage = await encryptedDb.saveData('messages', {
conversation_id: task.conversationId,
user_id: task.userId,
content: result,
sender_type: 'assistant',
role: 'assistant',
channel: 'web'
});
// Получаем расшифрованные данные для WebSocket
const decryptedAiMessage = await encryptedDb.getData('messages', { id: aiMessage.id }, 1);
if (decryptedAiMessage && decryptedAiMessage[0]) {
// Отправляем сообщение через WebSocket
const { broadcastChatMessage } = require('../wsHub');
broadcastChatMessage(decryptedAiMessage[0], task.userId);
}
logger.info(`[AIQueue] AI response saved for conversation ${task.conversationId}`);
} catch (dbError) {
logger.error(`[AIQueue] Error saving AI response:`, dbError);
}
}
// Обновляем статистику
this.updateStats(true, processingTime);
logger.info(`[AIQueue] Task ${taskId} completed in ${processingTime}ms`);
cb(null, {
success: true,
result,
processingTime,
taskId
});
} catch (error) {
const processingTime = Date.now() - startTime;
// Обновляем статистику
this.updateStats(false, processingTime);
logger.error(`[AIQueue] Task ${taskId} failed:`, error);
cb(null, {
success: false,
error: error.message,
processingTime,
taskId
});
}
}
// Добавление задачи в очередь
addTask(taskData) {
if (!this.isInitialized || !this.queue) {
throw new Error('Queue is not initialized');
}
const task = {
...taskData,
timestamp: Date.now(),
requestId: taskData.requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
};
return new Promise((resolve, reject) => {
const ticket = this.queue.push(task, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
// Добавляем обработчики событий для билета
ticket.on('failed', (error) => {
logger.error(`[AIQueue] Task ${task.requestId} failed:`, error);
reject(error);
});
ticket.on('finish', (result) => {
logger.info(`[AIQueue] Task ${task.requestId} finished`);
resolve(result);
});
});
}
// Настройка обработчиков событий очереди
setupEventListeners() {
this.queue.on('task_queued', (taskId) => {
logger.info(`[AIQueue] Task ${taskId} queued`);
this.stats.currentQueueSize = this.queue.length;
});
this.queue.on('task_started', (taskId) => {
logger.info(`[AIQueue] Task ${taskId} started`);
});
this.queue.on('task_finish', (taskId, result) => {
logger.info(`[AIQueue] Task ${taskId} finished successfully`);
this.stats.lastProcessedAt = new Date();
this.stats.currentQueueSize = this.queue.length;
});
this.queue.on('task_failed', (taskId, error) => {
logger.error(`[AIQueue] Task ${taskId} failed:`, error);
this.stats.currentQueueSize = this.queue.length;
});
this.queue.on('empty', () => {
logger.info('[AIQueue] Queue is empty');
this.stats.currentQueueSize = 0;
});
this.queue.on('drain', () => {
logger.info('[AIQueue] Queue drained');
this.stats.currentQueueSize = 0;
});
}
// Обновление статистики
updateStats(success, processingTime) {
this.stats.totalProcessed++;
if (!success) {
this.stats.totalFailed++;
}
// Обновляем среднее время обработки
const totalTime = this.stats.averageProcessingTime * (this.stats.totalProcessed - 1) + processingTime;
this.stats.averageProcessingTime = totalTime / this.stats.totalProcessed;
}
// Проверка ограничения частоты запросов пользователя
isUserRateLimited(userId) {
// Простая реализация - можно улучшить с использованием Redis
const now = Date.now();
const userRequests = this.userRequestTimes.get(userId) || [];
// Удаляем старые запросы (старше 1 минуты)
const recentRequests = userRequests.filter(time => now - time < 60000);
// Ограничиваем до 10 запросов в минуту
if (recentRequests.length >= 10) {
return true;
}
// Добавляем текущий запрос
recentRequests.push(now);
this.userRequestTimes.set(userId, recentRequests);
return false;
}
// Получение статистики очереди
getStats() {
const queueStats = this.queue ? this.queue.getStats() : {};
return {
...this.stats,
queueStats,
isInitialized: this.isInitialized,
currentQueueSize: this.queue ? this.queue.length : 0,
runningTasks: this.queue ? this.queue.running : 0
};
}
// Очистка очереди
clear() {
if (this.queue) {
this.queue.destroy();
this.initQueue();
}
}
// Пауза/возобновление очереди
pause() {
if (this.queue) {
this.queue.pause();
logger.info('[AIQueue] Queue paused');
}
}
resume() {
if (this.queue) {
this.queue.resume();
logger.info('[AIQueue] Queue resumed');
}
}
}
// Создаем и экспортируем единственный экземпляр
const aiQueueService = new AIQueueService();
module.exports = aiQueueService;

View File

@@ -10,38 +10,44 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const TABLE = 'ai_assistant_rules';
async function getAllRules() {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} ORDER BY id`);
return rows;
const rules = await encryptedDb.getData(TABLE, {}, null, 'id');
return rules;
}
async function getRuleById(id) {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]);
return rows[0] || null;
const rules = await encryptedDb.getData(TABLE, { id: id }, 1);
return rules[0] || null;
}
async function createRule({ name, description, rules }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (name, description, rules, created_at, updated_at)
VALUES ($1, $2, $3, NOW(), NOW()) RETURNING *`,
[name, description, rules]
);
return rows[0];
const rule = await encryptedDb.saveData(TABLE, {
name: name,
description: description,
rules: rules,
created_at: new Date(),
updated_at: new Date()
});
return rule;
}
async function updateRule(id, { name, description, rules }) {
const { rows } = await db.getQuery()(
`UPDATE ${TABLE} SET name = $1, description = $2, rules = $3, updated_at = NOW() WHERE id = $4 RETURNING *`,
[name, description, rules, id]
);
return rows[0];
const rule = await encryptedDb.saveData(TABLE, {
name: name,
description: description,
rules: rules,
updated_at: new Date()
}, {
id: id
});
return rule;
}
async function deleteRule(id) {
await db.getQuery()(`DELETE FROM ${TABLE} WHERE id = $1`, [id]);
await encryptedDb.deleteData(TABLE, { id: id });
}
module.exports = { getAllRules, getRuleById, createRule, updateRule, deleteRule };

View File

@@ -10,53 +10,80 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const TABLE = 'ai_assistant_settings';
async function getSettings() {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} ORDER BY id LIMIT 1`);
const settings = rows[0] || null;
if (!settings) return null;
const settings = await encryptedDb.getData(TABLE, {}, 1, 'id');
const setting = settings[0] || null;
if (!setting) return null;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем связанные данные из telegram_settings и email_settings
let telegramBot = null;
let supportEmail = null;
if (settings.telegram_settings_id) {
const tg = await db.getQuery()('SELECT * FROM telegram_settings WHERE id = $1', [settings.telegram_settings_id]);
if (setting.telegram_settings_id) {
const tg = await db.getQuery()(
'SELECT id, created_at, updated_at, decrypt_text(bot_token_encrypted, $2) as bot_token, decrypt_text(bot_username_encrypted, $2) as bot_username FROM telegram_settings WHERE id = $1',
[setting.telegram_settings_id, encryptionKey]
);
telegramBot = tg.rows[0] || null;
}
if (settings.email_settings_id) {
const em = await db.getQuery()('SELECT * FROM email_settings WHERE id = $1', [settings.email_settings_id]);
if (setting.email_settings_id) {
const em = await db.getQuery()(
'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $2) as smtp_host, decrypt_text(smtp_user_encrypted, $2) as smtp_user, decrypt_text(smtp_password_encrypted, $2) as smtp_password, decrypt_text(imap_host_encrypted, $2) as imap_host, decrypt_text(from_email_encrypted, $2) as from_email FROM email_settings WHERE id = $1',
[setting.email_settings_id, encryptionKey]
);
supportEmail = em.rows[0] || null;
}
return {
...settings,
...setting,
telegramBot,
supportEmail,
embedding_model: settings.embedding_model
embedding_model: setting.embedding_model
};
}
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (id, system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_at, updated_by, telegram_settings_id, email_settings_id, system_message)
VALUES (1, $1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10)
ON CONFLICT (id) DO UPDATE SET
system_prompt = EXCLUDED.system_prompt,
selected_rag_tables = EXCLUDED.selected_rag_tables,
languages = EXCLUDED.languages,
model = EXCLUDED.model,
embedding_model = EXCLUDED.embedding_model,
rules = EXCLUDED.rules,
updated_at = NOW(),
updated_by = EXCLUDED.updated_by,
telegram_settings_id = EXCLUDED.telegram_settings_id,
email_settings_id = EXCLUDED.email_settings_id,
system_message = EXCLUDED.system_message
RETURNING *`,
[system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message]
);
return rows[0];
const data = {
id: 1,
system_prompt,
selected_rag_tables,
languages,
model,
embedding_model,
rules,
updated_at: new Date(),
updated_by,
telegram_settings_id,
email_settings_id,
system_message
};
// Проверяем, существует ли запись
const existing = await encryptedDb.getData(TABLE, { id: 1 }, 1);
if (existing.length > 0) {
// Обновляем существующую запись
return await encryptedDb.saveData(TABLE, data, { id: 1 });
} else {
// Создаем новую запись
return await encryptedDb.saveData(TABLE, data);
}
}
module.exports = { getSettings, upsertSettings };

View File

@@ -10,41 +10,41 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const OpenAI = require('openai');
const Anthropic = require('@anthropic-ai/sdk');
const TABLE = 'ai_providers_settings';
async function getProviderSettings(provider) {
const { rows } = await db.getQuery()(
`SELECT * FROM ${TABLE} WHERE provider = $1 LIMIT 1`,
[provider]
);
return rows[0] || null;
const settings = await encryptedDb.getData(TABLE, { provider: provider }, 1);
return settings[0] || null;
}
async function upsertProviderSettings({ provider, api_key, base_url, selected_model, embedding_model }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, embedding_model, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (provider) DO UPDATE SET
api_key = EXCLUDED.api_key,
base_url = EXCLUDED.base_url,
selected_model = EXCLUDED.selected_model,
embedding_model = EXCLUDED.embedding_model,
updated_at = NOW()
RETURNING *`,
[provider, api_key, base_url, selected_model, embedding_model]
);
return rows[0];
const data = {
provider: provider,
api_key: api_key,
base_url: base_url,
selected_model: selected_model,
embedding_model: embedding_model,
updated_at: new Date()
};
// Проверяем, существует ли запись
const existing = await encryptedDb.getData(TABLE, { provider: provider }, 1);
if (existing.length > 0) {
// Обновляем существующую запись
return await encryptedDb.saveData(TABLE, data, { provider: provider });
} else {
// Создаем новую запись
return await encryptedDb.saveData(TABLE, data);
}
}
async function deleteProviderSettings(provider) {
await db.getQuery()(
`DELETE FROM ${TABLE} WHERE provider = $1`,
[provider]
);
await encryptedDb.deleteData(TABLE, { provider: provider });
}
async function getProviderModels(provider, { api_key, base_url } = {}) {
@@ -111,19 +111,130 @@ async function verifyProviderKey(provider, { api_key, base_url } = {}) {
}
async function getAllLLMModels() {
const { rows } = await db.getQuery()(
`SELECT provider, selected_model FROM ${TABLE} WHERE selected_model IS NOT NULL AND selected_model <> ''`
);
// Возвращаем массив объектов { id, provider }
return rows.map(r => ({ id: r.selected_model, provider: r.provider }));
try {
// Получаем все настройки провайдеров
const providers = await encryptedDb.getData(TABLE, {});
// Собираем все модели из всех провайдеров
const allModels = [];
for (const provider of providers) {
if (provider.selected_model) {
allModels.push({
id: provider.selected_model,
provider: provider.provider
});
}
}
// Для Ollama проверяем реально установленные модели
try {
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
// Проверяем, какие модели установлены в Ollama
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2) {
const modelName = parts[0];
allModels.push({
id: modelName,
provider: 'ollama'
});
}
}
} catch (ollamaError) {
console.error('Error checking Ollama models:', ollamaError);
// Если не удалось проверить Ollama, добавляем базовые модели
allModels.push({ id: 'qwen2.5:7b', provider: 'ollama' });
}
// Убираем дубликаты
const uniqueModels = [];
const seen = new Set();
for (const model of allModels) {
const key = `${model.id}-${model.provider}`;
if (!seen.has(key)) {
seen.add(key);
uniqueModels.push(model);
}
}
return uniqueModels;
} catch (error) {
console.error('Error getting LLM models:', error);
return [];
}
}
async function getAllEmbeddingModels() {
const { rows } = await db.getQuery()(
`SELECT provider, embedding_model FROM ${TABLE} WHERE embedding_model IS NOT NULL AND embedding_model <> ''`
);
// Возвращаем массив объектов { id, provider }
return rows.map(r => ({ id: r.embedding_model, provider: r.provider }));
try {
// Получаем все настройки провайдеров
const providers = await encryptedDb.getData(TABLE, {});
// Собираем все embedding модели из всех провайдеров
const allModels = [];
for (const provider of providers) {
if (provider.embedding_model) {
allModels.push({
id: provider.embedding_model,
provider: provider.provider
});
}
}
// Для Ollama проверяем реально установленные embedding модели
try {
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
// Проверяем, какие embedding модели установлены в Ollama
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2) {
const modelName = parts[0];
// Проверяем, что это embedding модель
if (modelName.includes('embed') || modelName.includes('bge') || modelName.includes('nomic')) {
allModels.push({
id: modelName,
provider: 'ollama'
});
}
}
}
} catch (ollamaError) {
console.error('Error checking Ollama embedding models:', ollamaError);
// Если не удалось проверить Ollama, добавляем базовые embedding модели
allModels.push({ id: 'mxbai-embed-large:latest', provider: 'ollama' });
}
// Убираем дубликаты
const uniqueModels = [];
const seen = new Set();
for (const model of allModels) {
const key = `${model.id}-${model.provider}`;
if (!seen.has(key)) {
seen.add(key);
uniqueModels.push(model);
}
}
return uniqueModels;
} catch (error) {
console.error('Error getting embedding models:', error);
return [];
}
}
module.exports = {

View File

@@ -10,6 +10,7 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const logger = require('../utils/logger');
const { ethers } = require('ethers');
@@ -34,13 +35,20 @@ class AuthService {
if (!message || !signature || !address) return false;
// Нормализуем входящий адрес
const normalizedAddress = ethers.getAddress(address).toLowerCase();
const normalizedAddress = ethers.getAddress(address);
// Восстанавливаем адрес из подписи
const recoveredAddress = ethers.verifyMessage(message, signature);
// Логируем для отладки
logger.info(`[verifySignature] Message: ${message}`);
logger.info(`[verifySignature] Signature: ${signature}`);
logger.info(`[verifySignature] Expected address: ${normalizedAddress}`);
logger.info(`[verifySignature] Recovered address: ${recoveredAddress}`);
logger.info(`[verifySignature] Addresses match: ${ethers.getAddress(recoveredAddress) === normalizedAddress}`);
// Сравниваем нормализованные адреса
return ethers.getAddress(recoveredAddress).toLowerCase() === normalizedAddress;
return ethers.getAddress(recoveredAddress) === normalizedAddress;
} catch (error) {
logger.error('Error in signature verification:', error);
return false;
@@ -58,43 +66,40 @@ class AuthService {
const normalizedAddress = ethers.getAddress(address).toLowerCase();
// Ищем пользователя по адресу в таблице user_identities
const userResult = await db.getQuery()(
`
SELECT u.* FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1
`,
[normalizedAddress]
);
const identities = await encryptedDb.getData('user_identities', {
provider: 'wallet',
provider_id: normalizedAddress
}, 1);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
if (identities.length > 0) {
const user = await encryptedDb.getData('users', { id: identities[0].user_id }, 1);
if (user.length === 0) {
throw new Error('User not found');
}
const userData = user[0];
// Проверяем роль администратора при каждой аутентификации
const isAdmin = await checkAdminRole(normalizedAddress);
// Если статус админа изменился, обновляем роль в базе данных
if (user.role === 'admin' && !isAdmin) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', user.id]);
logger.info(`Updated user ${user.id} role to user (admin tokens no longer present)`);
return { userId: user.id, isAdmin: false };
} else if (user.role !== 'admin' && isAdmin) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', user.id]);
logger.info(`Updated user ${user.id} role to admin (admin tokens found)`);
return { userId: user.id, isAdmin: true };
if (userData.role === 'admin' && !isAdmin) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', userData.id]);
logger.info(`Updated user ${userData.id} role to user (admin tokens no longer present)`);
return { userId: userData.id, isAdmin: false };
} else if (userData.role !== 'admin' && isAdmin) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userData.id]);
logger.info(`Updated user ${userData.id} role to admin (admin tokens found)`);
return { userId: userData.id, isAdmin: true };
}
return {
userId: user.id,
isAdmin: user.role === 'admin',
userId: userData.id,
isAdmin: userData.role === 'admin',
};
}
// Если пользователь не найден, создаем нового
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'user',
]);
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', ['user']);
const userId = newUserResult.rows[0].id;
// Добавляем идентификатор кошелька (всегда в нижнем регистре)
@@ -209,7 +214,7 @@ class AuthService {
}
// Создание сессии с проверкой роли
async createSession(session, { userId, authenticated, authType, guestId, address }) {
async createSession(session, { userId, authenticated, authType, guestId, address, isAdmin }) {
try {
// Если пользователь аутентифицирован, обрабатываем гостевые сообщения
if (authenticated && guestId) {
@@ -220,6 +225,7 @@ class AuthService {
session.userId = userId;
session.authenticated = authenticated;
session.authType = authType;
session.isAdmin = isAdmin || false;
// Сохраняем адрес кошелька если есть
if (address) {
@@ -237,6 +243,7 @@ class AuthService {
authenticated,
authType,
address,
isAdmin: isAdmin || false,
cookie: session.cookie,
}),
session.id,
@@ -328,7 +335,7 @@ class AuthService {
const email = result.providerId;
// Проверяем, существует ли пользователь с таким email
const userResult = await db.getQuery()('SELECT * FROM users WHERE id = $1', [userId]);
const userResult = await db.getQuery()('SELECT id, role, created_at, updated_at, is_blocked, blocked_at, preferred_language FROM users WHERE id = $1', [userId]);
if (userResult.rows.length === 0) {
return { verified: false };
@@ -428,9 +435,23 @@ class AuthService {
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя
if (session.guestId && isNewUser) {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
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]
'INSERT INTO guest_user_mapping (user_id, guest_id_encrypted) VALUES ($1, encrypt_text($2, $3)) ON CONFLICT (guest_id_encrypted) DO UPDATE SET user_id = $1',
[userId, session.guestId, encryptionKey]
);
logger.info(`[verifyTelegramAuth] Saved guest ID ${session.guestId} for user ${userId}`);
}
@@ -460,13 +481,27 @@ class AuthService {
// Обновляем роль пользователя в базе данных, если есть админские токены
if (isAdmin) {
try {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Находим userId по адресу
const userResult = await db.getQuery()(
`
SELECT u.id FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1`,
[address.toLowerCase()]
WHERE ui.provider_encrypted = encrypt_text('wallet', $2) AND ui.provider_id_encrypted = encrypt_text($1, $2)`,
[address.toLowerCase(), encryptionKey]
);
if (userResult.rows.length > 0) {
@@ -486,8 +521,8 @@ class AuthService {
`
SELECT u.id, u.role FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1`,
[address.toLowerCase()]
WHERE ui.provider_encrypted = encrypt_text('wallet', $2) AND ui.provider_id_encrypted = encrypt_text($1, $2)`,
[address.toLowerCase(), encryptionKey]
);
if (userResult.rows.length > 0 && userResult.rows[0].role === 'admin') {
@@ -531,7 +566,7 @@ class AuthService {
// Удаляем старые идентификаторы
for (const identity of identitiesToDelete) {
await db.getQuery()('DELETE FROM user_identities WHERE id = $1', [identity.id]);
logger.info(`Deleted old guest identity: ${identity.identity_value}`);
logger.info(`Deleted old guest identity: ${identity.id}`);
}
}
} catch (error) {
@@ -546,11 +581,8 @@ class AuthService {
*/
async getUserIdentities(userId) {
try {
const result = await db.getQuery()(
'SELECT * FROM user_identities WHERE user_id = $1 ORDER BY created_at DESC',
[userId]
);
return result.rows;
const identities = await encryptedDb.getData('user_identities', { user_id: userId }, null, 'created_at DESC');
return identities;
} catch (error) {
logger.error('[getUserIdentities] Error:', error);
throw error;
@@ -611,13 +643,13 @@ class AuthService {
);
// Проверяем, существует ли уже такой идентификатор
const existingResult = await db.getQuery()(
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
[provider, normalizedProviderId]
);
const existingIdentities = await encryptedDb.getData('user_identities', {
provider: provider,
provider_id: normalizedProviderId
}, 1);
if (existingResult.rows.length > 0) {
const existingUserId = existingResult.rows[0].user_id;
if (existingIdentities.length > 0) {
const existingUserId = existingIdentities[0].user_id;
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем
if (existingUserId === userId) {
@@ -779,8 +811,33 @@ class AuthService {
*/
async getUserTokenBalances(address) {
if (!address) return [];
const tokens = await authTokenService.getAllAuthTokens();
const rpcProviders = await rpcProviderService.getAllRpcProviders();
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем токены и RPC с расшифровкой
const tokensResult = await db.getQuery()(
'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
[encryptionKey]
);
const tokens = tokensResult.rows;
const rpcProvidersResult = await db.getQuery()(
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
[encryptionKey]
);
const rpcProviders = rpcProvidersResult.rows;
const rpcMap = {};
for (const rpc of rpcProviders) {
rpcMap[rpc.network_id] = rpc.rpc_url;
@@ -811,6 +868,41 @@ class AuthService {
}
return results;
}
/**
* Проверяет nonce для адреса кошелька
* @param {string} address - адрес кошелька
* @param {string} nonce - nonce для проверки
* @returns {Promise<boolean>} - true если nonce валиден
*/
async verifyNonce(address, nonce) {
try {
// Получаем nonce из базы данных через encryptedDb
const nonceData = await encryptedDb.getData('nonces', {
identity_value: address.toLowerCase()
}, 1);
if (nonceData.length === 0) {
logger.warn(`[verifyNonce] No nonce found for address: ${address}`);
return false;
}
// Получаем nonce из результата
const storedNonce = nonceData[0].nonce;
// Сравниваем с переданным nonce
const isValid = storedNonce === nonce;
if (!isValid) {
logger.warn(`[verifyNonce] Invalid nonce for address: ${address}. Expected: ${storedNonce}, Got: ${nonce}`);
}
return isValid;
} catch (error) {
logger.error(`[verifyNonce] Error verifying nonce for address ${address}:`, error);
return false;
}
}
}
// Создаем и экспортируем единственный экземпляр

View File

@@ -10,35 +10,59 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
async function getAllAuthTokens() {
const { rows } = await db.getQuery()('SELECT * FROM auth_tokens ORDER BY id');
return rows;
const tokens = await encryptedDb.getData('auth_tokens', {}, null, 'id');
return tokens;
}
async function saveAllAuthTokens(authTokens) {
await db.getQuery()('DELETE FROM auth_tokens');
// Удаляем все существующие токены
await encryptedDb.deleteData('auth_tokens', {});
// Сохраняем новые токены
for (const token of authTokens) {
await db.getQuery()(
'INSERT INTO auth_tokens (name, address, network, min_balance) VALUES ($1, $2, $3, $4)',
[token.name, token.address, token.network, token.minBalance]
);
await encryptedDb.saveData('auth_tokens', {
name: token.name,
address: token.address,
network: token.network,
min_balance: token.minBalance == null ? 0 : Number(token.minBalance)
});
}
}
async function upsertAuthToken(token) {
const minBalance = token.minBalance == null ? 0 : Number(token.minBalance);
await db.getQuery()(
`INSERT INTO auth_tokens (name, address, network, min_balance)
VALUES ($1, $2, $3, $4)
ON CONFLICT (address, network) DO UPDATE SET name=EXCLUDED.name, min_balance=EXCLUDED.min_balance`,
[token.name, token.address, token.network, minBalance]
);
// Проверяем, существует ли токен
const existingTokens = await encryptedDb.getData('auth_tokens', {
address: token.address,
network: token.network
}, 1);
if (existingTokens.length > 0) {
// Обновляем существующий токен
await encryptedDb.saveData('auth_tokens', {
name: token.name,
min_balance: minBalance
}, {
address: token.address,
network: token.network
});
} else {
// Создаем новый токен
await encryptedDb.saveData('auth_tokens', {
name: token.name,
address: token.address,
network: token.network,
min_balance: minBalance
});
}
}
async function deleteAuthToken(address, network) {
await db.getQuery()('DELETE FROM auth_tokens WHERE address = $1 AND network = $2', [address, network]);
await encryptedDb.deleteData('auth_tokens', { address, network });
}
module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken };

View File

@@ -10,29 +10,39 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
class DbSettingsService {
async getSettings() {
const { rows } = await db.getQuery()('SELECT * FROM db_settings WHERE id = 1');
const rows = await encryptedDb.getData('db_settings', { id: 1 }, 1);
return rows[0];
}
async upsertSettings({ db_host, db_port, db_name, db_user, db_password }) {
const { rows } = await db.getQuery()(
`INSERT INTO db_settings (id, db_host, db_port, db_name, db_user, db_password, updated_at)
VALUES (1, $1, $2, $3, $4, $5, NOW())
ON CONFLICT (id) DO UPDATE SET
db_host = EXCLUDED.db_host,
db_port = EXCLUDED.db_port,
db_name = EXCLUDED.db_name,
db_user = EXCLUDED.db_user,
db_password = EXCLUDED.db_password,
updated_at = NOW()
RETURNING *`,
[db_host, db_port, db_name, db_user, db_password]
);
return rows[0];
const data = {
id: 1,
db_host,
db_port,
db_name,
db_user,
db_password,
updated_at: new Date()
};
// Пытаемся обновить существующую запись
const existing = await this.getSettings();
if (existing) {
return await encryptedDb.saveData('db_settings', data, { id: 1 });
} else {
return await encryptedDb.saveData('db_settings', data);
}
}
/**
* Получить статус шифрования
*/
getEncryptionStatus() {
return encryptedDb.getEncryptionStatus();
}
}

View File

@@ -0,0 +1,313 @@
/**
* 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 { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const { ethers } = require('ethers');
const logger = require('../utils/logger');
const { getRpcUrlByNetworkId } = require('./rpcProviderService');
/**
* Сервис для управления DLE v2 (Digital Legal Entity)
* Современный подход с единым контрактом
*/
class DLEV2Service {
/**
* Создает новое DLE v2 с заданными параметрами
* @param {Object} dleParams - Параметры DLE
* @returns {Promise<Object>} - Результат создания DLE
*/
async createDLE(dleParams) {
try {
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
// Валидация входных данных
this.validateDLEParams(dleParams);
// Подготовка параметров для деплоя
const deployParams = this.prepareDeployParams(dleParams);
// Сохраняем параметры во временный файл
const paramsFile = this.saveParamsToFile(deployParams);
// Копируем параметры во временный файл с предсказуемым именем
const tempParamsFile = path.join(__dirname, '../scripts/deploy/current-params.json');
const deployDir = path.dirname(tempParamsFile);
if (!fs.existsSync(deployDir)) {
fs.mkdirSync(deployDir, { recursive: true });
}
fs.copyFileSync(paramsFile, tempParamsFile);
logger.info(`Файл параметров скопирован успешно`);
// Получаем rpc_url из базы по выбранной сети
const rpcUrl = await getRpcUrlByNetworkId(deployParams.network);
if (!rpcUrl) {
throw new Error(`RPC URL для сети ${deployParams.network} не найден в базе данных`);
}
if (!dleParams.privateKey) {
throw new Error('Приватный ключ для деплоя не передан');
}
// Запускаем скрипт деплоя с нужными переменными окружения
const result = await this.runDeployScript(paramsFile, {
rpcUrl,
privateKey: dleParams.privateKey,
networkId: deployParams.network,
envNetworkKey: deployParams.network.toUpperCase()
});
// Очищаем временные файлы
this.cleanupTempFiles(paramsFile, tempParamsFile);
return result;
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
throw error;
}
}
/**
* Валидирует параметры DLE
* @param {Object} params - Параметры для валидации
*/
validateDLEParams(params) {
if (!params.name || params.name.trim() === '') {
throw new Error('Название DLE обязательно');
}
if (!params.symbol || params.symbol.trim() === '') {
throw new Error('Символ токена обязателен');
}
if (!params.location || params.location.trim() === '') {
throw new Error('Местонахождение DLE обязательно');
}
if (!params.partners || !Array.isArray(params.partners)) {
throw new Error('Партнеры должны быть массивом');
}
if (!params.amounts || !Array.isArray(params.amounts)) {
throw new Error('Суммы должны быть массивом');
}
if (params.partners.length !== params.amounts.length) {
throw new Error('Количество партнеров должно соответствовать количеству сумм распределения');
}
if (params.partners.length === 0) {
throw new Error('Должен быть указан хотя бы один партнер');
}
if (params.quorumPercentage > 100) {
throw new Error('Процент кворума не может превышать 100%');
}
// Проверяем адреса партнеров
for (let i = 0; i < params.partners.length; i++) {
if (!ethers.isAddress(params.partners[i])) {
throw new Error(`Неверный адрес партнера ${i + 1}: ${params.partners[i]}`);
}
}
}
/**
* Подготавливает параметры для деплоя
* @param {Object} params - Параметры DLE
* @returns {Object} - Подготовленные параметры
*/
prepareDeployParams(params) {
// Создаем копию объекта, чтобы не изменять исходный
const deployParams = { ...params };
// Преобразуем суммы из строк или чисел в BigNumber, если нужно
deployParams.amounts = params.amounts.map(amount => {
if (typeof amount === 'string' && !amount.startsWith('0x')) {
return ethers.parseEther(amount).toString();
}
return amount.toString();
});
// Преобразуем параметры голосования
deployParams.votingDelay = params.votingDelay || 1;
deployParams.votingPeriod = params.votingPeriod || 45818; // ~1 неделя
deployParams.proposalThreshold = params.proposalThreshold || ethers.parseEther("100000").toString();
deployParams.quorumPercentage = params.quorumPercentage || 4;
deployParams.minTimelockDelay = params.minTimelockDelay || 2;
// Убеждаемся, что isicCodes - это массив
if (!Array.isArray(deployParams.isicCodes)) {
deployParams.isicCodes = [];
}
return deployParams;
}
/**
* Сохраняет параметры во временный файл
* @param {Object} params - Параметры для сохранения
* @returns {string} - Путь к сохраненному файлу
*/
saveParamsToFile(params) {
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const fileName = `dle-v2-params-${Date.now()}.json`;
const filePath = path.join(tempDir, fileName);
fs.writeFileSync(filePath, JSON.stringify(params, null, 2));
return filePath;
}
/**
* Запускает скрипт деплоя DLE v2
* @param {string} paramsFile - Путь к файлу с параметрами
* @returns {Promise<Object>} - Результат деплоя
*/
runDeployScript(paramsFile, extraEnv = {}) {
return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '../scripts/deploy/create-dle-v2.js');
if (!fs.existsSync(scriptPath)) {
reject(new Error('Скрипт деплоя DLE v2 не найден: ' + scriptPath));
return;
}
// Формируем универсальные переменные окружения
const envVars = {
...process.env,
[`${extraEnv.envNetworkKey}_RPC_URL`]: extraEnv.rpcUrl,
[`${extraEnv.envNetworkKey}_PRIVATE_KEY`]: extraEnv.privateKey
};
// Запускаем скрипт с нужной сетью
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath, '--network', extraEnv.networkId], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
hardhatProcess.stdout.on('data', (data) => {
stdout += data.toString();
logger.info(`[DLE v2 Deploy] ${data.toString().trim()}`);
});
hardhatProcess.stderr.on('data', (data) => {
stderr += data.toString();
logger.error(`[DLE v2 Deploy Error] ${data.toString().trim()}`);
});
hardhatProcess.on('close', (code) => {
if (code === 0) {
try {
// Пытаемся извлечь результат из stdout
const result = this.extractDeployResult(stdout);
resolve(result);
} catch (error) {
logger.error('Ошибка при извлечении результатов деплоя DLE v2:', error);
reject(new Error('Не удалось найти информацию о созданном DLE v2'));
}
} else {
reject(new Error(`Скрипт деплоя DLE v2 завершился с кодом ${code}: ${stderr}`));
}
});
hardhatProcess.on('error', (error) => {
logger.error('Ошибка запуска скрипта деплоя DLE v2:', error);
reject(error);
});
});
}
/**
* Извлекает результат деплоя из stdout
* @param {string} stdout - Вывод скрипта
* @returns {Object} - Результат деплоя
*/
extractDeployResult(stdout) {
// Ищем строки с адресами в выводе
const dleAddressMatch = stdout.match(/DLE v2 задеплоен по адресу: (0x[a-fA-F0-9]{40})/);
const timelockAddressMatch = stdout.match(/Таймлок создан по адресу: (0x[a-fA-F0-9]{40})/);
if (dleAddressMatch && timelockAddressMatch) {
return {
success: true,
data: {
dleAddress: dleAddressMatch[1],
timelockAddress: timelockAddressMatch[1],
version: 'v2'
}
};
}
throw new Error('Не удалось извлечь адреса из вывода скрипта');
}
/**
* Очищает временные файлы
* @param {string} paramsFile - Путь к файлу параметров
* @param {string} tempParamsFile - Путь к временному файлу параметров
*/
cleanupTempFiles(paramsFile, tempParamsFile) {
try {
if (fs.existsSync(paramsFile)) {
fs.unlinkSync(paramsFile);
}
if (fs.existsSync(tempParamsFile)) {
fs.unlinkSync(tempParamsFile);
}
} catch (error) {
logger.warn('Не удалось очистить временные файлы:', error);
}
}
/**
* Получает список всех созданных DLE v2
* @returns {Array<Object>} - Список DLE v2
*/
getAllDLEs() {
try {
const dlesDir = path.join(__dirname, '../contracts-data/dles');
if (!fs.existsSync(dlesDir)) {
return [];
}
const files = fs.readdirSync(dlesDir);
return files
.filter(file => file.endsWith('.json') && file.includes('dle-v2-'))
.map(file => {
try {
const data = JSON.parse(fs.readFileSync(path.join(dlesDir, file), 'utf8'));
return { ...data, _fileName: file };
} catch (error) {
logger.error(`Ошибка при чтении файла ${file}:`, error);
return null;
}
})
.filter(dle => dle !== null);
} catch (error) {
logger.error('Ошибка при получении списка DLE v2:', error);
return [];
}
}
}
module.exports = new DLEV2Service();

View File

@@ -14,7 +14,7 @@ const { pool } = require('../db');
const verificationService = require('./verification-service');
const logger = require('../utils/logger');
const EmailBotService = require('./emailBot.js');
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const authService = require('./auth-service');
const { checkAdminRole } = require('./admin-role');
const { broadcastContactsUpdate } = require('../wsHub');
@@ -31,12 +31,10 @@ class EmailAuth {
}
// Проверяем, существует ли пользователь с таким email
const existingEmailUser = await db.getQuery()(
`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()]
);
const existingEmailUsers = await encryptedDb.getData('user_identities', {
provider: 'email',
provider_id: email.toLowerCase()
}, 1);
// Создаем или получаем ID пользователя
let userId;
@@ -47,16 +45,16 @@ class EmailAuth {
logger.info(
`[initEmailAuth] Using existing authenticated user ${userId} for email ${email}`
);
} else if (existingEmailUser.rows.length > 0) {
} else if (existingEmailUsers.length > 0) {
// Если найден пользователь с таким email, используем его ID
userId = existingEmailUser.rows[0].id;
userId = existingEmailUsers[0].user_id;
logger.info(`[initEmailAuth] Found existing user ${userId} with email ${email}`);
} else {
// Создаем временного пользователя, если нужно будет создать нового
const userResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'user',
]);
userId = userResult.rows[0].id;
const newUser = await encryptedDb.saveData('users', {
role: 'user'
});
userId = newUser.id;
session.tempUserId = userId;
logger.info(`[initEmailAuth] Created temporary user ${userId} for email ${email}`);
}
@@ -165,11 +163,10 @@ class EmailAuth {
finalUserId = session.tempUserId;
logger.info(`[checkEmailVerification] Using temporary user ${finalUserId}`);
} else {
const newUserResult = await db.getQuery()(
'INSERT INTO users (role) VALUES ($1) RETURNING id',
['user']
);
finalUserId = newUserResult.rows[0].id;
const newUserResult = await encryptedDb.saveData('users', {
role: 'user'
});
finalUserId = newUserResult.id;
logger.info(`[checkEmailVerification] Created new user ${finalUserId}`);
}
}
@@ -189,9 +186,9 @@ class EmailAuth {
logger.info(`[checkEmailVerification] Role for user ${finalUserId} determined as: ${userRole}`);
// Опционально: Обновить роль в таблице users, если она отличается
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [finalUserId]);
if (currentUser.rows.length > 0 && currentUser.rows[0].role !== userRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [userRole, finalUserId]);
const currentUser = await encryptedDb.getData('users', { id: finalUserId }, 1);
if (currentUser.length > 0 && currentUser[0].role !== userRole) {
await encryptedDb.saveData('users', { role: userRole, id: finalUserId });
logger.info(`[checkEmailVerification] Updated user role in DB to ${userRole}`);
}
} else {

View File

@@ -11,6 +11,7 @@
*/
console.log('[EmailBot] emailBot.js loaded');
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const nodemailer = require('nodemailer');
const Imap = require('imap');
@@ -31,7 +32,24 @@ class EmailBotService {
}
async getSettingsFromDb() {
const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1');
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const { rows } = await db.getQuery()(
'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $1) as smtp_host, decrypt_text(smtp_user_encrypted, $1) as smtp_user, decrypt_text(smtp_password_encrypted, $1) as smtp_password, decrypt_text(imap_host_encrypted, $1) as imap_host, decrypt_text(from_email_encrypted, $1) as from_email FROM email_settings ORDER BY id LIMIT 1',
[encryptionKey]
);
if (!rows.length) throw new Error('Email settings not found in DB');
return rows[0];
}
@@ -160,43 +178,68 @@ class EmailBotService {
return;
}
// 1.1 Найти или создать беседу
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
);
let conversationResult = await encryptedDb.getData(
'conversations',
{ user_id: userId },
1,
'updated_at DESC, created_at DESC'
);
let conversation;
if (conversationResult.rows.length === 0) {
if (conversationResult.length === 0) {
const title = `Чат с пользователем ${userId}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[userId, title]
);
conversation = newConv.rows[0];
const newConv = await encryptedDb.saveData(
'conversations',
{ user_id: userId, title: title, created_at: new Date(), updated_at: new Date() }
);
conversation = newConv;
} else {
conversation = conversationResult.rows[0];
conversation = conversationResult[0];
}
// Проверяем, что conversation создан успешно
if (!conversation || !conversation.id) {
logger.error(`[EmailBot] Conversation is undefined or has no id for user ${userId}`);
return;
}
// 2. Сохранять все сообщения с conversation_id
let hasAttachments = parsed.attachments && parsed.attachments.length > 0;
if (hasAttachments) {
for (const att of parsed.attachments) {
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10, $11, $12)`,
[userId, conversation.id, 'user', text, 'email', role, 'in',
att.filename,
att.contentType,
att.size,
att.content,
JSON.stringify({ subject, html })
]
);
await encryptedDb.saveData(
'messages',
{
user_id: userId,
conversation_id: conversation.id,
sender_type: 'user',
content: text,
channel: 'email',
role: role,
direction: 'in',
created_at: new Date(),
attachment_filename: att.filename,
attachment_mimetype: att.contentType,
attachment_size: att.size,
attachment_data: att.content,
metadata: JSON.stringify({ subject, html })
}
);
}
} else {
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[userId, conversation.id, 'user', text, 'email', role, 'in', JSON.stringify({ subject, html })]
);
await encryptedDb.saveData(
'messages',
{
user_id: userId,
conversation_id: conversation.id,
sender_type: 'user',
content: text,
channel: 'email',
role: role,
direction: 'in',
created_at: new Date(),
metadata: JSON.stringify({ subject, html })
}
);
}
// 3. Получить ответ от ИИ (RAG + LLM)
const aiSettings = await aiAssistantSettingsService.getSettings();
@@ -231,11 +274,20 @@ class EmailBotService {
return;
}
// 4. Сохранить ответ в БД с conversation_id
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[userId, conversation.id, 'assistant', aiResponse, 'email', role, 'out', JSON.stringify({ subject, html })]
);
await encryptedDb.saveData(
'messages',
{
user_id: userId,
conversation_id: conversation.id,
sender_type: 'assistant',
content: aiResponse,
channel: 'email',
role: role,
direction: 'out',
created_at: new Date(),
metadata: JSON.stringify({ subject, html })
}
);
// 5. Отправить ответ на email
await this.sendEmail(fromEmail, 'Re: ' + subject, aiResponse);
// После каждого успешного создания пользователя:
@@ -359,10 +411,10 @@ class EmailBotService {
}
}
async getAllEmailSettings() {
const { rows } = await db.getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
return rows;
}
async getAllEmailSettings() {
const settings = await encryptedDb.getData('email_settings', {}, null, 'id');
return settings;
}
}
console.log('[EmailBot] module.exports = EmailBotService');

View File

@@ -0,0 +1,440 @@
/**
* 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 fs = require('fs');
const path = require('path');
class EncryptedDataService {
constructor() {
this.encryptionKey = this.loadEncryptionKey();
this.isEncryptionEnabled = !!this.encryptionKey;
if (this.isEncryptionEnabled) {
console.log('🔐 Шифрование базы данных активировано');
console.log('📋 Автоматическое определение зашифрованных колонок');
} else {
console.log('⚠️ Шифрование базы данных отключено - ключ не найден');
}
}
loadEncryptionKey() {
try {
const keyPath = path.join(__dirname, '../../ssl/keys/full_db_encryption.key');
console.log(`[EncryptedDB] Trying key path: ${keyPath}`);
if (fs.existsSync(keyPath)) {
const key = fs.readFileSync(keyPath, 'utf8').trim();
console.log(`[EncryptedDB] Key loaded from: ${keyPath}, length: ${key.length}`);
return key;
}
// Попробуем альтернативный путь относительно корня приложения
const altKeyPath = '/app/ssl/keys/full_db_encryption.key';
console.log(`[EncryptedDB] Trying alternative key path: ${altKeyPath}`);
if (fs.existsSync(altKeyPath)) {
const key = fs.readFileSync(altKeyPath, 'utf8').trim();
console.log(`[EncryptedDB] Key loaded from: ${altKeyPath}, length: ${key.length}`);
return key;
}
console.log(`[EncryptedDB] No key file found, using default key`);
return 'default-key';
} catch (error) {
console.error('❌ Ошибка загрузки ключа шифрования:', error);
return 'default-key';
}
}
/**
* Получить данные из таблицы с автоматической расшифровкой
*/
async getData(tableName, conditions = {}, limit = null, orderBy = null) {
try {
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedQuery(tableName, conditions, limit, orderBy);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
// Строим SELECT с расшифровкой
const selectFields = columns.map(col => {
if (col.column_name.endsWith('_encrypted')) {
const originalName = col.column_name.replace('_encrypted', '');
console.log(`🔓 Расшифровываем поле ${col.column_name} -> ${originalName}`);
if (col.data_type === 'jsonb') {
return `decrypt_json(${col.column_name}, $1) as "${originalName}"`;
} else {
return `decrypt_text(${col.column_name}, $1) as "${originalName}"`;
}
} else if (!col.column_name.includes('_encrypted')) {
// Проверяем, есть ли зашифрованная версия этой колонки
const hasEncryptedVersion = columns.some(encCol =>
encCol.column_name === `${col.column_name}_encrypted`
);
// Если есть зашифрованная версия, пропускаем незашифрованную
if (hasEncryptedVersion) {
console.log(`⚠️ Пропускаем незашифрованное поле ${col.column_name} (есть зашифрованная версия)`);
return null;
}
// Заключаем зарезервированные слова в кавычки
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
if (reservedWords.includes(col.column_name.toLowerCase())) {
return `"${col.column_name}"`;
}
return col.column_name;
}
return null;
}).filter(Boolean).join(', ');
let query = `SELECT ${selectFields} FROM ${tableName}`;
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
const params = hasEncryptedFields ? [this.encryptionKey] : [];
let paramIndex = hasEncryptedFields ? 2 : 1;
// Список зарезервированных слов для WHERE-условий
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => {
const value = conditions[key];
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
// Обрабатываем оператор $in
if (value && typeof value === 'object' && value.$in && Array.isArray(value.$in)) {
const placeholders = value.$in.map(() => `$${paramIndex++}`).join(', ');
const columnName = encryptedColumn ? key :
(reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key);
return `${columnName} IN (${placeholders})`;
}
// Обрабатываем оператор $ne
if (value && typeof value === 'object' && value.$ne !== undefined) {
const columnName = encryptedColumn ? key :
(reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key);
return `${columnName} != $${paramIndex++}`;
}
if (encryptedColumn) {
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
return `${key}_encrypted = encrypt_text($${paramIndex++}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
} else {
// Для незашифрованных колонок используем обычное сравнение
// Заключаем зарезервированные слова в кавычки
const columnName = reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key;
return `${columnName} = $${paramIndex++}`;
}
})
.join(' AND ');
query += ` WHERE ${whereClause}`;
// Добавляем параметры для $in операторов
const paramsToAdd = Object.values(conditions).map(value => {
if (value && typeof value === 'object' && value.$in && Array.isArray(value.$in)) {
return value.$in;
}
if (value && typeof value === 'object' && value.$ne !== undefined) {
return value.$ne;
}
return value;
}).flat();
params.push(...paramsToAdd);
}
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
if (limit) {
query += ` LIMIT ${limit}`;
}
console.log(`🔍 [getData] Выполняем запрос:`, query);
console.log(`🔍 [getData] Параметры:`, params);
const { rows } = await db.getQuery()(query, params);
console.log(`📊 Результат запроса из ${tableName}:`, rows);
return rows;
} catch (error) {
console.error(`❌ Ошибка получения данных из ${tableName}:`, error);
throw error;
}
}
/**
* Сохранить данные в таблицу с автоматическим шифрованием
*/
async saveData(tableName, data, whereConditions = null) {
try {
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedSave(tableName, data, whereConditions);
}
// Для таблицы users используем обычные запросы, так как она содержит смешанные колонки
if (tableName === 'users') {
return await this.executeUnencryptedSave(tableName, data, whereConditions);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
// Подготавливаем данные для шифрования
const encryptedData = {};
const unencryptedData = {};
const filteredData = {}; // Отфильтрованные данные для параметров
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
let paramIndex = hasEncryptedFields ? 2 : 1; // Начинаем с 2, если есть зашифрованные поля, иначе с 1
for (const [key, value] of Object.entries(data)) {
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
const unencryptedColumn = columns.find(col => col.column_name === key);
console.log(`🔍 Обрабатываем поле ${key} = "${value}" (тип: ${typeof value})`);
if (encryptedColumn) {
// Если есть зашифрованная колонка, шифруем данные
// Проверяем, что значение не пустое перед шифрованием
if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) {
// Пропускаем пустые значения
console.log(`⚠️ Пропускаем пустое зашифрованное поле ${key}`);
continue;
}
const currentParamIndex = paramIndex++;
filteredData[key] = value; // Добавляем в отфильтрованные данные
console.log(`✅ Добавили зашифрованное поле ${key} в filteredData`);
if (encryptedColumn.data_type === 'jsonb') {
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
} else {
encryptedData[`${key}_encrypted`] = `encrypt_text($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
}
} else if (unencryptedColumn) {
// Если есть незашифрованная колонка, сохраняем как есть
// Проверяем, что значение не пустое перед сохранением (кроме role и sender_type)
if ((value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) &&
key !== 'role' && key !== 'sender_type') {
// Пропускаем пустые значения, кроме role и sender_type
console.log(`⚠️ Пропускаем пустое незашифрованное поле ${key}`);
continue;
}
filteredData[key] = value; // Добавляем в отфильтрованные данные
unencryptedData[key] = `$${paramIndex++}`;
console.log(`✅ Добавили незашифрованное поле ${key} в filteredData и unencryptedData`);
} else {
// Если колонка не найдена, пропускаем
console.warn(`⚠️ Колонка ${key} не найдена в таблице ${tableName}`);
}
}
const allData = { ...unencryptedData, ...encryptedData };
// Проверяем, есть ли данные для сохранения
if (Object.keys(allData).length === 0) {
console.warn(`⚠️ Нет данных для сохранения в таблице ${tableName} - все значения пустые`);
console.warn(`⚠️ Исходные данные:`, data);
console.warn(`⚠️ Отфильтрованные данные:`, filteredData);
return null;
}
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
if (whereConditions) {
// UPDATE
const setClause = Object.keys(allData)
.map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${quoteReservedWord(key)} = $${paramIndex + index}`)
.join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const allParams = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData), ...Object.values(whereConditions)] : [...Object.values(filteredData), ...Object.values(whereConditions)];
const { rows } = await db.getQuery()(query, allParams);
return rows[0];
} else {
// INSERT
const columns = Object.keys(allData).map(key => quoteReservedWord(key));
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const params = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData)] : [...Object.values(filteredData)];
const { rows } = await db.getQuery()(query, params);
return rows[0];
}
} catch (error) {
console.error(`❌ Ошибка сохранения данных в ${tableName}:`, error);
throw error;
}
}
/**
* Удалить данные из таблицы
*/
async deleteData(tableName, conditions) {
try {
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
// Для зашифрованных таблиц - пока используем обычный DELETE
// TODO: Добавить логику для зашифрованных условий WHERE
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
} catch (error) {
console.error(`❌ Ошибка удаления данных из ${tableName}:`, error);
throw error;
}
}
/**
* Получить статус шифрования
*/
getEncryptionStatus() {
return {
enabled: this.isEncryptionEnabled,
keyExists: !!this.encryptionKey,
keyPath: path.join(__dirname, '../../ssl/keys/full_db_encryption.key')
};
}
/**
* Проверить, нужно ли шифровать колонку
*/
shouldEncryptColumn(column) {
const encryptableTypes = ['text', 'varchar', 'character varying', 'json', 'jsonb'];
return encryptableTypes.includes(column.data_type) &&
!column.column_name.includes('_encrypted') &&
!['created_at', 'updated_at', 'id'].includes(column.column_name);
}
/**
* Выполнить незашифрованный запрос (fallback)
*/
async executeUnencryptedQuery(tableName, conditions = {}, limit = null, orderBy = null) {
let query = `SELECT * FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${key} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
if (limit) {
query += ` LIMIT ${limit}`;
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
/**
* Выполнить незашифрованное сохранение (fallback)
*/
async executeUnencryptedSave(tableName, data, whereConditions = null) {
if (whereConditions) {
// UPDATE
const setClause = Object.keys(data)
.map((key, index) => `${key} = $${index + 1}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${key} = $${Object.keys(data).length + index + 1}`)
.join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const params = [...Object.values(data), ...Object.values(whereConditions)];
const { rows } = await db.getQuery()(query, params);
return rows[0];
} else {
// INSERT
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const { rows } = await db.getQuery()(query, values);
return rows[0];
}
}
}
module.exports = new EncryptedDataService();

View File

@@ -12,6 +12,7 @@
console.log('[identity-service] loaded');
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const logger = require('../utils/logger');
const { getLinkedWallet } = require('./wallet-service');
@@ -53,7 +54,7 @@ class IdentityService {
* @param {number} userId - ID пользователя
* @param {string} provider - Тип идентификатора (wallet, email, telegram)
* @param {string} providerId - Значение идентификатора
* @param {boolean} verified - Флаг верификации идентификатора
* @param {boolean} verified - Флаг верификации идентификатора (не используется в БД)
* @returns {Promise<object>} - Результат операции
*/
async saveIdentity(userId, provider, providerId, verified = true) {
@@ -79,10 +80,10 @@ class IdentityService {
);
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]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: normalizedProviderId
});
return { success: true };
} catch (guestError) {
logger.error(
@@ -99,54 +100,42 @@ class IdentityService {
logger.warn(`[IdentityService] Invalid provider type: ${normalizedProvider}`);
return {
success: false,
error: `Invalid provider type. Allowed types: ${allowedProviders.join(', ')}`,
error: `Invalid provider type: ${normalizedProvider}`,
};
}
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]
);
const existingIdentity = await this.findIdentity(userId, normalizedProvider);
if (existingIdentity) {
// Обновляем существующий идентификатор
await encryptedDb.saveData('user_identities', {
provider: normalizedProvider,
provider_id: normalizedProviderId
}, {
user_id: userId,
provider: normalizedProvider
});
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}`
`[IdentityService] Updated identity for user ${userId}: ${normalizedProvider}=${normalizedProviderId}`
);
} 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]
);
// Создаем новый идентификатор
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: normalizedProvider,
provider_id: normalizedProviderId
});
logger.info(
`[IdentityService] Created new identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`
`[IdentityService] Saved new identity for user ${userId}: ${normalizedProvider}=${normalizedProviderId}`
);
}
return { success: true };
} catch (error) {
logger.error(
`[IdentityService] Error saving identity ${provider}:${providerId} for user ${userId}:`,
`[IdentityService] Error saving identity for user ${userId}:`,
error
);
return { success: false, error: error.message };
@@ -160,18 +149,9 @@ class IdentityService {
*/
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;
const identities = await encryptedDb.getData('user_identities', { user_id: userId });
logger.info(`[IdentityService] Found ${identities.length} identities for user ${userId}`);
return identities;
} catch (error) {
logger.error(`[IdentityService] Error getting identities for user ${userId}:`, error);
return [];
@@ -179,121 +159,70 @@ class IdentityService {
}
/**
* Получает все идентификаторы пользователя определенного типа
* Получает идентификаторы пользователя по типу провайдера
* @param {number} userId - ID пользователя
* @param {string} provider - Тип идентификатора
* @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);
const identities = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: provider.toLowerCase()
});
return identities;
} catch (error) {
logger.error(
`[IdentityService] Error getting ${provider} identities for user ${userId}:`,
error
);
logger.error(`[IdentityService] Error getting identities by provider for user ${userId}:`, error);
return [];
}
}
/**
* Находит пользователя по идентификатору
* @param {string} provider - Тип идентификатора
* @param {string} provider - Тип провайдера
* @param {string} providerId - Значение идентификатора
* @returns {Promise<object|null>} - Информация о пользователе или null
* @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]
);
const identities = await encryptedDb.getData('user_identities', {
provider: normalizedProvider,
provider_id: normalizedProviderId
}, 1);
if (result.rows.length === 0) {
logger.info(
`[IdentityService] No user found with identity ${normalizedProvider}:${normalizedProviderId}`
);
if (identities.length === 0) {
return null;
}
logger.info(
`[IdentityService] Found user ${result.rows[0].id} with identity ${normalizedProvider}:${normalizedProviderId}`
);
return result.rows[0];
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 ${provider}:${providerId}:`,
error
);
logger.error(`[IdentityService] Error finding user by identity:`, error);
return null;
}
}
/**
* Находит конкретный идентификатор пользователя по его типу.
* Возвращает первую найденную запись.
* Находит конкретный идентификатор пользователя
* @param {number} userId - ID пользователя
* @param {string} provider - Тип идентификатора (например, 'wallet', 'email')
* @returns {Promise<object|null>} - Объект идентификатора (содержит provider_id) или null
* @param {string} provider - Тип провайдера
* @returns {Promise<object|null>} - Идентификатор или null
*/
async findIdentity(userId, provider) {
try {
if (!userId || !provider) {
logger.warn(`[IdentityService] Missing parameters for findIdentity: userId=${userId}, provider=${provider}`);
return null;
}
const identities = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: provider.toLowerCase()
}, 1);
// Нормализуем провайдера
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)
return identities.length > 0 ? identities[0] : null;
} catch (error) {
logger.error(
`[IdentityService] Error finding ${provider} identity for user ${userId}:`,
error
);
logger.error(`[IdentityService] Error finding identity for user ${userId}:`, error);
return null;
}
}
@@ -337,10 +266,10 @@ class IdentityService {
// Сохраняем гостевые идентификаторы в 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]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: session.guestId
});
results.push({ type: 'guest', result: { success: true } });
} catch (error) {
logger.error(`[IdentityService] Error saving guest ID for user ${userId}:`, error);
@@ -350,10 +279,10 @@ class IdentityService {
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]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: session.previousGuestId
});
results.push({ type: 'previousGuest', result: { success: true } });
} catch (error) {
logger.error(
@@ -392,92 +321,75 @@ class IdentityService {
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]
);
const identities = await encryptedDb.getData('user_identities', { user_id: 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]
);
for (const identity of identities) {
// Создаем новый идентификатор для целевого пользователя
await encryptedDb.saveData('user_identities', {
user_id: toUserId,
provider: identity.provider,
provider_id: 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]
);
await encryptedDb.deleteData('user_identities', {
user_id: fromUserId,
provider: identity.provider,
provider_id: identity.provider_id
});
}
// Мигрируем гостевые идентификаторы из новой таблицы guest_user_mapping
const guestMappingsResult = await client.query(
`SELECT guest_id, processed FROM guest_user_mapping WHERE user_id = $1`,
[fromUserId]
);
// Мигрируем гостевые идентификаторы
const guestMappings = await encryptedDb.getData('guest_user_mapping', { user_id: 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]
);
for (const mapping of guestMappings) {
await encryptedDb.saveData('guest_user_mapping', {
user_id: toUserId,
guest_id: mapping.guest_id,
processed: mapping.processed
});
}
// Удаляем старые гостевые маппинги
await client.query(`DELETE FROM guest_user_mapping WHERE user_id = $1`, [fromUserId]);
await encryptedDb.deleteData('guest_user_mapping', { user_id: fromUserId });
// Переносим все сообщения
await client.query(
`UPDATE messages
SET user_id = $1
WHERE user_id = $2`,
[toUserId, 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 });
}
// Переносим все диалоги
await client.query(
`UPDATE conversations
SET user_id = $1
WHERE user_id = $2`,
[toUserId, fromUserId]
);
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 });
}
// Переносим настройки пользователя
await client.query(
`UPDATE user_preferences
SET user_id = $1
WHERE user_id = $2`,
[toUserId, fromUserId]
);
// Завершаем транзакцию
await client.query('COMMIT');
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) {
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 };
@@ -496,14 +408,12 @@ class IdentityService {
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]
);
const users = await encryptedDb.getData('user_identities', {
provider: provider,
provider_id: providerId
});
result.rows.forEach((row) => userIds.add(row.user_id));
users.forEach((user) => userIds.add(user.user_id));
}
return Array.from(userIds);
@@ -527,12 +437,13 @@ class IdentityService {
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]
);
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.rowCount };
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 };
@@ -551,8 +462,10 @@ class IdentityService {
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;
const newUser = await encryptedDb.saveData('users', {
role: 'user'
});
const userId = newUser.id;
await this.saveIdentity(userId, provider, providerId, true);
user = { id: userId, role: 'user' };
isNew = true;
@@ -567,7 +480,11 @@ class IdentityService {
role = isAdmin ? 'admin' : 'user';
// Обновляем роль в users, если изменилась
if (user.role !== role) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, user.id]);
await encryptedDb.saveData('users', {
role: role
}, {
id: user.id
});
}
}
return { userId: user.id, role, isNew };

View File

@@ -10,22 +10,26 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const vectorSearch = require('./vectorSearchClient');
const { getProviderSettings } = require('./aiProviderSettingsService');
console.log('[RAG] ragService.js loaded');
// Простой кэш для RAG результатов
const ragCache = new Map();
const RAG_CACHE_TTL = 5 * 60 * 1000; // 5 минут
async function getTableData(tableId) {
console.log(`[RAG] getTableData called for tableId: ${tableId}`);
const columns = (await db.query('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
const columns = await encryptedDb.getData('user_columns', { table_id: tableId });
console.log(`[RAG] Found ${columns.length} columns:`, columns.map(col => ({ id: col.id, name: col.name, purpose: col.options?.purpose })));
const rows = (await db.query('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
const rows = await encryptedDb.getData('user_rows', { table_id: tableId });
console.log(`[RAG] Found ${rows.length} rows:`, rows.map(row => ({ id: row.id, name: row.name })));
const cellValues = (await db.query('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
const cellValues = await encryptedDb.getData('user_cell_values', { row_id: { $in: rows.map(row => row.id) } });
console.log(`[RAG] Found ${cellValues.length} cell values`);
const getColId = purpose => columns.find(col => col.options?.purpose === purpose)?.id;
@@ -66,6 +70,14 @@ async function getTableData(tableId) {
async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10 }) {
console.log(`[RAG] ragAnswer called: tableId=${tableId}, userQuestion="${userQuestion}"`);
// Проверяем кэш
const cacheKey = `${tableId}:${userQuestion}:${product}`;
const cached = ragCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < RAG_CACHE_TTL) {
console.log(`[RAG] Returning cached result for: ${cacheKey}`);
return cached.result;
}
const data = await getTableData(tableId);
console.log(`[RAG] Got ${data.length} rows from database`);
@@ -110,7 +122,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
// Поиск
let results = [];
if (rowsForUpsert.length > 0) {
results = await vectorSearch.search(tableId, userQuestion, 3);
results = await vectorSearch.search(tableId, userQuestion, 2); // Уменьшаем до 2 результатов
console.log(`[RAG] Search completed, got ${results.length} results`);
// Подробное логирование результатов поиска
@@ -153,7 +165,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
});
}
return {
const result = {
answer: best?.metadata?.answer,
context: best?.metadata?.context,
product: best?.metadata?.product,
@@ -161,6 +173,14 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
date: best?.metadata?.date,
score: best?.score,
};
// Кэшируем результат
ragCache.set(cacheKey, {
result,
timestamp: Date.now()
});
return result;
}
/**
@@ -169,16 +189,11 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
*/
async function getAllPlaceholdersWithValues() {
// Получаем все плейсхолдеры и их значения (берём первое значение для каждого плейсхолдера)
const result = await db.getQuery()(`
SELECT c.placeholder, cv.value
FROM user_columns c
JOIN user_cell_values cv ON c.id = cv.column_id
WHERE c.placeholder IS NOT NULL AND c.placeholder != ''
ORDER BY c.id, cv.id
`);
const result = await encryptedDb.getData('user_columns', {});
// Группируем по плейсхолдеру (берём первое значение)
const map = {};
for (const row of result.rows) {
for (const row of result) {
if (row.placeholder && !(row.placeholder in map)) {
map[row.placeholder] = row.value;
}

View File

@@ -10,39 +10,56 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
async function getAllRpcProviders() {
const { rows } = await db.getQuery()('SELECT * FROM rpc_providers ORDER BY id');
return rows;
const providers = await encryptedDb.getData('rpc_providers', {}, null, 'id');
return providers;
}
async function saveAllRpcProviders(rpcConfigs) {
await db.getQuery()('DELETE FROM rpc_providers');
// Удаляем все существующие провайдеры
await encryptedDb.deleteData('rpc_providers', {});
// Сохраняем новые провайдеры
for (const cfg of rpcConfigs) {
await db.query(
'INSERT INTO rpc_providers (network_id, rpc_url, chain_id) VALUES ($1, $2, $3)',
[cfg.networkId, cfg.rpcUrl, cfg.chainId || null]
);
await encryptedDb.saveData('rpc_providers', {
network_id: cfg.networkId,
rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null
});
}
}
async function upsertRpcProvider(cfg) {
await db.query(
`INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
VALUES ($1, $2, $3)
ON CONFLICT (network_id) DO UPDATE SET rpc_url=EXCLUDED.rpc_url, chain_id=EXCLUDED.chain_id`,
[cfg.networkId, cfg.rpcUrl, cfg.chainId || null]
);
// Проверяем, существует ли провайдер
const existing = await encryptedDb.getData('rpc_providers', { network_id: cfg.networkId }, 1);
if (existing.length > 0) {
// Обновляем существующий провайдер
await encryptedDb.saveData('rpc_providers', {
rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null
}, {
network_id: cfg.networkId
});
} else {
// Создаем новый провайдер
await encryptedDb.saveData('rpc_providers', {
network_id: cfg.networkId,
rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null
});
}
}
async function deleteRpcProvider(networkId) {
await db.getQuery()('DELETE FROM rpc_providers WHERE network_id = $1', [networkId]);
await encryptedDb.deleteData('rpc_providers', { network_id: networkId });
}
async function getRpcUrlByNetworkId(networkId) {
const { rows } = await db.getQuery()('SELECT rpc_url FROM rpc_providers WHERE network_id = $1', [networkId]);
return rows[0]?.rpc_url || null;
const providers = await encryptedDb.getData('rpc_providers', { network_id: networkId }, 1);
return providers[0]?.rpc_url || null;
}
module.exports = { getAllRpcProviders, saveAllRpcProviders, upsertRpcProvider, deleteRpcProvider, getRpcUrlByNetworkId };

View File

@@ -11,7 +11,7 @@
*/
const logger = require('../utils/logger');
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const { processGuestMessages } = require('../routes/chat');
/**
@@ -50,11 +50,8 @@ class SessionService {
async linkGuestMessages(session, userId) {
try {
// Получаем все гостевые ID для текущего пользователя из таблицы
const guestIdsResult = await db.getQuery()(
'SELECT guest_id FROM guest_user_mapping WHERE user_id = $1',
[userId]
);
const userGuestIds = guestIdsResult.rows.map((row) => row.guest_id);
const guestIdsResult = await encryptedDb.getData('guest_user_mapping', { user_id: userId });
const userGuestIds = guestIdsResult.map((row) => row.guest_id);
// Собираем все гостевые ID, которые нужно обработать
const guestIdsToProcess = new Set();
@@ -66,10 +63,10 @@ class SessionService {
guestIdsToProcess.add(session.guestId);
// Записываем связь с пользователем в новую таблицу
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]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: session.guestId
});
}
}
@@ -80,10 +77,10 @@ class SessionService {
guestIdsToProcess.add(session.previousGuestId);
// Записываем связь с пользователем в новую таблицу
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]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: session.previousGuestId
});
}
}
@@ -98,24 +95,18 @@ class SessionService {
// Логируем только если есть что обрабатывать
if (guestIdsToProcess.size > 0) {
logger.info(
`[linkGuestMessages] Processing ${guestIdsToProcess.size} guest IDs for user ${userId}`
`[SessionService] Linking ${guestIdsToProcess.size} guest IDs to user ${userId}: ${Array.from(guestIdsToProcess).join(', ')}`
);
// Обрабатываем сообщения для каждого гостевого ID
for (const guestId of guestIdsToProcess) {
await this.processGuestMessagesWrapper(userId, guestId);
}
}
// Обрабатываем все собранные гостевые ID
for (const guestId of guestIdsToProcess) {
await this.processGuestMessagesWrapper(userId, guestId);
// Помечаем guestId как обработанный в базе данных
await db.getQuery()(
'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1',
[guestId]
);
}
return { success: true };
return { success: true, processedCount: guestIdsToProcess.size };
} catch (error) {
logger.error('[linkGuestMessages] Error:', error);
logger.error(`[SessionService] Error linking guest messages:`, error);
return { success: false, error: error.message };
}
}
@@ -127,12 +118,9 @@ class SessionService {
*/
async isGuestIdProcessed(guestId) {
try {
const result = await db.getQuery()(
'SELECT processed FROM guest_user_mapping WHERE guest_id = $1',
[guestId]
);
const result = await encryptedDb.getData('guest_user_mapping', { guest_id: guestId });
return result.rows.length > 0 && result.rows[0].processed === true;
return result.length > 0 && result[0].processed === true;
} catch (error) {
logger.error(`[isGuestIdProcessed] Error checking guest ID ${guestId}:`, error);
return false;
@@ -208,17 +196,14 @@ class SessionService {
logger.info(`[SessionService] Attempting to retrieve session ${sessionId}`);
const result = await db.getQuery()(
'SELECT sess FROM session WHERE sid = $1',
[sessionId]
);
const result = await encryptedDb.getData('session', { sid: sessionId });
if (result.rows.length === 0) {
if (result.length === 0) {
logger.info(`[SessionService] No session found with ID ${sessionId}`);
return null;
}
const sessionData = result.rows[0].sess;
const sessionData = result[0].sess;
logger.info(`[SessionService] Retrieved session data for ${sessionId}`);
return sessionData;
@@ -294,13 +279,16 @@ class SessionService {
logger.info('[SessionService] Starting cleanup of processedGuestIds from sessions');
// Используем один SQL-запрос для обновления всех сессий
const result = await db.getQuery()(
`UPDATE session
SET sess = (sess::jsonb - 'processedGuestIds')::json
WHERE sess::text LIKE '%"processedGuestIds"%'`
);
const result = await encryptedDb.getData('session', { sess: { $regex: '.*"processedGuestIds":' } });
logger.info(`[SessionService] Cleaned processedGuestIds from ${result.rowCount} sessions`);
for (const session of result) {
const sessJson = JSON.parse(session.sess);
delete sessJson.processedGuestIds;
session.sess = JSON.stringify(sessJson);
await encryptedDb.saveData('session', session);
}
logger.info(`[SessionService] Cleaned processedGuestIds from ${result.length} sessions`);
return true;
} catch (error) {
logger.error('[SessionService] Error during cleanup:', error);

View File

@@ -12,6 +12,7 @@
const { Telegraf } = require('telegraf');
const logger = require('../utils/logger');
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const authService = require('./auth-service');
const verificationService = require('./verification-service');
@@ -19,7 +20,7 @@ const crypto = require('crypto');
const identityService = require('./identity-service');
const aiAssistant = require('./ai-assistant');
const { checkAdminRole } = require('./admin-role');
const { broadcastContactsUpdate } = require('../wsHub');
const { broadcastContactsUpdate, broadcastChatMessage } = require('../wsHub');
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
const { ragAnswer, generateLLMResponse } = require('./ragService');
const { isUserBlocked } = require('../utils/userUtils');
@@ -29,17 +30,23 @@ let telegramSettingsCache = null;
async function getTelegramSettings() {
if (telegramSettingsCache) return telegramSettingsCache;
const { rows } = await db.getQuery()('SELECT * FROM telegram_settings ORDER BY id LIMIT 1');
if (!rows.length) throw new Error('Telegram settings not found in DB');
telegramSettingsCache = rows[0];
const settings = await encryptedDb.getData('telegram_settings', {}, 1);
if (!settings.length) throw new Error('Telegram settings not found in DB');
telegramSettingsCache = settings[0];
return telegramSettingsCache;
}
// Создание и настройка бота
async function getBot() {
console.log('[TelegramBot] getBot() called');
if (!botInstance) {
console.log('[TelegramBot] Creating new bot instance...');
const settings = await getTelegramSettings();
console.log('[TelegramBot] Got settings, creating Telegraf instance...');
botInstance = new Telegraf(settings.bot_token);
console.log('[TelegramBot] Telegraf instance created');
// Обработка команды /start
botInstance.command('start', (ctx) => {
@@ -51,49 +58,51 @@ async function getBot() {
const text = ctx.message.text.trim();
// 1. Если команда — пропустить
if (text.startsWith('/')) return;
// Отправляем индикатор печати для улучшения UX
const typingAction = ctx.replyWithChatAction('typing');
// 2. Проверка: это потенциальный код?
const isPotentialCode = (str) => /^[A-Z0-9]{6}$/i.test(str);
if (isPotentialCode(text)) {
await typingAction;
try {
// Получаем код верификации для всех активных кодов с провайдером telegram
const codeResult = await db.getQuery()(
`SELECT * FROM verification_codes
WHERE code = $1
AND provider = 'telegram'
AND used = false
AND expires_at > NOW()`,
[text.toUpperCase()]
);
const codes = await encryptedDb.getData('verification_codes', {
code: text.toUpperCase(),
provider: 'telegram',
used: false
}, 1);
if (codeResult.rows.length === 0) {
if (codes.length === 0) {
ctx.reply('Неверный код подтверждения');
return;
}
const verification = codeResult.rows[0];
const verification = codes[0];
const providerId = verification.provider_id;
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть
let userId;
let userRole = 'user'; // Роль по умолчанию
// Отмечаем код как использованный
await db.getQuery()('UPDATE verification_codes SET used = true WHERE id = $1', [
verification.id,
]);
await encryptedDb.saveData('verification_codes', {
used: true
}, {
id: verification.id
});
logger.info('Starting Telegram auth process for code:', text);
// Проверяем, существует ли уже пользователь с таким Telegram ID
const existingTelegramUser = await db.getQuery()(
`SELECT ui.user_id
FROM user_identities ui
WHERE ui.provider = 'telegram' AND ui.provider_id = $1`,
[ctx.from.id.toString()]
);
const existingTelegramUsers = await encryptedDb.getData('user_identities', {
provider: 'telegram',
provider_id: ctx.from.id.toString()
}, 1);
if (existingTelegramUser.rows.length > 0) {
if (existingTelegramUsers.length > 0) {
// Если пользователь с таким Telegram ID уже существует, используем его
userId = existingTelegramUser.rows[0].user_id;
userId = existingTelegramUsers[0].user_id;
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
} else {
// Если код верификации был связан с существующим пользователем, используем его
@@ -101,12 +110,11 @@ async function getBot() {
// Используем userId из кода верификации
userId = linkedUserId;
// Связываем Telegram с этим пользователем
await db.getQuery()(
`INSERT INTO user_identities
(user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`,
[userId, 'telegram', ctx.from.id.toString()]
);
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: 'telegram',
provider_id: ctx.from.id.toString()
});
logger.info(
`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`
);
@@ -114,12 +122,11 @@ async function getBot() {
// Проверяем, есть ли пользователь, связанный с гостевым идентификатором
let existingUserWithGuestId = null;
if (providerId) {
const guestUserResult = await db.getQuery()(
`SELECT user_id FROM guest_user_mapping WHERE guest_id = $1`,
[providerId]
);
if (guestUserResult.rows.length > 0) {
existingUserWithGuestId = guestUserResult.rows[0].user_id;
const guestUserResult = await encryptedDb.getData('guest_user_mapping', {
guest_id: providerId
}, 1);
if (guestUserResult.length > 0) {
existingUserWithGuestId = guestUserResult[0].user_id;
logger.info(
`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`
);
@@ -129,38 +136,35 @@ async function getBot() {
if (existingUserWithGuestId) {
// Используем существующего пользователя и добавляем ему Telegram идентификатор
userId = existingUserWithGuestId;
await db.getQuery()(
`INSERT INTO user_identities
(user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`,
[userId, 'telegram', ctx.from.id.toString()]
);
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: 'telegram',
provider_id: ctx.from.id.toString()
});
logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`);
} else {
// Создаем нового пользователя, если не нашли существующего
const userResult = await db.getQuery()(
'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id',
['user']
);
userId = userResult.rows[0].id;
const userResult = await encryptedDb.saveData('users', {
created_at: new Date(),
role: 'user'
});
userId = userResult.id;
// Связываем Telegram с новым пользователем
await db.getQuery()(
`INSERT INTO user_identities
(user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`,
[userId, 'telegram', ctx.from.id.toString()]
);
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: 'telegram',
provider_id: ctx.from.id.toString()
});
// Если был гостевой ID, связываем его с новым пользователем
if (providerId) {
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, providerId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: providerId
}, {
user_id: userId
});
}
logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`);
@@ -180,25 +184,35 @@ async function getBot() {
logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`);
// Опционально: Обновить роль в таблице users
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0 && currentUser.rows[0].role !== userRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [userRole, userId]);
const currentUser = await encryptedDb.getData('users', {
id: userId
}, 1);
if (currentUser.length > 0 && currentUser[0].role !== userRole) {
await encryptedDb.saveData('users', {
role: userRole
}, {
id: userId
});
logger.info(`[TelegramBot] Updated user role in DB to ${userRole}`);
}
} else {
logger.info(`[TelegramBot] No linked wallet found for user ${userId}. Checking current DB role.`);
// Если кошелька нет, берем текущую роль из базы
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0) {
userRole = currentUser.rows[0].role;
const currentUser = await encryptedDb.getData('users', {
id: userId
}, 1);
if (currentUser.length > 0) {
userRole = currentUser[0].role;
}
}
} catch (roleCheckError) {
logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError);
// В случае ошибки берем роль из базы или оставляем 'user'
try {
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0) { userRole = currentUser.rows[0].role; }
const currentUser = await encryptedDb.getData('users', {
id: userId
}, 1);
if (currentUser.length > 0) { userRole = currentUser[0].role; }
} catch (dbError) { /* ignore */ }
}
} else {
@@ -214,37 +228,28 @@ async function getBot() {
try {
// Ищем сессию, где есть userId и она не истекла (проверка expires_at)
// Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько
const sessionResult = await db.getQuery()(
`SELECT sid FROM session
WHERE sess ->> 'userId' = $1
AND expire > NOW()
ORDER BY expire DESC
LIMIT 1`,
[userId?.toString()] // Используем optional chaining и преобразуем в строку
);
const sessionResult = await encryptedDb.getData('session', {
'sess->>userId': userId?.toString()
}, 1, 'expire', 'DESC');
if (sessionResult.rows.length > 0) {
activeSessionId = sessionResult.rows[0].sid;
if (sessionResult.length > 0) {
activeSessionId = sessionResult[0].sid;
logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`);
// Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram
const updateResult = await db.getQuery()(
`UPDATE session
SET sess = (sess::jsonb || $1::jsonb)::json
WHERE sid = $2`,
[
JSON.stringify({
// authenticated: true, // Не перезаписываем, т.к. сессия уже должна быть аутентифицирована
authType: 'telegram', // Обновляем тип аутентификации
telegramId: ctx.from.id.toString(),
telegramUsername: ctx.from.username,
telegramFirstName: ctx.from.first_name,
role: userRole, // Записываем определенную роль
// userId: userId?.toString() // userId уже должен быть в сессии
}),
activeSessionId // Обновляем по найденному session ID
]
);
const updateResult = await encryptedDb.saveData('session', {
sess: JSON.stringify({
// authenticated: true, // Не перезаписываем, т.к. сессия уже должна быть аутентифицирована
authType: 'telegram', // Обновляем тип аутентификации
telegramId: ctx.from.id.toString(),
telegramUsername: ctx.from.username,
telegramFirstName: ctx.from.first_name,
role: userRole, // Записываем определенную роль
// userId: userId?.toString() // userId уже должен быть в сессии
})
}, {
sid: activeSessionId
});
if (updateResult.rowCount > 0) {
logger.info(`[telegramBot] Session ${activeSessionId} updated successfully with Telegram data for user ${userId}`);
@@ -277,31 +282,36 @@ async function getBot() {
}
return;
}
// 3. Всё остальное — чат с ИИ-ассистентом
// 3. Всё остальное — чат с ИИ-ассистентом
try {
const telegramId = ctx.from.id.toString();
// 1. Найти или создать пользователя
const { userId, role } = await identityService.findOrCreateUserWithRole('telegram', telegramId);
if (await isUserBlocked(userId)) {
await ctx.reply('Вы заблокированы. Сообщения не принимаются.');
return;
}
// 1.1 Найти или создать беседу
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
);
let conversationResult = await encryptedDb.getData('conversations', {
user_id: userId
}, 1, 'updated_at', 'DESC', 'created_at', 'DESC');
let conversation;
if (conversationResult.rows.length === 0) {
if (conversationResult.length === 0) {
const title = `Чат с пользователем ${userId}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[userId, title]
);
conversation = newConv.rows[0];
const newConv = await encryptedDb.saveData('conversations', {
user_id: userId,
title: title,
created_at: new Date(),
updated_at: new Date()
});
conversation = newConv;
} else {
conversation = conversationResult.rows[0];
conversation = conversationResult[0];
}
// 2. Сохранять все сообщения с conversation_id
let content = text;
let attachmentMeta = {};
@@ -330,80 +340,171 @@ async function getBot() {
mimeType = ctx.message.video.mime_type || 'video/mp4';
fileSize = ctx.message.video.file_size;
}
// Асинхронная загрузка файлов
if (fileId) {
// Скачиваем файл
const fileLink = await ctx.telegram.getFileLink(fileId);
const res = await fetch(fileLink.href);
attachmentBuffer = await res.buffer();
attachmentMeta = {
attachment_filename: fileName,
attachment_mimetype: mimeType,
attachment_size: fileSize,
attachment_data: attachmentBuffer
};
try {
const fileLink = await ctx.telegram.getFileLink(fileId);
const res = await fetch(fileLink.href);
attachmentBuffer = await res.buffer();
attachmentMeta = {
attachment_filename: fileName,
attachment_mimetype: mimeType,
attachment_size: fileSize,
attachment_data: attachmentBuffer
};
} catch (fileError) {
logger.error('[TelegramBot] Error downloading file:', fileError);
// Продолжаем без файла
}
}
// Сохраняем сообщение в БД
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10, $11)`,
[userId, conversation.id, 'user', content, 'telegram', role, 'in',
attachmentMeta.attachment_filename || null,
attachmentMeta.attachment_mimetype || null,
attachmentMeta.attachment_size || null,
attachmentMeta.attachment_data || null
]
);
if (!conversation || !conversation.id) {
logger.error(`[TelegramBot] Conversation is undefined or has no id for user ${userId}`);
await ctx.reply('Произошла ошибка при создании диалога. Попробуйте позже.');
return;
}
const userMessage = await encryptedDb.saveData('messages', {
user_id: userId,
conversation_id: conversation.id,
sender_type: 'user',
content: content,
channel: 'telegram',
role: role,
direction: 'in',
created_at: new Date(),
attachment_filename: attachmentMeta.attachment_filename || null,
attachment_mimetype: attachmentMeta.attachment_mimetype || null,
attachment_size: attachmentMeta.attachment_size || null,
attachment_data: attachmentMeta.attachment_data || null
});
// Отправляем WebSocket уведомление о пользовательском сообщении
try {
const decryptedUserMessage = await encryptedDb.getData('messages', { id: userMessage.id }, 1);
if (decryptedUserMessage && decryptedUserMessage[0]) {
broadcastChatMessage(decryptedUserMessage[0], userId);
}
} catch (wsError) {
logger.error('[TelegramBot] WebSocket notification error for user message:', wsError);
}
if (await isUserBlocked(userId)) {
logger.info(`[TelegramBot] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`);
return;
}
// 3. Получить ответ от ИИ (RAG + LLM)
const aiSettings = await aiAssistantSettingsService.getSettings();
let ragTableId = null;
if (aiSettings && aiSettings.selected_rag_tables) {
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
? aiSettings.selected_rag_tables[0]
: aiSettings.selected_rag_tables;
}
let aiResponse;
if (ragTableId) {
// Сначала ищем ответ через RAG
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
aiResponse = ragResult.answer;
} else {
aiResponse = await generateLLMResponse({
userQuestion: content,
context: ragResult && ragResult.context ? ragResult.context : '',
answer: ragResult && ragResult.answer ? ragResult.answer : '',
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
history: null,
model: aiSettings ? aiSettings.model : undefined,
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
});
// 3. Получить ответ от ИИ (RAG + LLM) - асинхронно
const aiResponsePromise = (async () => {
const aiSettings = await aiAssistantSettingsService.getSettings();
let ragTableId = null;
if (aiSettings && aiSettings.selected_rag_tables) {
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
? aiSettings.selected_rag_tables[0]
: aiSettings.selected_rag_tables;
}
} else {
aiResponse = await aiAssistant.getResponse(content, 'auto');
}
// Загружаем историю сообщений для контекста (ограничиваем до 5 сообщений)
let history = null;
try {
const recentMessages = await encryptedDb.getData('messages', {
conversation_id: conversation.id
}, 5, 'created_at DESC');
if (recentMessages && recentMessages.length > 0) {
// Преобразуем сообщения в формат для AI
history = recentMessages.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
content: msg.content || '' // content уже расшифрован encryptedDb
}));
}
} catch (historyError) {
logger.error('[TelegramBot] Error loading message history:', historyError);
}
let aiResponse;
if (ragTableId) {
// Сначала ищем ответ через RAG
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
aiResponse = ragResult.answer;
} else {
aiResponse = await generateLLMResponse({
userQuestion: content,
context: ragResult && ragResult.context ? ragResult.context : '',
answer: ragResult && ragResult.answer ? ragResult.answer : '',
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
history: history,
model: aiSettings ? aiSettings.model : undefined,
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
});
}
} else {
// Используем системный промпт из настроек, если RAG не используется
const systemPrompt = aiSettings ? aiSettings.system_prompt : '';
aiResponse = await aiAssistant.getResponse(content, 'auto', history, systemPrompt);
}
return aiResponse;
})();
// Ждем ответ от ИИ с таймаутом
const aiResponse = await Promise.race([
aiResponsePromise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('AI response timeout')), 60000)
)
]);
// 4. Сохранить ответ в БД с conversation_id
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`,
[userId, conversation.id, 'assistant', aiResponse, 'telegram', role, 'out']
);
const aiMessage = await encryptedDb.saveData('messages', {
user_id: userId,
conversation_id: conversation.id,
sender_type: 'assistant',
content: aiResponse,
channel: 'telegram',
role: role,
direction: 'out',
created_at: new Date()
});
// 5. Отправить ответ пользователю
await ctx.reply(aiResponse);
// 6. Отправить WebSocket уведомление
try {
const decryptedAiMessage = await encryptedDb.getData('messages', { id: aiMessage.id }, 1);
if (decryptedAiMessage && decryptedAiMessage[0]) {
broadcastChatMessage(decryptedAiMessage[0], userId);
}
} catch (wsError) {
logger.error('[TelegramBot] WebSocket notification error:', wsError);
}
} catch (error) {
logger.error('[TelegramBot] Ошибка при обработке сообщения:', error);
await ctx.reply('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.');
}
});
// Запуск бота
await botInstance.launch();
logger.info('[TelegramBot] Бот запущен');
// Запуск бота с таймаутом
console.log('[TelegramBot] Before botInstance.launch()');
try {
// Запускаем бота с таймаутом
const launchPromise = botInstance.launch();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Telegram bot launch timeout')), 10000); // 10 секунд таймаут
});
await Promise.race([launchPromise, timeoutPromise]);
console.log('[TelegramBot] After botInstance.launch()');
logger.info('[TelegramBot] Бот запущен');
} catch (error) {
console.error('[TelegramBot] Error launching bot:', error);
// Не выбрасываем ошибку, чтобы не блокировать запуск сервера
console.log('[TelegramBot] Bot launch failed, but continuing...');
}
}
return botInstance;
@@ -436,12 +537,12 @@ async function initTelegramAuth(session) {
const guestId = session.guestId || tempId;
// Связываем гостевой ID с текущим пользователем
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`,
[session.userId, guestId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: session.userId,
guest_id: guestId
}, {
user_id: session.userId
});
logger.info(
`[initTelegramAuth] Linked guestId ${guestId} to authenticated user ${session.userId}`
@@ -475,8 +576,8 @@ function clearSettingsCache() {
}
async function getAllBots() {
const { rows } = await db.getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
return rows;
const settings = await encryptedDb.getData('telegram_settings', {}, 1, 'id');
return settings;
}
module.exports = {

View File

@@ -10,26 +10,25 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
async function deleteUserById(userId) {
console.log('[DELETE] Вызван deleteUserById для userId:', userId);
const query = db.getQuery();
try {
await query('BEGIN');
console.log('[DELETE] Начинаем удаление user_identities для userId:', userId);
const resIdentities = await query('DELETE FROM user_identities WHERE user_id = $1', [userId]);
console.log('[DELETE] Удалено user_identities:', resIdentities.rowCount);
const resIdentities = await encryptedDb.deleteData('user_identities', { user_id: userId });
console.log('[DELETE] Удалено user_identities:', resIdentities.length);
console.log('[DELETE] Начинаем удаление messages для userId:', userId);
const resMessages = await query('DELETE FROM messages WHERE user_id = $1', [userId]);
console.log('[DELETE] Удалено messages:', resMessages.rowCount);
const resMessages = await encryptedDb.deleteData('messages', { user_id: userId });
console.log('[DELETE] Удалено messages:', resMessages.length);
console.log('[DELETE] Начинаем удаление пользователя из users:', userId);
const result = await query('DELETE FROM users WHERE id = $1 RETURNING *', [userId]);
console.log('[DELETE] Результат удаления пользователя:', result.rowCount, result.rows);
await query('COMMIT');
return result.rowCount;
const result = await encryptedDb.deleteData('users', { id: userId });
console.log('[DELETE] Результат удаления пользователя:', result.length, result);
return result.length;
} catch (e) {
await query('ROLLBACK');
console.error('[DELETE] Ошибка при удалении пользователя:', e);
throw e;
}

View File

@@ -10,7 +10,7 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const logger = require('../utils/logger');
class VerificationService {
@@ -39,23 +39,21 @@ class VerificationService {
`Creating verification code for ${provider}:${providerId}, userId: ${userId || 'null'}`
);
// Если userId не указан, добавляем запись без ссылки на пользователя
if (userId === null || userId === undefined) {
await db.getQuery()(
`INSERT INTO verification_codes
(code, provider, provider_id, expires_at)
VALUES ($1, $2, $3, $4)`,
[code, provider, providerId, expiresAt]
);
} else {
await db.getQuery()(
`INSERT INTO verification_codes
(code, provider, provider_id, user_id, expires_at)
VALUES ($1, $2, $3, $4, $5)`,
[code, provider, providerId, userId, expiresAt]
);
const data = {
code: code,
provider: provider,
provider_id: providerId,
expires_at: expiresAt,
used: false
};
// Если userId указан, добавляем его
if (userId !== null && userId !== undefined) {
data.user_id = userId;
}
await encryptedDb.saveData('verification_codes', data);
logger.info(`Verification code created successfully for ${provider}:${providerId}`);
return code;
} catch (error) {
@@ -79,53 +77,52 @@ class VerificationService {
logger.info(`Normalized code: ${normalizedCode}`);
// Проверим, есть ли такой код в базе (для отладки)
const checkResult = await db.getQuery()(
`SELECT code FROM verification_codes
WHERE provider = $1
AND provider_id = $2
AND used = false
AND expires_at > NOW()`,
[provider, providerId]
);
const checkResult = await encryptedDb.getData('verification_codes', {
provider: provider,
provider_id: providerId,
used: false
});
if (checkResult.rows.length > 0) {
if (checkResult.length > 0) {
logger.info(
`Found codes for ${provider}:${providerId}: ${JSON.stringify(checkResult.rows.map((r) => r.code))}`
`Found codes for ${provider}:${providerId}: ${JSON.stringify(checkResult.map((r) => r.code))}`
);
} else {
logger.warn(`No active codes found for ${provider}:${providerId}`);
}
const result = await db.getQuery()(
`SELECT * FROM verification_codes
WHERE code = $1
AND provider = $2
AND provider_id = $3
AND used = false
AND expires_at > NOW()`,
[normalizedCode, provider, providerId]
);
const result = await encryptedDb.getData('verification_codes', {
code: normalizedCode,
provider: provider,
provider_id: providerId,
used: false
}, 1);
if (result.rows.length === 0) {
logger.warn(
`Invalid or expired code for ${provider}:${providerId}. Input: ${normalizedCode}`
);
return { success: false, error: 'Неверный или истекший код' };
if (result.length === 0) {
logger.warn(`No valid verification code found for ${provider}:${providerId}`);
return { valid: false, message: 'Invalid or expired code' };
}
const verification = result.rows[0];
const verificationCode = result[0];
// Проверяем срок действия
if (new Date(verificationCode.expires_at) < new Date()) {
logger.warn(`Verification code expired for ${provider}:${providerId}`);
return { valid: false, message: 'Code has expired' };
}
// Отмечаем код как использованный
await db.getQuery()(
'UPDATE verification_codes SET used = true WHERE id = $1',
[verification.id]
);
await encryptedDb.saveData('verification_codes', {
used: true
}, {
id: verificationCode.id
});
logger.info(`Code verified successfully for ${provider}:${providerId}`);
logger.info(`Verification code verified successfully for ${provider}:${providerId}`);
return {
success: true,
userId: verification.user_id,
providerId: verification.provider_id,
valid: true,
userId: verificationCode.user_id,
message: 'Code verified successfully'
};
} catch (error) {
logger.error('Error verifying code:', {
@@ -141,15 +138,19 @@ class VerificationService {
// Очистка истекших кодов
async cleanupExpiredCodes() {
try {
const result = await db.getQuery()(
'DELETE FROM verification_codes WHERE expires_at <= NOW() RETURNING id'
);
logger.info(`Cleaned up ${result.rowCount} expired verification codes`);
const expiredCodes = await encryptedDb.getData('verification_codes', {
expires_at: { $lt: new Date() }
});
for (const code of expiredCodes) {
await encryptedDb.deleteData('verification_codes', { id: code.id });
}
logger.info(`Cleaned up ${expiredCodes.length} expired verification codes`);
} catch (error) {
logger.error('Error cleaning up expired codes:', error);
}
}
}
const verificationService = new VerificationService();
module.exports = verificationService;
module.exports = new VerificationService();

View File

@@ -10,21 +10,19 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const logger = require('../utils/logger');
// Получение связанного кошелька
async function getLinkedWallet(userId) {
logger.info(`[getLinkedWallet] Called with userId: ${userId} (Type: ${typeof userId})`);
try {
const result = await db.getQuery()(
`SELECT provider_id as address
FROM user_identities
WHERE user_id = $1 AND provider = 'wallet'`,
[userId]
);
logger.info(`[getLinkedWallet] DB query result for userId ${userId}:`, result.rows);
const address = result.rows[0]?.address;
const result = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: 'wallet'
}, 1);
logger.info(`[getLinkedWallet] DB query result for userId ${userId}:`, result);
const address = result[0]?.provider_id;
logger.info(`[getLinkedWallet] Returning address: ${address} for userId ${userId}`);
return address;
} catch (error) {