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

This commit is contained in:
2025-07-27 20:55:24 +03:00
parent 6dbaf91121
commit 34be65743b
14 changed files with 1124 additions and 126 deletions

View File

@@ -163,8 +163,8 @@ router.post('/verify', async (req, res) => {
}
// Создаем SIWE сообщение для проверки подписи
const domain = 'localhost:5173'; // Используем тот же домен, что и на frontend
const origin = req.get('origin') || 'http://localhost:5173';
const domain = new URL(origin).host; // Извлекаем домен из origin
const { SiweMessage } = require('siwe');
const message = new SiweMessage({
@@ -184,6 +184,9 @@ router.post('/verify', async (req, res) => {
logger.info(`[verify] SIWE message for verification: ${messageToSign}`);
logger.info(`[verify] Domain: ${domain}, Origin: ${origin}`);
logger.info(`[verify] Normalized address: ${normalizedAddress}`);
logger.info(`[verify] Request headers origin: ${req.get('origin')}`);
logger.info(`[verify] Request headers host: ${req.get('host')}`);
logger.info(`[verify] Request headers referer: ${req.get('referer')}`);
// Проверяем подпись
const isValid = await authService.verifySignature(messageToSign, signature, normalizedAddress);

View File

@@ -15,6 +15,7 @@ const router = express.Router();
const { requireAdmin } = require('../middleware/auth');
const logger = require('../utils/logger');
const { ethers } = require('ethers');
const db = require('../db');
const rpcProviderService = require('../services/rpcProviderService');
const authTokenService = require('../services/authTokenService');
const aiProviderSettingsService = require('../services/aiProviderSettingsService');

View File

@@ -17,6 +17,7 @@ const router = express.Router();
const db = require('../db');
const { requireAuth } = require('../middleware/auth');
const vectorSearchClient = require('../services/vectorSearchClient');
const { broadcastTableUpdate, broadcastTableRelationsUpdate } = require('../wsHub');
router.use((req, res, next) => {
console.log('Tables router received:', req.method, req.originalUrl);
@@ -32,7 +33,7 @@ router.get('/', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -58,7 +59,7 @@ router.post('/', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -85,7 +86,7 @@ router.get('/rag-sources', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -115,7 +116,7 @@ router.get('/:id', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -123,11 +124,26 @@ router.get('/:id', async (req, res, next) => {
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]);
// Выполняем все 4 запроса параллельно для ускорения
const [tableMetaResult, columnsResult, rowsResult, cellValuesResult] = await Promise.all([
// 1. Метаданные таблицы
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]),
// 2. Столбцы
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, options, placeholder FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId, encryptionKey]),
// 3. Строки
db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1 ORDER BY id', [tableId]),
// 4. Значения ячеек
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])
]);
const tableMeta = tableMetaResult.rows[0] || { name: '', description: '' };
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 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;
const columns = columnsResult.rows;
const rows = rowsResult.rows;
const cellValues = cellValuesResult.rows;
res.json({ name: tableMeta.name, description: tableMeta.description, columns, rows, cellValues });
} catch (err) {
next(err);
@@ -177,7 +193,7 @@ router.post('/:id/columns', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -194,6 +210,7 @@ router.post('/:id/columns', async (req, res, next) => {
[tableId, name, type, order || 0, placeholder, placeholder, encryptionKey]
);
res.json(result.rows[0]);
broadcastTableUpdate(tableId);
} catch (err) {
next(err);
}
@@ -214,7 +231,7 @@ router.post('/:id/rows', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -231,6 +248,7 @@ router.post('/:id/rows', async (req, res, next) => {
}
console.log('[DEBUG][addRow] res.json:', result.rows[0]);
res.json(result.rows[0]);
broadcastTableUpdate(tableId);
} catch (err) {
next(err);
}
@@ -247,7 +265,7 @@ router.get('/:id/rows', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -331,7 +349,7 @@ router.patch('/cell/:cellId', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -362,6 +380,7 @@ router.patch('/cell/:cellId', async (req, res, next) => {
}
}
res.json(result.rows[0]);
broadcastTableUpdate(tableId);
} catch (err) {
next(err);
}
@@ -377,7 +396,7 @@ router.post('/cell', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -391,10 +410,20 @@ router.post('/cell', async (req, res, next) => {
RETURNING *`,
[row_id, column_id, value, encryptionKey]
);
// Получаем table_id
// Получаем 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;
// Проверяем, является ли это таблицей "Теги клиентов" - ОТКЛЮЧАЕМ WebSocket
// const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [tableId, encryptionKey])).rows[0];
// if (tableName && tableName.name === 'Теги клиентов') {
// // Отправляем WebSocket уведомление об обновлении тегов
// const { broadcastTagsUpdate } = require('../wsHub');
// broadcastTagsUpdate();
// }
// Получаем всю строку для upsert
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) {
@@ -404,7 +433,12 @@ router.post('/cell', async (req, res, next) => {
await vectorSearchClient.upsert(tableId, upsertRows);
}
}
// Отправляем WebSocket уведомление об обновлении таблицы
const { broadcastTableUpdate } = require('../wsHub');
broadcastTableUpdate(tableId);
}
res.json(result.rows[0]);
} catch (err) {
next(err);
@@ -417,7 +451,29 @@ router.delete('/row/:rowId', async (req, res, next) => {
const rowId = req.params.rowId;
// Получаем table_id
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
// Проверяем, является ли это таблицей тегов перед удалением
let isTagsTable = false;
if (table) {
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = '/app/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 tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [table.table_id, encryptionKey])).rows[0];
isTagsTable = tableName && tableName.name === 'Теги клиентов';
}
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
if (table) {
const tableId = table.table_id;
// Получаем все строки для rebuild
@@ -427,7 +483,7 @@ router.delete('/row/:rowId', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -442,6 +498,17 @@ router.delete('/row/:rowId', async (req, res, next) => {
await vectorSearchClient.rebuild(tableId, rebuildRows);
}
}
// Отправляем WebSocket уведомление, если это была таблица тегов - ОТКЛЮЧАЕМ
// if (isTagsTable) {
// const { broadcastTagsUpdate } = require('../wsHub');
// broadcastTagsUpdate();
// }
// Отправляем WebSocket уведомление об обновлении таблицы
const { broadcastTableUpdate } = require('../wsHub');
broadcastTableUpdate(tableId);
res.json({ success: true });
} catch (err) {
next(err);
@@ -471,7 +538,7 @@ router.patch('/column/:columnId', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
@@ -523,6 +590,7 @@ router.patch('/column/:columnId', async (req, res, next) => {
return res.status(404).json({ error: 'Column not found' });
}
res.json(result.rows[0]);
broadcastTableUpdate(colInfo.table_id);
} catch (err) {
next(err);
}
@@ -588,21 +656,45 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
if (!req.session.isAdmin) {
return res.status(403).json({ error: 'Доступ только для администратора' });
}
// Получаем ключ шифрования
const fs = require('fs');
let encryptionKey = 'default-key';
try {
const keyPath = '/app/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 tableId = req.params.id;
const { questionCol, answerCol } = await getQuestionAnswerColumnIds(tableId);
if (!questionCol || !answerCol) {
return res.status(400).json({ error: 'Не найдены колонки с вопросами и ответами' });
}
const rows = (await db.getQuery()(
`SELECT r.id as row_id, c.value as text, c2.value as answer
`SELECT r.id as row_id,
decrypt_text(c.value_encrypted, $4) as text,
decrypt_text(c2.value_encrypted, $4) as answer
FROM user_rows r
LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = $2
LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = $3
WHERE r.table_id = $1`,
[tableId, questionCol, answerCol]
[tableId, questionCol, answerCol, 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 } }));
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) {
await vectorSearchClient.rebuild(tableId, rebuildRows);
res.json({ success: true, count: rebuildRows.length });
@@ -708,6 +800,10 @@ router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
[rowId, column_id, to_table_id, to_row_id]
);
}
// Отправляем WebSocket уведомление об обновлении связей
broadcastTableRelationsUpdate(tableId, rowId);
res.json({ success: true });
} catch (err) {
next(err);
@@ -724,7 +820,7 @@ router.get('/:id/placeholders', async (req, res, next) => {
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
const keyPath = '/app/ssl/keys/full_db_encryption.key';
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}

View File

@@ -524,6 +524,11 @@ router.patch('/:id/tags', async (req, res) => {
[userId, tagId]
);
}
// Отправляем WebSocket уведомление об обновлении тегов - ОТКЛЮЧАЕМ
// const { broadcastTagsUpdate } = require('../wsHub');
// broadcastTagsUpdate();
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });
@@ -553,6 +558,11 @@ router.delete('/:id/tags/:tagId', async (req, res) => {
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
[userId, tagId]
);
// Отправляем WebSocket уведомление об обновлении тегов - ОТКЛЮЧАЕМ
// const { broadcastTagsUpdate } = require('../wsHub');
// broadcastTagsUpdate();
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: e.message });

View File

@@ -16,6 +16,10 @@ let wss = null;
// Храним клиентов по userId для персонализированных уведомлений
const wsClients = new Map(); // userId -> Set of WebSocket connections
// Кэш для отслеживания изменений тегов
const tagsChangeCache = new Map();
const TAGS_CACHE_TTL = 5000; // 5 секунд
function initWSS(server) {
wss = new WebSocket.Server({ server, path: '/ws' });
@@ -172,6 +176,96 @@ function broadcastConversationUpdate(conversationId, targetUserId = null) {
}
}
function broadcastTableUpdate(tableId) {
console.log('📢 [WebSocket] Отправка обновления таблицы', tableId);
const payload = { type: 'table-updated', tableId };
for (const [userId, clients] of wsClients.entries()) {
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
}
}
function broadcastTableRelationsUpdate(tableId, rowId, targetUserId = null) {
console.log(`📢 [WebSocket] Отправка обновления связей таблицы`, {
tableId,
rowId,
targetUserId
});
const payload = {
type: 'table-relations-updated',
tableId,
rowId
};
if (targetUserId) {
// Отправляем конкретному пользователю
const userClients = wsClients.get(targetUserId.toString());
if (userClients) {
for (const ws of userClients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
}
} else {
// Отправляем всем
for (const [userId, clients] of wsClients.entries()) {
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
}
}
}
function broadcastTagsUpdate(targetUserId = null) {
const now = Date.now();
const cacheKey = targetUserId || 'global';
// Проверяем, не отправляли ли мы недавно уведомление
const lastUpdate = tagsChangeCache.get(cacheKey);
if (lastUpdate && (now - lastUpdate) < TAGS_CACHE_TTL) {
console.log(`🏷️ [WebSocket] Пропускаем отправку уведомления о тегах (слишком часто)`, { targetUserId });
return;
}
// Обновляем кэш
tagsChangeCache.set(cacheKey, now);
console.log(`🏷️ [WebSocket] Отправка обновления тегов`, { targetUserId });
const payload = {
type: 'tags-updated',
timestamp: now
};
if (targetUserId) {
// Отправляем конкретному пользователю
const userClients = wsClients.get(targetUserId.toString());
if (userClients) {
for (const ws of userClients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
}
} else {
// Отправляем всем
for (const [userId, clients] of wsClients.entries()) {
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
}
}
}
function getConnectedUsers() {
const users = [];
for (const [userId, clients] of wsClients.entries()) {
@@ -210,6 +304,9 @@ module.exports = {
broadcastMessagesUpdate,
broadcastChatMessage,
broadcastConversationUpdate,
broadcastTableUpdate,
broadcastTableRelationsUpdate,
broadcastTagsUpdate,
getConnectedUsers,
getStats
};