ваше сообщение коммита
This commit is contained in:
188
backend/routes/ai-queue.js
Normal file
188
backend/routes/ai-queue.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
102
backend/routes/countries.js
Normal 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
173
backend/routes/dleV2.js
Normal 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;
|
||||
@@ -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
78
backend/routes/kpp.js
Normal 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;
|
||||
@@ -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
173
backend/routes/ollama.js
Normal 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;
|
||||
184
backend/routes/russian-classifiers.js
Normal file
184
backend/routes/russian-classifiers.js
Normal 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;
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user