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

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

188
backend/routes/ai-queue.js Normal file
View File

@@ -0,0 +1,188 @@
/**
* 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 express = require('express');
const router = express.Router();
const aiQueueService = require('../services/ai-queue');
const { requireAuth } = require('../middleware/auth');
const logger = require('../utils/logger');
// Получение статистики очереди
router.get('/stats', requireAuth, async (req, res) => {
try {
const stats = aiQueueService.getStats();
res.json({
success: true,
data: stats
});
} catch (error) {
logger.error('[AIQueue] Error getting stats:', error);
res.status(500).json({
success: false,
error: 'Failed to get queue statistics'
});
}
});
// Добавление задачи в очередь
router.post('/task', requireAuth, async (req, res) => {
try {
const { message, language, history, systemPrompt, rules, type = 'chat' } = req.body;
const userId = req.session.userId;
const userRole = req.session.isAdmin ? 'admin' : 'user';
if (!message) {
return res.status(400).json({
success: false,
error: 'Message is required'
});
}
const taskData = {
message,
language: language || 'auto',
history: history || null,
systemPrompt: systemPrompt || '',
rules: rules || null,
type,
userId,
userRole,
requestId: req.body.requestId || null
};
const result = await aiQueueService.addTask(taskData);
res.json({
success: true,
data: {
taskId: result.taskId,
status: 'queued',
estimatedWaitTime: aiQueueService.getStats().currentQueueSize * 30 // Примерное время ожидания
}
});
} catch (error) {
logger.error('[AIQueue] Error adding task:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to add task to queue'
});
}
});
// Получение статуса задачи
router.get('/task/:taskId', requireAuth, async (req, res) => {
try {
const { taskId } = req.params;
const stats = aiQueueService.getStats();
// Простая реализация - в реальном проекте нужно хранить статусы задач
res.json({
success: true,
data: {
taskId,
status: 'processing', // Упрощенная реализация
queuePosition: stats.currentQueueSize,
estimatedWaitTime: stats.currentQueueSize * 30
}
});
} catch (error) {
logger.error('[AIQueue] Error getting task status:', error);
res.status(500).json({
success: false,
error: 'Failed to get task status'
});
}
});
// Управление очередью (только для администраторов)
router.post('/control', requireAuth, async (req, res) => {
try {
const { action } = req.body;
if (!req.session.isAdmin) {
return res.status(403).json({
success: false,
error: 'Admin access required'
});
}
switch (action) {
case 'pause':
aiQueueService.pause();
res.json({
success: true,
message: 'Queue paused'
});
break;
case 'resume':
aiQueueService.resume();
res.json({
success: true,
message: 'Queue resumed'
});
break;
case 'clear':
aiQueueService.clear();
res.json({
success: true,
message: 'Queue cleared'
});
break;
default:
res.status(400).json({
success: false,
error: 'Invalid action. Use: pause, resume, or clear'
});
}
} catch (error) {
logger.error('[AIQueue] Error controlling queue:', error);
res.status(500).json({
success: false,
error: 'Failed to control queue'
});
}
});
// Получение информации о производительности
router.get('/performance', requireAuth, async (req, res) => {
try {
const stats = aiQueueService.getStats();
const performance = {
successRate: stats.totalProcessed > 0 ?
((stats.totalProcessed - stats.totalFailed) / stats.totalProcessed * 100).toFixed(2) : 0,
averageProcessingTime: Math.round(stats.averageProcessingTime),
totalProcessed: stats.totalProcessed,
totalFailed: stats.totalFailed,
currentQueueSize: stats.currentQueueSize,
runningTasks: stats.runningTasks,
lastProcessedAt: stats.lastProcessedAt
};
res.json({
success: true,
data: performance
});
} catch (error) {
logger.error('[AIQueue] Error getting performance:', error);
res.status(500).json({
success: false,
error: 'Failed to get performance data'
});
}
});
module.exports = router;

View File

@@ -14,6 +14,7 @@ const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const db = require('../db');
const encryptedDb = require('../services/encryptedDatabaseService');
const logger = require('../utils/logger');
const rateLimit = require('express-rate-limit');
const { requireAuth } = require('../middleware/auth');
@@ -44,24 +45,49 @@ router.get('/nonce', async (req, res) => {
// Генерируем случайный nonce
const nonce = crypto.randomBytes(16).toString('hex');
logger.info(`[nonce] Generated nonce: ${nonce}`);
// Проверяем, существует ли уже nonce для этого адреса
const existingNonce = await db.getQuery()('SELECT id FROM nonces WHERE identity_value = $1', [
address.toLowerCase(),
]);
// Используем правильный ключ шифрования
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();
logger.info(`[nonce] Using encryption key: ${encryptionKey.substring(0, 10)}...`);
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
// Проверяем, существует ли уже nonce для этого адреса
const existingNonces = await db.getQuery()(
'SELECT id FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
[address.toLowerCase(), encryptionKey]
);
if (existingNonce.rows.length > 0) {
// Обновляем существующий nonce
await db.getQuery()(
"UPDATE nonces SET nonce = $1, expires_at = NOW() + INTERVAL '15 minutes' WHERE identity_value = $2",
[nonce, address.toLowerCase()]
);
} else {
// Создаем новый nonce
await db.getQuery()(
"INSERT INTO nonces (identity_value, nonce, expires_at) VALUES ($1, $2, NOW() + INTERVAL '15 minutes')",
[address.toLowerCase(), nonce]
);
if (existingNonces.rows.length > 0) {
// Обновляем существующий nonce
logger.info(`[nonce] Updating existing nonce for address: ${address.toLowerCase()}`);
await db.getQuery()(
'UPDATE nonces SET nonce_encrypted = encrypt_text($1, $2), expires_at = $3 WHERE id = $4',
[nonce, encryptionKey, new Date(Date.now() + 15 * 60 * 1000), existingNonces.rows[0].id]
);
} else {
// Создаем новый nonce
logger.info(`[nonce] Creating new nonce for address: ${address.toLowerCase()}`);
await db.getQuery()(
'INSERT INTO nonces (identity_value_encrypted, nonce_encrypted, expires_at) VALUES (encrypt_text($1, $2), encrypt_text($3, $2), $4)',
[address.toLowerCase(), encryptionKey, nonce, new Date(Date.now() + 15 * 60 * 1000)]
);
}
} catch (dbError) {
console.error('Database error:', dbError);
// Fallback: просто возвращаем nonce без сохранения в БД
logger.warn(`Nonce ${nonce} generated for address ${address} but not saved to DB due to error`);
}
logger.info(`Nonce ${nonce} сохранен для адреса ${address}`);
@@ -76,34 +102,96 @@ router.get('/nonce', async (req, res) => {
// Верификация подписи и создание сессии
router.post('/verify', async (req, res) => {
try {
const { address, message, signature } = req.body;
const { address, signature, nonce, issuedAt } = req.body;
logger.info(`[verify] Verifying signature for address: ${address}`);
logger.info(`[verify] Request body:`, JSON.stringify(req.body, null, 2));
logger.info(`[verify] Request headers:`, JSON.stringify(req.headers, null, 2));
logger.info(`[verify] Raw request body:`, req.body);
logger.info(`[verify] Request body type:`, typeof req.body);
logger.info(`[verify] Request body keys:`, Object.keys(req.body || {}));
logger.info(`[verify] Nonce from request: ${nonce}`);
logger.info(`[verify] Address from request: ${address}`);
logger.info(`[verify] Signature from request: ${signature}`);
// Сохраняем гостевые ID до проверки
const guestId = req.session.guestId;
const previousGuestId = req.session.previousGuestId;
// Проверяем подпись
const isValid = await authService.verifySignature(message, signature, address);
if (!isValid) {
return res.status(401).json({ success: false, error: 'Invalid signature' });
// Нормализуем адрес для использования в запросах
const normalizedAddress = ethers.getAddress(address);
const normalizedAddressLower = normalizedAddress.toLowerCase();
// Читаем ключ шифрования
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 normalizedAddress = ethers.getAddress(address).toLowerCase();
// Проверяем nonce в базе данных
const nonceResult = await db.getQuery()(
'SELECT nonce_encrypted FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
[normalizedAddressLower, encryptionKey]
);
if (nonceResult.rows.length === 0) {
logger.error(`[verify] Nonce not found for address: ${normalizedAddressLower}`);
return res.status(401).json({ success: false, error: 'Nonce not found' });
}
// Проверяем nonce
const nonceResult = await db.getQuery()('SELECT nonce FROM nonces WHERE identity_value = $1', [
normalizedAddress,
]);
if (
nonceResult.rows.length === 0 ||
nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]
) {
// Расшифровываем nonce из базы данных
const storedNonce = await db.getQuery()(
'SELECT decrypt_text(nonce_encrypted, $1) as nonce FROM nonces WHERE identity_value_encrypted = encrypt_text($2, $1)',
[encryptionKey, normalizedAddressLower]
);
logger.info(`[verify] Stored nonce from DB: ${storedNonce.rows[0]?.nonce}`);
logger.info(`[verify] Nonce from request: ${nonce}`);
logger.info(`[verify] Nonce match: ${storedNonce.rows[0]?.nonce === nonce}`);
if (storedNonce.rows.length === 0 || storedNonce.rows[0].nonce !== nonce) {
logger.error(`[verify] Invalid nonce for address: ${normalizedAddressLower}. Expected: ${storedNonce.rows[0]?.nonce}, Got: ${nonce}`);
return res.status(401).json({ success: false, error: 'Invalid nonce' });
}
// Создаем SIWE сообщение для проверки подписи
const domain = 'localhost:5173'; // Используем тот же домен, что и на frontend
const origin = req.get('origin') || 'http://localhost:5173';
const { SiweMessage } = require('siwe');
const message = new SiweMessage({
domain,
address: normalizedAddress,
statement: 'Sign in with Ethereum to the app.',
uri: origin,
version: '1',
chainId: 1,
nonce: nonce,
issuedAt: issuedAt || new Date().toISOString(),
resources: [`${origin}/api/auth/verify`],
});
const messageToSign = message.prepareMessage();
logger.info(`[verify] SIWE message for verification: ${messageToSign}`);
logger.info(`[verify] Domain: ${domain}, Origin: ${origin}`);
logger.info(`[verify] Normalized address: ${normalizedAddress}`);
// Проверяем подпись
const isValid = await authService.verifySignature(messageToSign, signature, normalizedAddress);
if (!isValid) {
logger.error(`[verify] Invalid signature for address: ${normalizedAddress}`);
return res.status(401).json({ success: false, error: 'Invalid signature' });
}
let userId;
let isAdmin = false;

View File

@@ -14,14 +14,16 @@ const express = require('express');
const router = express.Router();
const multer = require('multer');
const aiAssistant = require('../services/ai-assistant');
const aiQueueService = require('../services/ai-queue'); // Добавляем импорт AI Queue сервиса
const db = require('../db');
const encryptedDb = require('../services/encryptedDatabaseService');
const logger = require('../utils/logger');
const { requireAuth } = require('../middleware/auth');
const crypto = require('crypto');
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
const { isUserBlocked } = require('../utils/userUtils');
const { broadcastChatMessage } = require('../wsHub');
const { broadcastChatMessage, broadcastConversationUpdate } = require('../wsHub');
// Настройка multer для обработки файлов в памяти
const storage = multer.memoryStorage();
@@ -32,10 +34,24 @@ async function processGuestMessages(userId, guestId) {
try {
logger.info(`Processing guest messages for user ${userId} with guest ID ${guestId}`);
// Получаем ключ шифрования
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 mappingCheck = await db.getQuery()(
'SELECT processed FROM guest_user_mapping WHERE guest_id = $1',
[guestId]
'SELECT processed FROM guest_user_mapping WHERE guest_id_encrypted = encrypt_text($1, $2)',
[guestId, encryptionKey]
);
// Если сообщения уже обработаны, пропускаем
@@ -47,8 +63,8 @@ async function processGuestMessages(userId, guestId) {
// Проверяем наличие mapping записи и создаем если нет
if (mappingCheck.rows.length === 0) {
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, 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, guestId, encryptionKey]
);
logger.info(`Created mapping for guest ID ${guestId} to user ${userId}`);
}
@@ -56,17 +72,17 @@ async function processGuestMessages(userId, guestId) {
// Получаем все гостевые сообщения со всеми новыми полями
const guestMessagesResult = await db.getQuery()(
`SELECT
id, guest_id, content, language, is_ai, created_at,
attachment_filename, attachment_mimetype, attachment_size, attachment_data
FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC`,
[guestId]
id, decrypt_text(guest_id_encrypted, $2) as guest_id, decrypt_text(content_encrypted, $2) as content, decrypt_text(language_encrypted, $2) as language, is_ai, created_at,
decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
FROM guest_messages WHERE guest_id_encrypted = encrypt_text($1, $2) ORDER BY created_at ASC`,
[guestId, encryptionKey]
);
if (guestMessagesResult.rows.length === 0) {
logger.info(`No guest messages found for guest ID ${guestId}`);
const checkResult = await db.getQuery()('SELECT 1 FROM guest_user_mapping WHERE guest_id = $1', [guestId]);
const checkResult = await db.getQuery()('SELECT 1 FROM guest_user_mapping WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]);
if (checkResult.rows.length > 0) {
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]);
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]);
logger.info(`Marked guest mapping as processed (no messages found) for guest ID ${guestId}`);
} else {
logger.warn(`Attempted to mark non-existent guest mapping as processed for guest ID ${guestId}`);
@@ -80,20 +96,20 @@ async function processGuestMessages(userId, guestId) {
// --- Новый порядок: ищем последний диалог пользователя ---
let conversation = null;
const lastConvResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId, encryptionKey]
);
if (lastConvResult.rows.length > 0) {
conversation = lastConvResult.rows[0];
} else {
// Если нет ни одного диалога, создаём новый
const firstMessage = guestMessages[0];
const title = firstMessage.content
? (firstMessage.content.length > 30 ? `${firstMessage.content.substring(0, 30)}...` : firstMessage.content)
const title = firstMessage.content && firstMessage.content.trim()
? (firstMessage.content.trim().length > 30 ? `${firstMessage.content.trim().substring(0, 30)}...` : firstMessage.content.trim())
: (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог');
const newConversationResult = await db.getQuery()(
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, title]
'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *',
[userId, title, encryptionKey]
);
conversation = newConversationResult.rows[0];
logger.info(`Created new conversation ${conversation.id} for guest messages`);
@@ -110,11 +126,11 @@ async function processGuestMessages(userId, guestId) {
// Сохраняем сообщение пользователя в таблицу messages, включая данные файла
const userMessageResult = await db.getQuery()(
`INSERT INTO messages
(conversation_id, content, sender_type, role, channel, created_at, user_id,
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
(conversation_id, content_encrypted, sender_type_encrypted, role_encrypted, channel_encrypted, created_at, user_id,
attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
VALUES
($1, $2, 'user', 'user', 'web', $3, $4,
$5, $6, $7, $8)
($1, encrypt_text($2, $9), encrypt_text('user', $9), encrypt_text('user', $9), encrypt_text('web', $9), $3, $4,
encrypt_text($5, $9), encrypt_text($6, $9), $7, $8)
RETURNING *`,
[
conversation.id,
@@ -124,7 +140,8 @@ async function processGuestMessages(userId, guestId) {
guestMessage.attachment_filename, // Метаданные и данные файла
guestMessage.attachment_mimetype,
guestMessage.attachment_size,
guestMessage.attachment_data // BYTEA
guestMessage.attachment_data, // BYTEA
encryptionKey
]
);
const savedUserMessage = userMessageResult.rows[0];
@@ -134,8 +151,8 @@ async function processGuestMessages(userId, guestId) {
if (guestMessage.content) {
// Проверяем, что на это сообщение ещё нет ответа ассистента
const aiReplyExists = await db.getQuery()(
`SELECT 1 FROM messages WHERE conversation_id = $1 AND sender_type = 'assistant' AND created_at > $2 LIMIT 1`,
[conversation.id, guestMessage.created_at]
`SELECT 1 FROM messages WHERE conversation_id = $1 AND sender_type_encrypted = encrypt_text('assistant', $3) AND created_at > $2 LIMIT 1`,
[conversation.id, guestMessage.created_at, encryptionKey]
);
if (!aiReplyExists.rows.length) {
try {
@@ -147,8 +164,8 @@ async function processGuestMessages(userId, guestId) {
}
// Получаем историю сообщений до этого guestMessage (до created_at)
const historyResult = await db.getQuery()(
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT 10',
[conversation.id, guestMessage.created_at]
'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT 10',
[conversation.id, guestMessage.created_at, encryptionKey]
);
const history = historyResult.rows.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
@@ -168,9 +185,9 @@ async function processGuestMessages(userId, guestId) {
if (aiResponseContent) {
await db.getQuery()(
`INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel)
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')`,
[conversation.id, userId, aiResponseContent]
(conversation_id, user_id, content_encrypted, sender_type_encrypted, role_encrypted, channel_encrypted)
VALUES ($1, $2, encrypt_text($3, $4), encrypt_text('assistant', $4), encrypt_text('assistant', $4), encrypt_text('web', $4))`,
[conversation.id, userId, aiResponseContent, encryptionKey]
);
logger.info('AI response for guest message saved', { conversationId: conversation.id });
}
@@ -194,14 +211,14 @@ async function processGuestMessages(userId, guestId) {
);
// Помечаем гостевой ID как обработанный
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
guestId,
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [
guestId, encryptionKey
]);
logger.info(`Marked guest mapping as processed for guest ID ${guestId}`);
} else {
logger.warn(`No guest messages were successfully processed, skipping deletion for guest ID ${guestId}`);
// Если не было успешных, все равно пометим как обработанные, чтобы не пытаться снова
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]);
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]);
logger.info(`Marked guest mapping as processed (no successful messages) for guest ID ${guestId}`);
}
@@ -224,6 +241,20 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
logger.debug('Request Body:', req.body);
logger.debug('Request Files:', req.files); // Файлы будут здесь
// Получаем ключ шифрования
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);
}
try {
// Извлекаем данные из req.body (текстовые поля)
const { message, language, guestId: requestGuestId } = req.body;
@@ -250,12 +281,18 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
}
// Подготавливаем данные для вставки
const messageContent = message || ''; // Текст или ПУСТАЯ СТРОКА, если есть файл
const messageContent = message && message.trim() ? message.trim() : null; // Текст или NULL, если пустой
const attachmentFilename = file ? file.originalname : null;
const attachmentMimetype = file ? file.mimetype : null;
const attachmentSize = file ? file.size : null;
const attachmentData = file ? file.buffer : null; // Сам буфер файла
// Проверяем, что есть контент для сохранения
if (!messageContent && !attachmentData) {
logger.warn('Guest message attempt without content or file');
return res.status(400).json({ success: false, error: 'Требуется текст сообщения или файл.' });
}
logger.info('Saving guest message:', {
guestId,
message: messageContent,
@@ -267,9 +304,9 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
// Сохраняем сообщение пользователя с текстом или файлом
const result = await db.getQuery()(
`INSERT INTO guest_messages
(guest_id, content, language, is_ai,
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
VALUES ($1, $2, $3, false, $4, $5, $6, $7) RETURNING id`,
(guest_id_encrypted, content_encrypted, language_encrypted, is_ai,
attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
VALUES (encrypt_text($1, $8), ${messageContent ? 'encrypt_text($2, $8)' : 'NULL'}, encrypt_text($3, $8), false, ${attachmentFilename ? 'encrypt_text($4, $8)' : 'NULL'}, ${attachmentMimetype ? 'encrypt_text($5, $8)' : 'NULL'}, $6, $7) RETURNING id`,
[
guestId,
messageContent, // Текст сообщения или NULL
@@ -277,7 +314,8 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
attachmentFilename,
attachmentMimetype,
attachmentSize,
attachmentData // BYTEA данные файла или NULL
attachmentData, // BYTEA данные файла или NULL
encryptionKey
]
);
@@ -333,6 +371,20 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
logger.debug('Request Body:', req.body);
logger.debug('Request Files:', req.files);
// Получаем ключ шифрования
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 userId = req.session.userId;
const { message, language, conversationId: convIdFromRequest } = req.body;
const files = req.files;
@@ -359,14 +411,14 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
if (req.session.isAdmin) {
// Админ может писать в любой диалог
convResult = await db.getQuery()(
'SELECT * FROM conversations WHERE id = $1',
[conversationId]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE id = $1',
[conversationId, encryptionKey]
);
} else {
// Обычный пользователь — только в свой диалог
convResult = await db.getQuery()(
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $3) as title FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId, encryptionKey]
);
}
if (convResult.rows.length === 0) {
@@ -377,20 +429,20 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
} else {
// Ищем последний диалог пользователя
const lastConvResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId, encryptionKey]
);
if (lastConvResult.rows.length > 0) {
conversation = lastConvResult.rows[0];
conversationId = conversation.id;
} else {
// Создаем новый диалог, если нет ни одного
const title = message
? (message.length > 50 ? `${message.substring(0, 50)}...` : message)
const title = message && message.trim()
? (message.trim().length > 50 ? `${message.trim().substring(0, 50)}...` : message.trim())
: (file ? `Файл: ${file.originalname}` : 'Новый диалог');
const newConvResult = await db.getQuery()(
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, title]
'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *',
[userId, title, encryptionKey]
);
conversation = newConvResult.rows[0];
conversationId = conversation.id;
@@ -399,7 +451,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
}
// Подготавливаем данные для вставки сообщения пользователя
const messageContent = message || ''; // Текст или ПУСТАЯ СТРОКА, если есть файл
const messageContent = message && message.trim() ? message.trim() : null; // Текст или NULL, если пустой
const attachmentFilename = file ? file.originalname : null;
const attachmentMimetype = file ? file.mimetype : null;
const attachmentSize = file ? file.size : null;
@@ -415,26 +467,26 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
role = 'admin';
}
// Сохраняем сообщение
const userMessageResult = await db.getQuery()(
`INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel,
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
VALUES ($1, $2, $3, $4, $5, 'web', $6, $7, $8, $9)
RETURNING *`,
[
conversationId,
recipientId, // user_id контакта
messageContent,
senderType,
role,
attachmentFilename,
attachmentMimetype,
attachmentSize,
attachmentData
]
);
const userMessage = userMessageResult.rows[0];
// Сохраняем сообщение через encryptedDb
const userMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: recipientId, // user_id контакта
content: messageContent,
sender_type: senderType,
role: role,
channel: 'web',
attachment_filename: attachmentFilename,
attachment_mimetype: attachmentMimetype,
attachment_size: attachmentSize,
attachment_data: attachmentData
});
// Проверяем, что сообщение было сохранено
if (!userMessage) {
logger.warn('Message not saved - all content was empty');
return res.status(400).json({ error: 'Message content cannot be empty' });
}
logger.info('User message saved', { messageId: userMessage.id, conversationId });
if (await isUserBlocked(userId)) {
@@ -476,22 +528,24 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= threshold) {
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
// Прямой ответ из RAG
const aiMessageResult = await db.getQuery()(
`INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel)
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
RETURNING *`,
[conversationId, userId, ragResult.answer]
);
aiMessage = aiMessageResult.rows[0];
logger.info(`[RAG] Сохраняем AI сообщение с контентом: "${ragResult.answer}"`);
aiMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: userId,
content: ragResult.answer,
sender_type: 'assistant',
role: 'assistant',
channel: 'web'
});
logger.info(`[RAG] AI сообщение сохранено:`, aiMessage);
// Пушим новое сообщение через WebSocket
broadcastChatMessage(aiMessage);
} else if (ragResult) {
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
// Генерация через LLM с подстановкой значений из RAG
const historyResult = await db.getQuery()(
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
[conversationId, userMessage.id]
'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
[conversationId, userMessage.id, encryptionKey]
);
const history = historyResult.rows.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
@@ -509,14 +563,14 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
});
if (llmResponse) {
const aiMessageResult = await db.getQuery()(
`INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel)
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
RETURNING *`,
[conversationId, userId, llmResponse]
);
aiMessage = aiMessageResult.rows[0];
aiMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: userId,
content: llmResponse,
sender_type: 'assistant',
role: 'assistant',
channel: 'web'
});
// Пушим новое сообщение через WebSocket
broadcastChatMessage(aiMessage);
} else {
@@ -531,25 +585,53 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
}
}
// Fallback: если AI не смог ответить, создаем fallback сообщение
if (!aiMessage && messageContent && shouldGenerateAiReply) {
try {
logger.info('[Chat] Creating fallback AI response due to AI error');
aiMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: userId,
content: 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.',
sender_type: 'assistant',
role: 'assistant',
channel: 'web'
});
// Пушим новое сообщение через WebSocket
broadcastChatMessage(aiMessage);
} catch (fallbackError) {
logger.error('Error creating fallback AI response:', fallbackError);
}
}
// Форматируем ответ для фронтенда
const formatMessageForFrontend = (msg) => {
if (!msg) return null;
console.log(`🔍 [formatMessageForFrontend] Форматируем сообщение:`, {
id: msg.id,
sender_type: msg.sender_type,
role: msg.role,
content: msg.content,
// Добавляем все поля для диагностики
allFields: Object.keys(msg),
rawMsg: msg
});
const formatted = {
id: msg.id,
conversation_id: msg.conversation_id,
user_id: msg.user_id,
content: msg.content,
sender_type: msg.sender_type,
role: msg.role,
channel: msg.channel,
content: msg.content, // content уже расшифрован encryptedDb
sender_type: msg.sender_type, // sender_type уже расшифрован encryptedDb
role: msg.role, // role уже расшифрован encryptedDb
channel: msg.channel, // channel уже расшифрован encryptedDb
created_at: msg.created_at,
attachments: null // Инициализируем как null
};
// Добавляем информацию о файле, если она есть
if (msg.attachment_filename) {
formatted.attachments = [{
originalname: msg.attachment_filename,
mimetype: msg.attachment_mimetype,
originalname: msg.attachment_filename, // attachment_filename уже расшифрован encryptedDb
mimetype: msg.attachment_mimetype, // attachment_mimetype уже расшифрован encryptedDb
size: msg.attachment_size,
// НЕ передаем attachment_data обратно в ответе на POST
}];
@@ -563,18 +645,228 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
[conversationId]
);
res.json({
// Получаем расшифрованные данные для форматирования
const decryptedUserMessage = userMessage ? await encryptedDb.getData('messages', { id: userMessage.id }, 1) : null;
const decryptedAiMessage = aiMessage ? await encryptedDb.getData('messages', { id: aiMessage.id }, 1) : null;
const response = {
success: true,
conversationId: conversationId,
userMessage: formatMessageForFrontend(userMessage),
aiMessage: formatMessageForFrontend(aiMessage),
userMessage: formatMessageForFrontend(decryptedUserMessage ? decryptedUserMessage[0] : null),
aiMessage: formatMessageForFrontend(decryptedAiMessage ? decryptedAiMessage[0] : null),
};
console.log(`📤 [Chat] Отправляем ответ на фронтенд:`, {
userMessage: response.userMessage,
aiMessage: response.aiMessage
});
// Отправляем WebSocket уведомления
if (response.userMessage) {
broadcastChatMessage(response.userMessage, userId);
}
if (response.aiMessage) {
broadcastChatMessage(response.aiMessage, userId);
}
broadcastConversationUpdate(conversationId, userId);
res.json(response);
} catch (error) {
logger.error('Error processing authenticated message:', error);
res.status(500).json({ success: false, error: 'Ошибка обработки сообщения' });
}
});
// Новый маршрут для обработки сообщений через очередь
router.post('/message-queued', requireAuth, upload.array('attachments'), async (req, res) => {
logger.info('Received /message-queued request');
try {
const userId = req.session.userId;
const { message, language, conversationId: convIdFromRequest, type = 'chat' } = req.body;
const files = req.files;
const file = files && files.length > 0 ? files[0] : null;
// Валидация
if (!message && !file) {
return res.status(400).json({
success: false,
error: 'Требуется текст сообщения или файл.'
});
}
if (message && file) {
return res.status(400).json({
success: false,
error: 'Нельзя отправить текст и файл одновременно.'
});
}
// Получаем ключ шифрования
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);
}
let conversationId = convIdFromRequest;
let conversation = null;
// Найти или создать диалог
if (conversationId) {
let convResult;
if (req.session.isAdmin) {
convResult = await db.getQuery()(
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE id = $1',
[conversationId, encryptionKey]
);
} else {
convResult = await db.getQuery()(
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $3) as title FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId, encryptionKey]
);
}
if (convResult.rows.length === 0) {
return res.status(404).json({
success: false,
error: 'Диалог не найден или доступ запрещен'
});
}
conversation = convResult.rows[0];
} else {
// Ищем последний диалог пользователя
const lastConvResult = await db.getQuery()(
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId, encryptionKey]
);
if (lastConvResult.rows.length > 0) {
conversation = lastConvResult.rows[0];
conversationId = conversation.id;
} else {
// Создаем новый диалог
const title = message && message.trim()
? (message.trim().length > 50 ? `${message.trim().substring(0, 50)}...` : message.trim())
: (file ? `Файл: ${file.originalname}` : 'Новый диалог');
const newConvResult = await db.getQuery()(
'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *',
[userId, title, encryptionKey]
);
conversation = newConvResult.rows[0];
conversationId = conversation.id;
}
}
// Сохраняем сообщение пользователя
const messageContent = message && message.trim() ? message.trim() : null;
const attachmentFilename = file ? file.originalname : null;
const attachmentMimetype = file ? file.mimetype : null;
const attachmentSize = file ? file.size : null;
const attachmentData = file ? file.buffer : null;
const recipientId = conversation.user_id;
let senderType = 'user';
let role = 'user';
if (req.session.isAdmin) {
senderType = 'admin';
role = 'admin';
}
const userMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: recipientId,
content: messageContent,
sender_type: senderType,
role: role,
channel: 'web',
attachment_filename: attachmentFilename,
attachment_mimetype: attachmentMimetype,
attachment_size: attachmentSize,
attachment_data: attachmentData
});
// Проверяем, нужно ли генерировать AI ответ
if (await isUserBlocked(userId)) {
logger.info(`[Chat] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`);
return res.json({ success: true, message: userMessage });
}
let shouldGenerateAiReply = true;
if (senderType === 'admin' && userId !== recipientId) {
shouldGenerateAiReply = false;
}
if (messageContent && shouldGenerateAiReply) {
try {
// Получаем историю сообщений
const historyResult = await db.getQuery()(
'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
[conversationId, userMessage.id, encryptionKey]
);
const history = historyResult.rows.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
content: msg.content
}));
// Получаем настройки AI
const aiSettings = await aiAssistantSettingsService.getSettings();
let rules = null;
if (aiSettings && aiSettings.rules_id) {
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
}
// Добавляем задачу в очередь
const taskData = {
message: messageContent,
language: language || 'auto',
history: history,
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
rules: rules,
type: type,
userId: userId,
userRole: req.session.isAdmin ? 'admin' : 'user',
conversationId: conversationId,
userMessageId: userMessage.id
};
const queueResult = await aiQueueService.addTask(taskData);
res.json({
success: true,
message: userMessage,
queueInfo: {
taskId: queueResult.taskId,
status: 'queued',
estimatedWaitTime: aiQueueService.getStats().currentQueueSize * 30
}
});
} catch (error) {
logger.error('Error adding task to queue:', error);
res.status(500).json({
success: false,
error: 'Ошибка при добавлении задачи в очередь.'
});
}
} else {
res.json({ success: true, message: userMessage });
}
} catch (error) {
logger.error('Error processing queued message:', error);
res.status(500).json({
success: false,
error: 'Внутренняя ошибка сервера.'
});
}
});
// Добавьте этот маршрут для проверки доступных моделей
router.get('/models', async (req, res) => {
try {
@@ -601,6 +893,20 @@ router.get('/history', requireAuth, async (req, res) => {
// Опциональный ID диалога
const conversationId = req.query.conversation_id;
// Получаем ключ шифрования
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);
}
try {
// Если нужен только подсчет
if (countOnly) {
@@ -615,51 +921,24 @@ router.get('/history', requireAuth, async (req, res) => {
return res.json({ success: true, count: totalCount });
}
// Формируем основной запрос
let query = `
SELECT
id,
conversation_id,
user_id,
content,
sender_type,
role,
channel,
created_at,
attachment_filename,
attachment_mimetype,
attachment_size,
attachment_data -- Выбираем и данные файла
FROM messages
WHERE user_id = $1
`;
const params = [userId];
// Добавляем фильтр по диалогу, если нужно
// Загружаем сообщения через encryptedDb
const whereConditions = { user_id: userId };
if (conversationId) {
query += ' AND conversation_id = $2';
params.push(conversationId);
whereConditions.conversation_id = conversationId;
}
// Добавляем сортировку и пагинацию
query += ' ORDER BY created_at ASC LIMIT $' + (params.length + 1) + ' OFFSET $' + (params.length + 2);
params.push(limit);
params.push(offset);
logger.debug('Executing history query:', { query, params });
const result = await db.getQuery()(query, params);
const messages = await encryptedDb.getData('messages', whereConditions, limit, 'created_at ASC', offset);
// Обрабатываем результаты для фронтенда
const messages = result.rows.map(msg => {
const formattedMessages = messages.map(msg => {
const formatted = {
id: msg.id,
conversation_id: msg.conversation_id,
user_id: msg.user_id,
content: msg.content,
sender_type: msg.sender_type,
role: msg.role,
channel: msg.channel,
content: msg.content, // content уже расшифрован encryptedDb
sender_type: msg.sender_type, // sender_type уже расшифрован encryptedDb
role: msg.role, // role уже расшифрован encryptedDb
channel: msg.channel, // channel уже расшифрован encryptedDb
created_at: msg.created_at,
attachments: null // Инициализируем
};
@@ -667,17 +946,13 @@ router.get('/history', requireAuth, async (req, res) => {
// Если есть данные файла, добавляем их в attachments
if (msg.attachment_data) {
formatted.attachments = [{
originalname: msg.attachment_filename,
mimetype: msg.attachment_mimetype,
originalname: msg.attachment_filename, // attachment_filename уже расшифрован encryptedDb
mimetype: msg.attachment_mimetype, // attachment_mimetype уже расшифрован encryptedDb
size: msg.attachment_size,
// Кодируем Buffer в Base64 для передачи на фронтенд
data_base64: msg.attachment_data.toString('base64')
}];
}
// Не забываем удалить поле attachment_data из итогового объекта,
// так как оно уже обработано и не нужно в сыром виде на фронте
// (хотя map и так создает новый объект, это для ясности)
delete formatted.attachment_data;
return formatted;
});
@@ -692,11 +967,11 @@ router.get('/history', requireAuth, async (req, res) => {
const totalCountResult = await db.getQuery()(totalCountQuery, totalCountParams);
const totalMessages = parseInt(totalCountResult.rows[0].count, 10);
logger.info(`Returning message history for user ${userId}`, { count: messages.length, offset, limit, total: totalMessages });
logger.info(`Returning message history for user ${userId}`, { count: formattedMessages.length, offset, limit, total: totalMessages });
res.json({
success: true,
messages: messages,
messages: formattedMessages,
offset: offset,
limit: limit,
total: totalMessages
@@ -732,6 +1007,20 @@ router.post('/process-guest', requireAuth, async (req, res) => {
router.post('/ai-draft', requireAuth, async (req, res) => {
const userId = req.session.userId;
const { conversationId, messages, language } = req.body;
// Получаем ключ шифрования
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);
}
if (!conversationId || !Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({ success: false, error: 'conversationId и messages обязательны' });
}
@@ -746,8 +1035,8 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
const promptText = messages.map(m => m.content).join('\n\n');
// Получаем последние 10 сообщений из диалога для истории
const historyResult = await db.getQuery()(
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 ORDER BY created_at DESC LIMIT 10',
[conversationId]
'SELECT decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content FROM messages WHERE conversation_id = $1 ORDER BY created_at DESC LIMIT 10',
[conversationId, encryptionKey]
);
const history = historyResult.rows.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',

102
backend/routes/countries.js Normal file
View File

@@ -0,0 +1,102 @@
/**
* 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 express = require('express');
const path = require('path');
const fs = require('fs');
const router = express.Router();
/**
* @route GET /api/countries
* @desc Получить список всех стран
* @access Public
*/
router.get('/', async (req, res, next) => {
try {
// Путь к файлу с данными стран
const countriesFilePath = path.join(__dirname, '../db/data/countries.json');
// Проверяем существование файла
if (!fs.existsSync(countriesFilePath)) {
return res.status(404).json({
success: false,
message: 'Файл с данными стран не найден'
});
}
// Читаем файл
const countriesData = fs.readFileSync(countriesFilePath, 'utf8');
const countries = JSON.parse(countriesData);
// Возвращаем список стран
res.json({
success: true,
data: countries.countries || [],
count: countries.countries ? countries.countries.length : 0
});
} catch (error) {
console.error('Ошибка при получении списка стран:', error);
next(error);
}
});
/**
* @route GET /api/countries/:code
* @desc Получить информацию о стране по коду
* @access Public
*/
router.get('/:code', async (req, res, next) => {
try {
const { code } = req.params;
// Путь к файлу с данными стран
const countriesFilePath = path.join(__dirname, '../db/data/countries.json');
// Проверяем существование файла
if (!fs.existsSync(countriesFilePath)) {
return res.status(404).json({
success: false,
message: 'Файл с данными стран не найден'
});
}
// Читаем файл
const countriesData = fs.readFileSync(countriesFilePath, 'utf8');
const countries = JSON.parse(countriesData);
// Ищем страну по коду (поддерживаем поиск по code, code3 или numeric)
const country = countries.countries.find(c =>
c.code === code.toUpperCase() ||
c.code3 === code.toUpperCase() ||
c.numeric === code
);
if (!country) {
return res.status(404).json({
success: false,
message: `Страна с кодом ${code} не найдена`
});
}
res.json({
success: true,
data: country
});
} catch (error) {
console.error('Ошибка при получении информации о стране:', error);
next(error);
}
});
module.exports = router;

173
backend/routes/dleV2.js Normal file
View File

@@ -0,0 +1,173 @@
/**
* 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 express = require('express');
const router = express.Router();
const dleV2Service = require('../services/dleV2Service');
const logger = require('../utils/logger');
const auth = require('../middleware/auth');
const path = require('path');
const fs = require('fs');
/**
* @route POST /api/dle-v2
* @desc Создать новое DLE v2 (Digital Legal Entity)
* @access Private (только для авторизованных пользователей с ролью admin)
*/
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try {
const dleParams = req.body;
logger.info('Получен запрос на создание DLE v2:', dleParams);
// Если параметр partners не был передан явно, используем адрес авторизованного пользователя
if (!dleParams.partners || dleParams.partners.length === 0) {
// Проверяем, есть ли в сессии адрес кошелька пользователя
if (!req.user || !req.user.walletAddress) {
return res.status(400).json({
success: false,
message: 'Не указан адрес кошелька пользователя или партнеров для распределения токенов'
});
}
// Используем адрес авторизованного пользователя
dleParams.partners = [req.user.address || req.user.walletAddress];
// Если суммы не указаны, используем значение по умолчанию (100% токенов)
if (!dleParams.amounts || dleParams.amounts.length === 0) {
dleParams.amounts = ['1000000'];
}
}
// Создаем DLE v2
const result = await dleV2Service.createDLE(dleParams);
logger.info('DLE v2 успешно создано:', result);
res.json({
success: true,
message: 'DLE v2 успешно создано',
data: result.data
});
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при создании DLE v2'
});
}
});
/**
* @route GET /api/dle-v2
* @desc Получить список всех DLE v2
* @access Private (только для авторизованных пользователей)
*/
router.get('/', auth.requireAuth, async (req, res, next) => {
try {
const dles = dleV2Service.getAllDLEs();
res.json({
success: true,
data: dles
});
} catch (error) {
logger.error('Ошибка при получении списка DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при получении списка DLE v2'
});
}
});
/**
* @route GET /api/dle-v2/defaults
* @desc Получить настройки по умолчанию для DLE v2
* @access Private (только для авторизованных пользователей)
*/
router.get('/defaults', auth.requireAuth, async (req, res, next) => {
// Возвращаем настройки по умолчанию, которые будут использоваться
// при заполнении формы на фронтенде
res.json({
success: true,
data: {
votingDelay: 1, // 1 блок задержки перед началом голосования
votingPeriod: 45818, // ~1 неделя в блоках (при 13 секундах на блок)
proposalThreshold: '100000', // 100,000 токенов
quorumPercentage: 4, // 4% от общего количества токенов
minTimelockDelay: 2 // 2 дня
}
});
});
/**
* @route DELETE /api/dle-v2/:dleAddress
* @desc Удалить DLE v2 по адресу
* @access Private (только для авторизованных пользователей с ролью admin)
*/
router.delete('/:dleAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try {
const { dleAddress } = req.params;
logger.info(`Получен запрос на удаление DLE v2 с адресом: ${dleAddress}`);
// Проверяем существование DLE v2 в директории contracts-data/dles
const dlesDir = path.join(__dirname, '../contracts-data/dles');
const files = fs.readdirSync(dlesDir);
let fileToDelete = null;
// Находим файл, содержащий указанный адрес DLE
for (const file of files) {
if (file.includes('dle-v2-') && file.endsWith('.json')) {
const filePath = path.join(dlesDir, file);
if (fs.statSync(filePath).isFile()) {
try {
const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (dleData.dleAddress && dleData.dleAddress.toLowerCase() === dleAddress.toLowerCase()) {
fileToDelete = filePath;
break;
}
} catch (err) {
logger.error(`Ошибка при чтении файла ${file}:`, err);
}
}
}
}
if (!fileToDelete) {
return res.status(404).json({
success: false,
message: `DLE v2 с адресом ${dleAddress} не найдено`
});
}
// Удаляем файл
fs.unlinkSync(fileToDelete);
logger.info(`DLE v2 с адресом ${dleAddress} успешно удалено`);
res.json({
success: true,
message: `DLE v2 с адресом ${dleAddress} успешно удалено`
});
} catch (error) {
logger.error('Ошибка при удалении DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при удалении DLE v2'
});
}
});
module.exports = router;

View File

@@ -39,11 +39,25 @@ router.post('/link', requireAuth, async (req, res, next) => {
if (type === 'wallet') {
const normalizedWallet = value.toLowerCase();
// Получаем ключ шифрования
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 existingCheck = await db.getQuery()(
`SELECT user_id FROM user_identities
WHERE provider = 'wallet' AND provider_id = $1`,
[normalizedWallet]
WHERE provider_encrypted = encrypt_text('wallet', $2) AND provider_id_encrypted = encrypt_text($1, $2)`,
[normalizedWallet, encryptionKey]
);
if (existingCheck.rows.length > 0) {
@@ -138,8 +152,25 @@ router.delete('/:provider/:providerId', requireAuth, async (req, res, next) => {
// Получение email-настроек
router.get('/email-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1');
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);
}
try {
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) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.smtp_password; // не возвращаем пароль
@@ -152,6 +183,20 @@ router.get('/email-settings', requireAuth, async (req, res, next) => {
// Обновление email-настроек
router.put('/email-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
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);
}
try {
const { smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email } = req.body;
if (!smtp_host || !smtp_port || !smtp_user || !from_email) {
@@ -161,14 +206,14 @@ router.put('/email-settings', requireAuth, async (req, res, next) => {
if (rows.length) {
// Обновляем существующую запись
await db.getQuery()(
`UPDATE email_settings SET smtp_host=$1, smtp_port=$2, smtp_user=$3, smtp_password=COALESCE($4, smtp_password), imap_host=$5, imap_port=$6, from_email=$7, updated_at=NOW() WHERE id=$8`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, rows[0].id]
`UPDATE email_settings SET smtp_host_encrypted=encrypt_text($1, $9), smtp_port=$2, smtp_user_encrypted=encrypt_text($3, $9), smtp_password_encrypted=COALESCE(encrypt_text($4, $9), smtp_password_encrypted), imap_host_encrypted=encrypt_text($5, $9), imap_port=$6, from_email_encrypted=encrypt_text($7, $9), updated_at=NOW() WHERE id=$8`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, rows[0].id, encryptionKey]
);
} else {
// Вставляем новую
await db.getQuery()(
`INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email]
`INSERT INTO email_settings (smtp_host_encrypted, smtp_port, smtp_user_encrypted, smtp_password_encrypted, imap_host_encrypted, imap_port, from_email_encrypted) VALUES (encrypt_text($1, $8), $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), $6, encrypt_text($7, $8))`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, encryptionKey]
);
}
res.json({ success: true });
@@ -180,8 +225,25 @@ router.put('/email-settings', requireAuth, async (req, res, next) => {
// Получение telegram-настроек
router.get('/telegram-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const { rows } = await db.getQuery()('SELECT * FROM telegram_settings ORDER BY id LIMIT 1');
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);
}
try {
const { rows } = await db.getQuery()(
'SELECT id, created_at, updated_at, decrypt_text(bot_token_encrypted, $1) as bot_token, decrypt_text(bot_username_encrypted, $1) as bot_username FROM telegram_settings ORDER BY id LIMIT 1',
[encryptionKey]
);
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.bot_token; // не возвращаем токен
@@ -194,6 +256,20 @@ router.get('/telegram-settings', requireAuth, async (req, res, next) => {
// Обновление telegram-настроек
router.put('/telegram-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
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);
}
try {
const { bot_token, bot_username } = req.body;
if (!bot_token || !bot_username) {
@@ -203,14 +279,14 @@ router.put('/telegram-settings', requireAuth, async (req, res, next) => {
if (rows.length) {
// Обновляем существующую запись
await db.getQuery()(
`UPDATE telegram_settings SET bot_token=$1, bot_username=$2, updated_at=NOW() WHERE id=$3`,
[bot_token, bot_username, rows[0].id]
`UPDATE telegram_settings SET bot_token_encrypted=encrypt_text($1, $4), bot_username_encrypted=encrypt_text($2, $4), updated_at=NOW() WHERE id=$3`,
[bot_token, bot_username, rows[0].id, encryptionKey]
);
} else {
// Вставляем новую
await db.getQuery()(
`INSERT INTO telegram_settings (bot_token, bot_username) VALUES ($1,$2)` ,
[bot_token, bot_username]
`INSERT INTO telegram_settings (bot_token_encrypted, bot_username_encrypted) VALUES (encrypt_text($1, $3), encrypt_text($2, $3))` ,
[bot_token, bot_username, encryptionKey]
);
}
res.json({ success: true });
@@ -222,8 +298,25 @@ router.put('/telegram-settings', requireAuth, async (req, res, next) => {
// Получение db-настроек
router.get('/db-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const { rows } = await db.getQuery()('SELECT * FROM db_settings ORDER BY id LIMIT 1');
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);
}
try {
const { rows } = await db.getQuery()(
'SELECT id, db_port, created_at, updated_at, decrypt_text(db_host_encrypted, $1) as db_host, decrypt_text(db_name_encrypted, $1) as db_name, decrypt_text(db_user_encrypted, $1) as db_user, decrypt_text(db_password_encrypted, $1) as db_password FROM db_settings ORDER BY id LIMIT 1',
[encryptionKey]
);
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.db_password; // не возвращаем пароль

78
backend/routes/kpp.js Normal file
View File

@@ -0,0 +1,78 @@
/**
* 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 express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const logger = require('../utils/logger');
/**
* @swagger
* tags:
* name: KPP
* description: API для КПП кодов (Код причины постановки на учет)
*/
/**
* @swagger
* /api/kpp/codes:
* get:
* summary: Получить список КПП кодов
* tags: [KPP]
* responses:
* 200:
* description: Список КПП кодов
* content:
* application/json:
* schema:
* type: object
* properties:
* codes:
* type: array
* items:
* type: object
* properties:
* code:
* type: string
* example: "773001001"
* title:
* type: string
* example: "По месту нахождения организации"
* 500:
* description: Ошибка сервера
*/
router.get('/codes', (req, res) => {
try {
// Путь к файлу с КПП кодами
const kppFilePath = path.join(__dirname, '../db/data/kpp_codes.json');
// Читаем файл синхронно (для простоты, можно переделать на асинхронный)
const kppData = fs.readFileSync(kppFilePath, 'utf8');
const kppJson = JSON.parse(kppData);
// Возвращаем данные в том же формате, что ожидает frontend
res.json({
codes: kppJson.kpp_codes || []
});
logger.info(`[KPP] Returned ${kppJson.kpp_codes?.length || 0} KPP codes`);
} catch (error) {
logger.error('Error fetching KPP codes:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Не удалось загрузить КПП коды'
});
}
});
module.exports = router;

View File

@@ -22,29 +22,45 @@ const { isUserBlocked } = require('../utils/userUtils');
router.get('/', async (req, res) => {
const userId = req.query.userId;
const conversationId = req.query.conversationId;
// Получаем ключ шифрования
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);
}
try {
let result;
if (conversationId) {
result = await db.getQuery()(
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
FROM messages
WHERE conversation_id = $1
ORDER BY created_at ASC`,
[conversationId]
[conversationId, encryptionKey]
);
} else if (userId) {
result = await db.getQuery()(
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
FROM messages
WHERE user_id = $1
ORDER BY created_at ASC`,
[userId]
[userId, encryptionKey]
);
} else {
result = await db.getQuery()(
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data
FROM messages
ORDER BY created_at ASC`
ORDER BY created_at ASC`,
[encryptionKey]
);
}
res.json(result.rows);
@@ -55,7 +71,22 @@ router.get('/', async (req, res) => {
// POST /api/messages
router.post('/', async (req, res) => {
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata } = req.body;
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data } = req.body;
// Получаем ключ шифрования
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);
}
try {
// Проверка блокировки пользователя
if (await isUserBlocked(user_id)) {
@@ -64,8 +95,8 @@ router.post('/', async (req, res) => {
// Проверка наличия идентификатора для выбранного канала
if (channel === 'email') {
const emailIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'email']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'email', encryptionKey]
);
if (emailIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не указан email. Сообщение не отправлено.' });
@@ -73,8 +104,8 @@ router.post('/', async (req, res) => {
}
if (channel === 'telegram') {
const tgIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'telegram']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'telegram', encryptionKey]
);
if (tgIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не привязан Telegram. Сообщение не отправлено.' });
@@ -82,8 +113,8 @@ router.post('/', async (req, res) => {
}
if (channel === 'wallet' || channel === 'web3' || channel === 'web') {
const walletIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'wallet']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'wallet', encryptionKey]
);
if (walletIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' });
@@ -91,16 +122,16 @@ router.post('/', async (req, res) => {
}
// 1. Проверяем, есть ли беседа для user_id
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id, encryptionKey]
);
let conversation;
if (conversationResult.rows.length === 0) {
// 2. Если нет — создаём новую беседу
const title = `Чат с пользователем ${user_id}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[user_id, title]
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
[user_id, title, encryptionKey]
);
conversation = newConv.rows[0];
} else {
@@ -108,9 +139,9 @@ router.post('/', async (req, res) => {
}
// 3. Сохраняем сообщение с conversation_id
const result = 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) RETURNING *`,
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata]
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`,
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
);
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
if (channel === 'telegram' && direction === 'out') {
@@ -118,8 +149,8 @@ router.post('/', async (req, res) => {
console.log(`[messages.js] Попытка отправки сообщения в Telegram для user_id=${user_id}`);
// Получаем Telegram ID пользователя
const tgIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'telegram']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'telegram', encryptionKey]
);
console.log(`[messages.js] Результат поиска Telegram ID:`, tgIdentity.rows);
if (tgIdentity.rows.length > 0) {
@@ -144,8 +175,8 @@ router.post('/', async (req, res) => {
try {
// Получаем email пользователя
const emailIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'email']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'email', encryptionKey]
);
if (emailIdentity.rows.length > 0) {
const email = emailIdentity.rows[0].provider_id;
@@ -237,24 +268,39 @@ router.post('/broadcast', async (req, res) => {
if (!user_id || !content) {
return res.status(400).json({ error: 'user_id и content обязательны' });
}
// Получаем ключ шифрования
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);
}
try {
// Получаем все идентификаторы пользователя
const identitiesRes = await db.getQuery()(
'SELECT provider, provider_id FROM user_identities WHERE user_id = $1',
[user_id]
'SELECT decrypt_text(provider_encrypted, $2) as provider, decrypt_text(provider_id_encrypted, $2) as provider_id FROM user_identities WHERE user_id = $1',
[user_id, encryptionKey]
);
const identities = identitiesRes.rows;
// --- Найти или создать беседу (conversation) ---
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id, encryptionKey]
);
let conversation;
if (conversationResult.rows.length === 0) {
const title = `Чат с пользователем ${user_id}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[user_id, title]
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
[user_id, title, encryptionKey]
);
conversation = newConv.rows[0];
} else {
@@ -269,9 +315,9 @@ router.post('/broadcast', async (req, res) => {
await emailBot.sendEmail(email, 'Новое сообщение', content);
// Сохраняем в messages с 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)`,
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', JSON.stringify({ broadcast: true })]
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', encryptionKey]
);
results.push({ channel: 'email', status: 'sent' });
sent = true;
@@ -286,9 +332,9 @@ router.post('/broadcast', async (req, res) => {
const bot = await telegramBot.getBot();
await bot.telegram.sendMessage(telegram, content);
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)`,
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', JSON.stringify({ broadcast: true })]
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', encryptionKey]
);
results.push({ channel: 'telegram', status: 'sent' });
sent = true;
@@ -301,9 +347,9 @@ router.post('/broadcast', async (req, res) => {
if (wallet) {
// Здесь можно реализовать отправку через web3, если нужно
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)`,
[user_id, conversation.id, 'admin', content, 'wallet', 'user', 'out', JSON.stringify({ broadcast: true })]
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
[user_id, conversation.id, 'admin', content, 'wallet', 'user', 'out', encryptionKey]
);
results.push({ channel: 'wallet', status: 'saved' });
sent = true;

173
backend/routes/ollama.js Normal file
View File

@@ -0,0 +1,173 @@
/**
* 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 express = require('express');
const router = express.Router();
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
const logger = require('../utils/logger');
const { requireAuth } = require('../middleware/auth');
// Проверка статуса подключения к Ollama
router.get('/status', requireAuth, async (req, res) => {
try {
// Проверяем, что контейнер Ollama запущен
const { stdout } = await execAsync('docker ps --filter "name=dapp-ollama" --format "{{.Names}}"');
const isContainerRunning = stdout.trim() === 'dapp-ollama';
if (!isContainerRunning) {
return res.json({ connected: false, error: 'Ollama container not running' });
}
// Проверяем API Ollama
try {
const { stdout: apiResponse } = await execAsync('docker exec dapp-ollama ollama list');
return res.json({ connected: true, message: 'Ollama is running' });
} catch (apiError) {
return res.json({ connected: false, error: 'Ollama API not responding' });
}
} catch (error) {
logger.error('Error checking Ollama status:', error);
res.status(500).json({ connected: false, error: 'Failed to check Ollama status' });
}
});
// Получение списка установленных моделей
router.get('/models', requireAuth, async (req, res) => {
try {
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
const models = lines.map(line => {
const parts = line.trim().split(/\s+/);
if (parts.length >= 4) {
return {
name: parts[0],
id: parts[1],
size: parseInt(parts[2]) || 0,
modified: parts.slice(3).join(' ')
};
}
return null;
}).filter(model => model !== null);
res.json({ models });
} catch (error) {
logger.error('Error getting Ollama models:', error);
res.status(500).json({ error: 'Failed to get models' });
}
});
// Установка модели
router.post('/install', requireAuth, async (req, res) => {
const { model } = req.body;
if (!model) {
return res.status(400).json({ error: 'Model name is required' });
}
try {
logger.info(`Starting installation of model: ${model}`);
// Запускаем установку в фоне
const installProcess = exec(`docker exec dapp-ollama ollama pull ${model}`, (error, stdout, stderr) => {
if (error) {
logger.error(`Error installing model ${model}:`, error);
} else {
logger.info(`Successfully installed model: ${model}`);
}
});
// Возвращаем ответ сразу, не ждем завершения
res.json({
success: true,
message: `Installation of ${model} started`,
processId: installProcess.pid
});
} catch (error) {
logger.error('Error starting model installation:', error);
res.status(500).json({ error: 'Failed to start installation' });
}
});
// Удаление модели
router.delete('/models/:modelName', requireAuth, async (req, res) => {
const { modelName } = req.params;
if (!modelName) {
return res.status(400).json({ error: 'Model name is required' });
}
try {
logger.info(`Removing model: ${modelName}`);
const { stdout, stderr } = await execAsync(`docker exec dapp-ollama ollama rm ${modelName}`);
if (stderr && !stderr.includes('deleted')) {
throw new Error(stderr);
}
logger.info(`Successfully removed model: ${modelName}`);
res.json({ success: true, message: `Model ${modelName} removed successfully` });
} catch (error) {
logger.error(`Error removing model ${modelName}:`, error);
res.status(500).json({ error: `Failed to remove model: ${error.message}` });
}
});
// Получение информации о модели
router.get('/models/:modelName', requireAuth, async (req, res) => {
const { modelName } = req.params;
try {
const { stdout } = await execAsync(`docker exec dapp-ollama ollama show ${modelName}`);
res.json({ model: modelName, info: stdout });
} catch (error) {
logger.error(`Error getting model info for ${modelName}:`, error);
res.status(404).json({ error: 'Model not found' });
}
});
// Поиск моделей в реестре (если поддерживается)
router.get('/search', requireAuth, async (req, res) => {
const { query } = req.query;
if (!query) {
return res.status(400).json({ error: 'Search query is required' });
}
try {
// Пока просто возвращаем популярные модели
const popularModels = [
'qwen2.5:7b',
'llama2:7b',
'mistral:7b',
'codellama:7b',
'llama2:13b',
'qwen2.5:14b',
'gemma:7b',
'phi3:3.8b'
];
const filteredModels = popularModels.filter(model =>
model.toLowerCase().includes(query.toLowerCase())
);
res.json({ models: filteredModels });
} catch (error) {
logger.error('Error searching models:', error);
res.status(500).json({ error: 'Failed to search models' });
}
});
module.exports = router;

View File

@@ -0,0 +1,184 @@
/**
* 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 express = require('express');
const path = require('path');
const fs = require('fs');
const router = express.Router();
/**
* @route GET /api/russian-classifiers/oktmo
* @desc Получить список кодов ОКТМО (муниципальные образования)
* @access Public
*/
router.get('/oktmo', async (req, res, next) => {
try {
const filePath = path.join(__dirname, '../db/data/oktmo.json');
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'Файл с кодами ОКТМО не найден'
});
}
const data = fs.readFileSync(filePath, 'utf8');
const oktmoData = JSON.parse(data);
res.json({
success: true,
data: oktmoData.oktmo_codes || [],
count: oktmoData.oktmo_codes ? oktmoData.oktmo_codes.length : 0
});
} catch (error) {
console.error('Ошибка при получении кодов ОКТМО:', error);
next(error);
}
});
/**
* @route GET /api/russian-classifiers/okved
* @desc Получить список кодов ОКВЭД (виды экономической деятельности)
* @access Public
*/
router.get('/okved', async (req, res, next) => {
try {
const filePath = path.join(__dirname, '../db/data/okved.json');
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'Файл с кодами ОКВЭД не найден'
});
}
const data = fs.readFileSync(filePath, 'utf8');
const okvedData = JSON.parse(data);
// Для ОКВЭД можем добавить фильтрацию по запросу
const { search, level } = req.query;
let codes = okvedData.okved_codes || [];
// Фильтрация по поисковому запросу
if (search) {
const searchTerm = search.toLowerCase();
codes = codes.filter(code =>
code.code.toLowerCase().includes(searchTerm) ||
code.title.toLowerCase().includes(searchTerm)
);
}
// Фильтрация по уровню (количество точек в коде)
if (level) {
const targetLevel = parseInt(level);
codes = codes.filter(code => {
const codeLevel = (code.code.match(/\./g) || []).length + 1;
return codeLevel === targetLevel;
});
}
// Ограничиваем количество результатов для производительности
const limit = parseInt(req.query.limit) || 2000; // Увеличили лимит для полного списка
codes = codes.slice(0, limit);
res.json({
success: true,
data: codes,
count: codes.length,
total: okvedData.okved_codes ? okvedData.okved_codes.length : 0
});
} catch (error) {
console.error('Ошибка при получении кодов ОКВЭД:', error);
next(error);
}
});
/**
* @route GET /api/russian-classifiers/okved/:code
* @desc Получить информацию о коде ОКВЭД
* @access Public
*/
router.get('/okved/:code', async (req, res, next) => {
try {
const { code } = req.params;
const filePath = path.join(__dirname, '../db/data/okved.json');
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'Файл с кодами ОКВЭД не найден'
});
}
const data = fs.readFileSync(filePath, 'utf8');
const okvedData = JSON.parse(data);
const okvedCode = okvedData.okved_codes.find(c => c.code === code);
if (!okvedCode) {
return res.status(404).json({
success: false,
message: `Код ОКВЭД ${code} не найден`
});
}
res.json({
success: true,
data: okvedCode
});
} catch (error) {
console.error('Ошибка при получении информации о коде ОКВЭД:', error);
next(error);
}
});
/**
* @route GET /api/russian-classifiers/all
* @desc Получить все российские классификаторы одним запросом
* @access Public
*/
router.get('/all', async (req, res, next) => {
try {
const oktmoPath = path.join(__dirname, '../db/data/oktmo.json');
const okvedPath = path.join(__dirname, '../db/data/okved.json');
const result = {};
// ОКТМО
if (fs.existsSync(oktmoPath)) {
const oktmoData = JSON.parse(fs.readFileSync(oktmoPath, 'utf8'));
result.oktmo = oktmoData.oktmo_codes || [];
}
// ОКВЭД (полный список)
if (fs.existsSync(okvedPath)) {
const okvedData = JSON.parse(fs.readFileSync(okvedPath, 'utf8'));
// Отдаем ВСЕ коды ОКВЭД - пользователь хочет полный список
result.okved = okvedData.okved_codes || [];
}
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Ошибка при получении всех российских классификаторов:', error);
next(error);
}
});
module.exports = router;

View File

@@ -45,7 +45,25 @@ router.get('/rpc', async (req, res, next) => {
}
}
const rpcConfigs = 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);
}
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 rpcConfigs = rpcProvidersResult.rows;
if (isAdmin) {
// Для админов возвращаем полные данные
@@ -108,7 +126,25 @@ router.delete('/rpc/:networkId', requireAdmin, async (req, res, next) => {
// Получение токенов для аутентификации
router.get('/auth-tokens', async (req, res, next) => {
try {
const authTokens = await authTokenService.getAllAuthTokens();
// Получаем ключ шифрования
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 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 authTokens = tokensResult.rows;
// Возвращаем полные данные для всех пользователей (включая гостевых)
res.json({ success: true, data: authTokens });

View File

@@ -26,7 +26,21 @@ router.use((req, res, next) => {
// Получить список всех таблиц (доступно всем)
router.get('/', async (req, res, next) => {
try {
const result = await db.getQuery()('SELECT * FROM user_tables ORDER BY id');
// Получаем ключ шифрования
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 result = await db.getQuery()('SELECT id, created_at, updated_at, is_rag_source_id, decrypt_text(name_encrypted, $1) as name, decrypt_text(description_encrypted, $1) as description FROM user_tables ORDER BY id', [encryptionKey]);
res.json(result.rows);
} catch (err) {
next(err);
@@ -37,9 +51,24 @@ router.get('/', async (req, res, next) => {
router.post('/', async (req, res, next) => {
try {
const { name, description, isRagSourceId } = req.body;
// Получаем ключ шифрования
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 result = await db.getQuery()(
'INSERT INTO user_tables (name, description, is_rag_source_id) VALUES ($1, $2, $3) RETURNING *',
[name, description || null, isRagSourceId || 2]
'INSERT INTO user_tables (name_encrypted, description_encrypted, is_rag_source_id) VALUES (encrypt_text($1, $4), encrypt_text($2, $4), $3) RETURNING *',
[name, description || null, isRagSourceId || 2, encryptionKey]
);
res.json(result.rows[0]);
} catch (err) {
@@ -47,15 +76,58 @@ router.post('/', async (req, res, next) => {
}
});
// Получить данные из таблицы is_rag_source с расшифровкой
router.get('/rag-sources', async (req, res, next) => {
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);
}
const result = await db.getQuery()(
'SELECT id, decrypt_text(name_encrypted, $1) as name FROM is_rag_source ORDER BY id',
[encryptionKey]
);
res.json(result.rows);
} catch (err) {
console.error('[RAG Sources] Error:', err);
next(err);
}
});
// Получить структуру и данные таблицы (доступно всем)
router.get('/:id', async (req, res, next) => {
try {
const tableId = req.params.id;
const tableMetaResult = await db.getQuery()('SELECT name, description FROM user_tables WHERE id = $1', [tableId]);
// Получаем ключ шифрования
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 tableMetaResult = await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name, decrypt_text(description_encrypted, $2) as description FROM user_tables WHERE id = $1', [tableId, encryptionKey]);
const tableMeta = tableMetaResult.rows[0] || { name: '', description: '' };
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId])).rows;
const columns = (await db.getQuery()('SELECT id, table_id, "order", created_at, updated_at, decrypt_text(name_encrypted, $2) as name, decrypt_text(type_encrypted, $2) as type, decrypt_text(placeholder_encrypted, $2) as placeholder_encrypted, placeholder FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId, encryptionKey])).rows;
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1 ORDER BY id', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT id, row_id, column_id, created_at, updated_at, decrypt_text(value_encrypted, $2) as value FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId, encryptionKey])).rows;
res.json({ name: tableMeta.name, description: tableMeta.description, columns, rows, cellValues });
} catch (err) {
next(err);
@@ -98,13 +170,28 @@ router.post('/:id/columns', async (req, res, next) => {
if (purpose) {
finalOptions.purpose = purpose;
}
// Получаем ключ шифрования
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 existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE table_id = $1', [tableId])).rows;
const existingPlaceholders = existing.map(c => c.placeholder).filter(Boolean);
const placeholder = generatePlaceholder(name, existingPlaceholders);
const result = await db.getQuery()(
'INSERT INTO user_columns (table_id, name, type, options, "order", placeholder) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
[tableId, name, type, finalOptions ? JSON.stringify(finalOptions) : null, order || 0, placeholder]
'INSERT INTO user_columns (table_id, name_encrypted, type_encrypted, placeholder_encrypted, "order", placeholder) VALUES ($1, encrypt_text($2, $7), encrypt_text($3, $7), encrypt_text($6, $7), $4, $5) RETURNING *',
[tableId, name, type, order || 0, placeholder, placeholder, encryptionKey]
);
res.json(result.rows[0]);
} catch (err) {
@@ -121,8 +208,22 @@ router.post('/:id/rows', async (req, res, next) => {
[tableId]
);
console.log('[DEBUG][addRow] result.rows[0]:', result.rows[0]);
// Получаем ключ шифрования
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);
}
// Получаем все строки и значения для upsert
const rows = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId])).rows;
const rows = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId, encryptionKey])).rows;
const upsertRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
console.log('[DEBUG][upsertRows]', upsertRows);
if (upsertRows.length > 0) {
@@ -140,10 +241,24 @@ router.get('/:id/rows', async (req, res, next) => {
try {
const tableId = req.params.id;
const { product, tags, ...relationFilters } = req.query; // tags = "B2B,VIP", relation_{colId}=rowId
// Получаем ключ шифрования
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 columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
const columns = (await db.getQuery()('SELECT id, table_id, "order", created_at, updated_at, decrypt_text(name_encrypted, $2) as name, decrypt_text(type_encrypted, $2) as type, decrypt_text(placeholder_encrypted, $2) as placeholder_encrypted, placeholder FROM user_columns WHERE table_id = $1', [tableId, encryptionKey])).rows;
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT id, row_id, column_id, created_at, updated_at, decrypt_text(value_encrypted, $2) as value FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId, encryptionKey])).rows;
// Находим id нужных колонок
const productCol = columns.find(c => c.options && c.options.purpose === 'product');
@@ -210,9 +325,23 @@ router.patch('/cell/:cellId', async (req, res, next) => {
try {
const cellId = req.params.cellId;
const { value } = req.body;
// Получаем ключ шифрования
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 result = await db.getQuery()(
'UPDATE user_cell_values SET value = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
[value, cellId]
'UPDATE user_cell_values SET value_encrypted = encrypt_text($1, $3), updated_at = NOW() WHERE id = $2 RETURNING *',
[value, cellId, encryptionKey]
);
// Получаем row_id и table_id
const row = (await db.getQuery()('SELECT row_id FROM user_cell_values WHERE id = $1', [cellId])).rows[0];
@@ -222,7 +351,7 @@ router.patch('/cell/:cellId', async (req, res, next) => {
if (table) {
const tableId = table.table_id;
// Получаем всю строку для upsert
const rowData = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [rowId])).rows[0];
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [rowId, encryptionKey])).rows[0];
if (rowData) {
const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text);
console.log('[DEBUG][upsertRows]', upsertRows);
@@ -242,18 +371,32 @@ router.patch('/cell/:cellId', async (req, res, next) => {
router.post('/cell', async (req, res, next) => {
try {
const { row_id, column_id, value } = req.body;
// Получаем ключ шифрования
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 result = await db.getQuery()(
`INSERT INTO user_cell_values (row_id, column_id, value) VALUES ($1, $2, $3)
ON CONFLICT (row_id, column_id) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
`INSERT INTO user_cell_values (row_id, column_id, value_encrypted) VALUES ($1, $2, encrypt_text($3, $4))
ON CONFLICT (row_id, column_id) DO UPDATE SET value_encrypted = encrypt_text($3, $4), updated_at = NOW()
RETURNING *`,
[row_id, column_id, value]
[row_id, column_id, value, encryptionKey]
);
// Получаем table_id
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0];
if (table) {
const tableId = table.table_id;
// Получаем всю строку для upsert
const rowData = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id])).rows[0];
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id, encryptionKey])).rows[0];
if (rowData) {
const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text);
console.log('[DEBUG][upsertRows]', upsertRows);
@@ -278,7 +421,21 @@ router.delete('/row/:rowId', async (req, res, next) => {
if (table) {
const tableId = table.table_id;
// Получаем все строки для rebuild
const rows = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId])).rows;
// Получаем ключ шифрования
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 r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId, encryptionKey])).rows;
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
console.log('[DEBUG][rebuildRows]', rebuildRows);
if (rebuildRows.length > 0) {
@@ -308,7 +465,21 @@ router.patch('/column/:columnId', async (req, res, next) => {
const columnId = req.params.columnId;
const { name, type, options, order, placeholder } = req.body;
// Получаем table_id для проверки уникальности плейсхолдера
const colInfo = (await db.getQuery()('SELECT table_id, name FROM user_columns WHERE id = $1', [columnId])).rows[0];
// Получаем ключ шифрования
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 colInfo = (await db.getQuery()('SELECT table_id, decrypt_text(name_encrypted, $2) as name FROM user_columns WHERE id = $1', [columnId, encryptionKey])).rows[0];
if (!colInfo) return res.status(404).json({ error: 'Column not found' });
let newPlaceholder = placeholder;
if (name !== undefined && !placeholder) {
@@ -547,7 +718,21 @@ router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
router.get('/:id/placeholders', async (req, res, next) => {
try {
const tableId = req.params.id;
const columns = (await db.getQuery()('SELECT id, name, placeholder FROM user_columns WHERE table_id = $1', [tableId])).rows;
// Получаем ключ шифрования
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 columns = (await db.getQuery()('SELECT id, decrypt_text(name_encrypted, $2) as name, placeholder FROM user_columns WHERE table_id = $1', [tableId, encryptionKey])).rows;
res.json(columns.map(col => ({
id: col.id,
name: col.name,
@@ -561,15 +746,33 @@ router.get('/:id/placeholders', async (req, res, next) => {
// Получить все плейсхолдеры по всем пользовательским таблицам
router.get('/placeholders/all', async (req, res, next) => {
try {
const result = await db.getQuery()(`
SELECT c.id as column_id, c.name as column_name, c.placeholder, t.id as table_id, t.name as table_name
FROM user_columns c
JOIN user_tables t ON c.table_id = t.id
WHERE c.placeholder IS NOT NULL AND c.placeholder != ''
ORDER BY t.id, c.id
`);
res.json(result.rows);
const encryptedDb = require('../services/encryptedDatabaseService');
// Получаем все колонки с плейсхолдерами
const columns = await encryptedDb.getData('user_columns', {});
// Фильтруем только те, у которых есть плейсхолдеры
const columnsWithPlaceholders = columns.filter(col => col.placeholder && col.placeholder !== '');
// Получаем информацию о таблицах
const tables = await encryptedDb.getData('user_tables', {});
const tableMap = {};
tables.forEach(table => {
tableMap[table.id] = table.name;
});
// Формируем результат
const result = columnsWithPlaceholders.map(col => ({
column_id: col.id,
column_name: col.name,
placeholder: col.placeholder,
table_id: col.table_id,
table_name: tableMap[col.table_id] || `Таблица ${col.table_id}`
}));
res.json(result);
} catch (err) {
console.error('[Placeholders] Error:', err);
next(err);
}
});

View File

@@ -78,6 +78,20 @@ router.get('/', requireAuth, async (req, res, next) => {
} = req.query;
const adminId = req.user && req.user.id;
// Получаем ключ шифрования
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 where = [];
const params = [];
@@ -97,9 +111,10 @@ router.get('/', requireAuth, async (req, res, next) => {
if (contactType !== 'all') {
where.push(`EXISTS (
SELECT 1 FROM user_identities ui
WHERE ui.user_id = u.id AND ui.provider = $${idx++}
WHERE ui.user_id = u.id AND ui.provider_encrypted = encrypt_text($${idx++}, $${idx++})
)`);
params.push(contactType);
params.push(encryptionKey);
}
// Фильтр по поиску
@@ -107,10 +122,11 @@ router.get('/', requireAuth, async (req, res, next) => {
where.push(`(
LOWER(u.first_name) LIKE $${idx} OR
LOWER(u.last_name) LIKE $${idx} OR
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(ui.provider_id) LIKE $${idx})
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(decrypt_text(ui.provider_id_encrypted, $${idx + 1})) LIKE $${idx})
)`);
params.push(`%${search.toLowerCase()}%`);
idx++;
params.push(encryptionKey);
idx += 2;
}
// Фильтр по блокировке
@@ -123,11 +139,12 @@ router.get('/', requireAuth, async (req, res, next) => {
// --- Основной SQL ---
let sql = `
SELECT u.id, u.first_name, u.last_name, u.created_at, u.preferred_language, u.is_blocked,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'email' LIMIT 1) AS email,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'telegram' LIMIT 1) AS telegram,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'wallet' LIMIT 1) AS wallet
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('email', $${idx++}) LIMIT 1) AS email,
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('telegram', $${idx++}) LIMIT 1) AS telegram,
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('wallet', $${idx++}) LIMIT 1) AS wallet
FROM users u
`;
params.push(encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey);
// Фильтрация по тегам
if (tagIds) {
@@ -330,16 +347,31 @@ router.delete('/:id', async (req, res) => {
// Получить пользователя по id
router.get('/:id', async (req, res, next) => {
const userId = req.params.id;
// Получаем ключ шифрования
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);
}
try {
const query = db.getQuery();
// Получаем пользователя
const userResult = await query('SELECT id, first_name, last_name, created_at, preferred_language, is_blocked FROM users WHERE id = $1', [userId]);
const userResult = await query('SELECT id, decrypt_text(first_name_encrypted, $2) as first_name, decrypt_text(last_name_encrypted, $2) as last_name, created_at, preferred_language, is_blocked FROM users WHERE id = $1', [userId, encryptionKey]);
if (userResult.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
const user = userResult.rows[0];
// Получаем идентификаторы
const identitiesResult = await query('SELECT provider, provider_id FROM user_identities WHERE user_id = $1', [userId]);
const identitiesResult = await query('SELECT decrypt_text(provider_encrypted, $2) as provider, decrypt_text(provider_id_encrypted, $2) as provider_id FROM user_identities WHERE user_id = $1', [userId, encryptionKey]);
const identityMap = {};
for (const id of identitiesResult.rows) {
identityMap[id.provider] = id.provider_id;
@@ -362,11 +394,26 @@ router.get('/:id', async (req, res, next) => {
// POST /api/users
router.post('/', async (req, res) => {
const { first_name, last_name, preferred_language } = req.body;
// Получаем ключ шифрования
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);
}
try {
const result = await db.getQuery()(
`INSERT INTO users (first_name, last_name, preferred_language, created_at)
VALUES ($1, $2, $3, NOW()) RETURNING *`,
[first_name, last_name, JSON.stringify(preferred_language || [])]
`INSERT INTO users (first_name_encrypted, last_name_encrypted, preferred_language, created_at)
VALUES (encrypt_text($1, $4), encrypt_text($2, $4), $3, NOW()) RETURNING *`,
[first_name, last_name, JSON.stringify(preferred_language || []), encryptionKey]
);
broadcastContactsUpdate();
res.json({ success: true, user: result.rows[0] });
@@ -377,6 +424,20 @@ router.post('/', async (req, res) => {
// Массовый импорт контактов
router.post('/import', requireAuth, async (req, res) => {
// Получаем ключ шифрования
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);
}
try {
const contacts = req.body;
if (!Array.isArray(contacts)) {
@@ -397,15 +458,15 @@ router.post('/import', requireAuth, async (req, res) => {
let userId = null;
let foundUser = null;
if (c.email) {
const r = await dbq('SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2', ['email', c.email.toLowerCase()]);
const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['email', c.email.toLowerCase(), encryptionKey]);
if (r.rows.length) foundUser = r.rows[0].user_id;
}
if (!foundUser && c.telegram) {
const r = await dbq('SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2', ['telegram', c.telegram]);
const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['telegram', c.telegram, encryptionKey]);
if (r.rows.length) foundUser = r.rows[0].user_id;
}
if (!foundUser && c.wallet) {
const r = await dbq('SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2', ['wallet', c.wallet]);
const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['wallet', c.wallet, encryptionKey]);
if (r.rows.length) foundUser = r.rows[0].user_id;
}
if (foundUser) {
@@ -413,11 +474,11 @@ router.post('/import', requireAuth, async (req, res) => {
updated++;
// Обновляем имя, если нужно
if (first_name || last_name) {
await dbq('UPDATE users SET first_name = COALESCE($1, first_name), last_name = COALESCE($2, last_name) WHERE id = $3', [first_name, last_name, userId]);
await dbq('UPDATE users SET first_name_encrypted = COALESCE(encrypt_text($1, $4), first_name_encrypted), last_name_encrypted = COALESCE(encrypt_text($2, $4), last_name_encrypted) WHERE id = $3', [first_name, last_name, userId, encryptionKey]);
}
} else {
// Создаём нового пользователя
const ins = await dbq('INSERT INTO users (first_name, last_name, created_at) VALUES ($1, $2, NOW()) RETURNING id', [first_name, last_name]);
const ins = await dbq('INSERT INTO users (first_name_encrypted, last_name_encrypted, created_at) VALUES (encrypt_text($1, $3), encrypt_text($2, $3), NOW()) RETURNING id', [first_name, last_name, encryptionKey]);
userId = ins.rows[0].id;
added++;
}