ваше сообщение коммита
This commit is contained in:
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
377
backend/services/ai-queue.js
Normal file
377
backend/services/ai-queue.js
Normal 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;
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем и экспортируем единственный экземпляр
|
||||
|
||||
@@ -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 };
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
313
backend/services/dleV2Service.js
Normal file
313
backend/services/dleV2Service.js
Normal 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();
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
440
backend/services/encryptedDatabaseService.js
Normal file
440
backend/services/encryptedDatabaseService.js
Normal 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();
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user