ваше сообщение коммита
This commit is contained in:
@@ -262,6 +262,37 @@ router.get('/conversations', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/conversations - создать беседу для пользователя
|
||||
router.post('/conversations', async (req, res) => {
|
||||
const { userId, title } = req.body;
|
||||
if (!userId) return res.status(400).json({ error: 'userId required' });
|
||||
|
||||
// Получаем ключ шифрования
|
||||
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 conversationTitle = title || `Чат с пользователем ${userId}`;
|
||||
const result = await db.getQuery()(
|
||||
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
|
||||
[userId, conversationTitle, encryptionKey]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'DB error', details: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Массовая рассылка сообщения во все каналы пользователя
|
||||
router.post('/broadcast', async (req, res) => {
|
||||
const { user_id, content } = req.body;
|
||||
|
||||
@@ -14,6 +14,10 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const axios = require('axios');
|
||||
const db = require('../db');
|
||||
const aiAssistant = require('../services/ai-assistant');
|
||||
const aiCache = require('../services/ai-cache');
|
||||
const aiQueue = require('../services/ai-queue');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const results = {};
|
||||
@@ -50,4 +54,71 @@ router.get('/', async (req, res) => {
|
||||
res.json({ status: 'ok', services: results, timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// GET /api/monitoring/ai-stats - статистика AI
|
||||
router.get('/ai-stats', async (req, res) => {
|
||||
try {
|
||||
const aiHealth = await aiAssistant.checkHealth();
|
||||
const cacheStats = aiCache.getStats();
|
||||
const queueStats = aiQueue.getStats();
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
ai: {
|
||||
health: aiHealth,
|
||||
model: process.env.OLLAMA_MODEL || 'qwen2.5:7b',
|
||||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434'
|
||||
},
|
||||
cache: {
|
||||
...cacheStats,
|
||||
hitRate: `${(cacheStats.hitRate * 100).toFixed(1)}%`
|
||||
},
|
||||
queue: {
|
||||
...queueStats,
|
||||
avgResponseTime: `${queueStats.avgResponseTime.toFixed(0)}ms`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting AI stats:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/monitoring/ai-cache/clear - очистка кэша
|
||||
router.post('/ai-cache/clear', async (req, res) => {
|
||||
try {
|
||||
aiCache.clear();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: 'AI cache cleared successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error clearing AI cache:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/monitoring/ai-queue/clear - очистка очереди
|
||||
router.post('/ai-queue/clear', async (req, res) => {
|
||||
try {
|
||||
aiQueue.clear();
|
||||
res.json({
|
||||
status: 'ok',
|
||||
message: 'AI queue cleared successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error clearing AI queue:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -338,53 +338,7 @@ router.get('/:id/rows', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Изменить значение ячейки (доступно всем)
|
||||
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 = '/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 result = await db.getQuery()(
|
||||
'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];
|
||||
if (row) {
|
||||
const rowId = row.row_id;
|
||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
|
||||
if (table) {
|
||||
const tableId = table.table_id;
|
||||
// Получаем всю строку для 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', [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);
|
||||
if (upsertRows.length > 0) {
|
||||
await vectorSearchClient.upsert(tableId, upsertRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
broadcastTableUpdate(tableId);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Создать/обновить значение ячейки (upsert) (доступно всем)
|
||||
router.post('/cell', async (req, res, next) => {
|
||||
@@ -416,13 +370,15 @@ router.post('/cell', async (req, res, next) => {
|
||||
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();
|
||||
// }
|
||||
// Проверяем, является ли это таблицей "Теги клиентов"
|
||||
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [tableId, encryptionKey])).rows[0];
|
||||
console.log('🔄 [Tables] Проверяем таблицу:', { tableId, tableName: tableName?.name });
|
||||
if (tableName && tableName.name === 'Теги клиентов') {
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
console.log('🔄 [Tables] Обновление ячейки в таблице тегов, отправляем уведомление');
|
||||
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];
|
||||
@@ -499,11 +455,12 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление, если это была таблица тегов - ОТКЛЮЧАЕМ
|
||||
// if (isTagsTable) {
|
||||
// const { broadcastTagsUpdate } = require('../wsHub');
|
||||
// broadcastTagsUpdate();
|
||||
// }
|
||||
// Отправляем WebSocket уведомление, если это была таблица тегов
|
||||
if (isTagsTable) {
|
||||
console.log('🔄 [Tables] Обновление строки в таблице тегов, отправляем уведомление');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении таблицы
|
||||
const { broadcastTableUpdate } = require('../wsHub');
|
||||
@@ -790,6 +747,31 @@ router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
|
||||
const { tableId, rowId } = req.params;
|
||||
const { column_id, to_table_id, to_row_ids } = req.body; // to_row_ids: массив id
|
||||
if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
|
||||
|
||||
// Получаем ключ шифрования
|
||||
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 relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
|
||||
console.log('🔄 [Tables] Multirelations: проверяем связанную таблицу:', { to_table_id, tableName: relatedTableName?.name });
|
||||
|
||||
if (relatedTableName && relatedTableName.name === 'Теги клиентов') {
|
||||
console.log('🔄 [Tables] Multirelations: обновление тегов, отправляем уведомление');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
}
|
||||
|
||||
// Удаляем старые связи для этой строки/столбца
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
||||
// Добавляем новые связи
|
||||
|
||||
@@ -84,7 +84,7 @@ router.get('/', requireAuth, 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();
|
||||
}
|
||||
@@ -120,13 +120,16 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
// Фильтр по поиску
|
||||
if (search) {
|
||||
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(decrypt_text(ui.provider_id_encrypted, $${idx + 1})) LIKE $${idx})
|
||||
LOWER(decrypt_text(u.first_name_encrypted, $${idx++})) LIKE $${idx++} OR
|
||||
LOWER(decrypt_text(u.last_name_encrypted, $${idx++})) LIKE $${idx++} OR
|
||||
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(decrypt_text(ui.provider_id_encrypted, $${idx++})) LIKE $${idx++})
|
||||
)`);
|
||||
params.push(encryptionKey); // Для first_name_encrypted
|
||||
params.push(`%${search.toLowerCase()}%`);
|
||||
params.push(encryptionKey); // Для last_name_encrypted
|
||||
params.push(`%${search.toLowerCase()}%`);
|
||||
params.push(encryptionKey); // Для provider_id_encrypted
|
||||
params.push(`%${search.toLowerCase()}%`);
|
||||
params.push(encryptionKey);
|
||||
idx += 2;
|
||||
}
|
||||
|
||||
// Фильтр по блокировке
|
||||
@@ -138,13 +141,22 @@ 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 u.id,
|
||||
CASE
|
||||
WHEN u.first_name_encrypted IS NULL OR u.first_name_encrypted = '' THEN NULL
|
||||
ELSE decrypt_text(u.first_name_encrypted, $${idx++})
|
||||
END as first_name,
|
||||
CASE
|
||||
WHEN u.last_name_encrypted IS NULL OR u.last_name_encrypted = '' THEN NULL
|
||||
ELSE decrypt_text(u.last_name_encrypted, $${idx++})
|
||||
END as last_name,
|
||||
u.created_at, u.preferred_language, u.is_blocked,
|
||||
(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);
|
||||
params.push(encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey);
|
||||
|
||||
// Фильтрация по тегам
|
||||
if (tagIds) {
|
||||
@@ -298,13 +310,55 @@ router.patch('/:id/unblock', requireAuth, async (req, res) => {
|
||||
router.patch('/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { first_name, last_name, preferred_language, is_blocked } = req.body;
|
||||
const { first_name, last_name, name, preferred_language, language, is_blocked } = req.body;
|
||||
const fields = [];
|
||||
const values = [];
|
||||
let idx = 1;
|
||||
if (first_name !== undefined) { fields.push(`first_name = $${idx++}`); values.push(first_name); }
|
||||
if (last_name !== undefined) { fields.push(`last_name = $${idx++}`); values.push(last_name); }
|
||||
if (preferred_language !== undefined) { fields.push(`preferred_language = $${idx++}`); values.push(JSON.stringify(preferred_language)); }
|
||||
|
||||
// Получаем ключ шифрования один раз
|
||||
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();
|
||||
console.log('Encryption key loaded:', encryptionKey.length, 'characters');
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Обработка поля name - разбиваем на first_name и last_name
|
||||
if (name !== undefined) {
|
||||
const nameParts = name.trim().split(' ');
|
||||
const firstName = nameParts[0] || '';
|
||||
const lastName = nameParts.slice(1).join(' ') || '';
|
||||
fields.push(`first_name_encrypted = encrypt_text($${idx++}, $${idx++})`);
|
||||
values.push(firstName);
|
||||
values.push(encryptionKey);
|
||||
fields.push(`last_name_encrypted = encrypt_text($${idx++}, $${idx++})`);
|
||||
values.push(lastName);
|
||||
values.push(encryptionKey);
|
||||
} else {
|
||||
if (first_name !== undefined) {
|
||||
fields.push(`first_name_encrypted = encrypt_text($${idx++}, $${idx++})`);
|
||||
values.push(first_name);
|
||||
values.push(encryptionKey);
|
||||
}
|
||||
if (last_name !== undefined) {
|
||||
fields.push(`last_name_encrypted = encrypt_text($${idx++}, $${idx++})`);
|
||||
values.push(last_name);
|
||||
values.push(encryptionKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка поля language (alias для preferred_language)
|
||||
const languageToUpdate = language !== undefined ? language : preferred_language;
|
||||
if (languageToUpdate !== undefined) {
|
||||
fields.push(`preferred_language = $${idx++}`);
|
||||
values.push(JSON.stringify(languageToUpdate));
|
||||
}
|
||||
if (is_blocked !== undefined) {
|
||||
fields.push(`is_blocked = $${idx++}`);
|
||||
values.push(is_blocked);
|
||||
@@ -318,6 +372,7 @@ router.patch('/:id', requireAuth, async (req, res) => {
|
||||
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = $${idx}`;
|
||||
values.push(userId);
|
||||
await db.query(sql, values);
|
||||
broadcastContactsUpdate();
|
||||
res.json({ success: true, message: 'Пользователь обновлен' });
|
||||
} catch (e) {
|
||||
logger.error('Ошибка обновления пользователя:', e);
|
||||
@@ -326,7 +381,7 @@ router.patch('/:id', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
// DELETE /api/users/:id — удалить контакт и все связанные данные
|
||||
router.delete('/:id', async (req, res) => {
|
||||
router.delete('/:id', requireAuth, async (req, res) => {
|
||||
console.log('[users.js] DELETE HANDLER', req.params.id);
|
||||
const userId = Number(req.params.id);
|
||||
console.log('[ROUTER] Перед вызовом deleteUserById для userId:', userId);
|
||||
@@ -354,7 +409,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();
|
||||
}
|
||||
@@ -401,7 +456,7 @@ router.post('/', async (req, res) => {
|
||||
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();
|
||||
}
|
||||
@@ -430,7 +485,7 @@ router.post('/import', requireAuth, async (req, res) => {
|
||||
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();
|
||||
}
|
||||
@@ -525,9 +580,9 @@ router.patch('/:id/tags', async (req, res) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов - ОТКЛЮЧАЕМ
|
||||
// const { broadcastTagsUpdate } = require('../wsHub');
|
||||
// broadcastTagsUpdate();
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
@@ -559,9 +614,9 @@ router.delete('/:id/tags/:tagId', async (req, res) => {
|
||||
[userId, tagId]
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов - ОТКЛЮЧАЕМ
|
||||
// const { broadcastTagsUpdate } = require('../wsHub');
|
||||
// broadcastTagsUpdate();
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user