feat: новая функция

This commit is contained in:
2025-10-09 16:48:20 +03:00
parent dd2c9988a5
commit 13fb51e447
60 changed files with 7694 additions and 1157 deletions

View File

@@ -981,4 +981,114 @@ router.get('/access-level/:address', async (req, res) => {
}
});
/**
* Подключение кошелька через токен связывания
* Используется для привязки Telegram/Email идентификаторов к кошельку
*/
router.post('/wallet-with-link', authLimiter, async (req, res) => {
try {
const { address, signature, message, token } = req.body;
if (!address || !signature || !message || !token) {
return res.status(400).json({
success: false,
error: 'Требуются: address, signature, message, token'
});
}
// 1. Проверяем подпись
let recoveredAddress;
try {
recoveredAddress = ethers.verifyMessage(message, signature);
} catch (err) {
logger.error('[Auth] Ошибка верификации подписи:', err);
return res.status(400).json({
success: false,
error: 'Неверная подпись'
});
}
if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
return res.status(400).json({
success: false,
error: 'Подпись не соответствует адресу'
});
}
// 2. Проверяем и используем токен
const identityLinkService = require('../services/IdentityLinkService');
const linkResult = await identityLinkService.useLinkToken(token, address);
if (!linkResult.success) {
return res.status(400).json({
success: false,
error: linkResult.error
});
}
const { userId, identifier, role } = linkResult;
// 3. Мигрируем историю гостя
const universalGuestService = require('../services/UniversalGuestService');
const migrationResult = await universalGuestService.migrateToUser(identifier, userId);
logger.info('[Auth] История мигрирована:', migrationResult);
// 4. Обновляем сессию
req.session.userId = userId;
req.session.address = address.toLowerCase();
req.session.authenticated = true;
req.session.authType = 'wallet';
req.session.isAdmin = (role === 'admin' || role === 'editor' || role === 'readonly');
await sessionService.saveSession(req.session, 'wallet-with-link');
logger.info(`[Auth] Кошелек подключен через токен: ${address} → user ${userId}`);
res.json({
success: true,
userId,
role,
conversationId: migrationResult.conversationId,
migratedMessages: migrationResult.migrated,
message: 'Кошелек успешно подключен, история перенесена'
});
} catch (error) {
logger.error('[Auth] Ошибка в wallet-with-link:', error);
res.status(500).json({
success: false,
error: 'Внутренняя ошибка сервера'
});
}
});
// ✨ НОВОЕ: Получение прав доступа пользователя
router.get('/permissions', requireAuth, async (req, res) => {
try {
const userId = req.session.userId;
// Получаем роль пользователя из БД
const users = await encryptedDb.getData('users', { id: userId }, 1);
const userRole = users && users.length > 0 ? users[0].role : 'user';
// Получаем настройки прав через adminLogicService
const adminLogicService = require('../services/adminLogicService');
const permissions = adminLogicService.getAdminSettings({ role: userRole });
res.json({
success: true,
userId: userId,
permissions: permissions
});
} catch (error) {
logger.error('[Auth] Ошибка получения permissions:', error);
res.status(500).json({
success: false,
error: 'Ошибка получения прав доступа'
});
}
});
module.exports = router;

View File

@@ -15,21 +15,44 @@ const router = express.Router();
const multer = require('multer');
const aiAssistant = require('../services/ai-assistant');
const db = require('../db');
const encryptedDb = require('../services/encryptedDatabaseService');
const logger = require('../utils/logger');
const { requireAuth, requireAdmin } = require('../middleware/auth');
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
const botManager = require('../services/botManager');
const universalMediaProcessor = require('../services/UniversalMediaProcessor');
// Настройка multer для обработки файлов в памяти
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
// Функция processGuestMessages перенесена в services/guestMessageService.js
// Функция processGuestMessages заменена на UniversalGuestService.migrateToUser()
// Обработчик для гостевых сообщений
// Обработчик для гостевых сообщений (НОВАЯ ВЕРСИЯ)
router.post('/guest-message', upload.array('attachments'), async (req, res) => {
try {
// Frontend отправляет FormData, поэтому читаем из req.body
const content = req.body.content || req.body.message;
const guestId = req.body.guestId;
const files = req.files || [];
logger.info('[Chat] Получен guest-message запрос:', {
content: content?.substring(0, 50),
guestId,
hasFiles: files.length > 0,
bodyKeys: Object.keys(req.body)
});
// Проверяем, что есть либо текст, либо файлы
if (!content && (!files || files.length === 0)) {
logger.warn('[Chat] Гостевое сообщение без content и файлов:', req.body);
return res.status(400).json({
success: false,
error: 'Текст сообщения или файлы обязательны'
});
}
// Проверяем готовность системы
if (!botManager.isReady()) {
return res.status(503).json({
@@ -38,18 +61,100 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
});
}
// Получаем WebBot
const webBot = botManager.getBot('web');
if (!webBot || !webBot.isInitialized) {
return res.status(503).json({
success: false,
error: 'Web Bot не инициализирован'
});
const universalGuestService = require('../services/UniversalGuestService');
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
// Создаем или используем существующий гостевой ID
const webGuestId = guestId || universalGuestService.generateWebGuestId();
const identifier = universalGuestService.createIdentifier('web', webGuestId);
// Обработка вложений через медиа-процессор
let contentData = null;
if (files && files.length > 0) {
const mediaFiles = [];
for (const file of files) {
try {
const processedFile = await universalMediaProcessor.processFile(
file.buffer,
file.originalname,
{
webUpload: true,
originalSize: file.size,
mimeType: file.mimetype
}
);
mediaFiles.push(processedFile);
} catch (fileError) {
logger.error('[Chat] Ошибка обработки файла:', fileError);
// Fallback: сохраняем как есть
mediaFiles.push({
type: 'document',
content: `[Файл: ${file.originalname}]`,
processed: false,
error: fileError.message,
file: {
filename: file.originalname,
mimetype: file.mimetype,
size: file.size,
data: file.buffer
}
});
}
}
// Создаем contentData только если есть обработанные файлы
if (mediaFiles.length > 0) {
contentData = {
text: content,
files: mediaFiles.map(file => ({
data: file.file?.data || file.file?.buffer,
filename: file.file?.originalName || file.file?.filename,
metadata: {
type: file.type,
processed: file.processed,
webUpload: true,
mimeType: file.file?.mimetype,
originalSize: file.file?.size,
size: file.file?.size
}
}))
};
}
}
// Обрабатываем сообщение через новую архитектуру
await webBot.handleMessage(req, res, async (messageData) => {
return await botManager.processMessage(messageData);
// Обратная совместимость - старый формат attachments
const attachments = (files || []).map(file => ({
filename: file.originalname,
mimetype: file.mimetype,
size: file.size,
data: file.buffer
}));
const messageData = {
identifier: identifier,
content: content,
channel: 'web',
attachments: attachments,
contentData: contentData
};
// Обработка через unified processor
const result = await unifiedMessageProcessor.processMessage(messageData);
logger.info('[Chat] Результат обработки:', {
success: result.success,
hasAiResponse: !!result.aiResponse,
aiResponseType: typeof result.aiResponse?.response
});
res.json({
success: true,
guestId: webGuestId,
aiResponse: result.aiResponse ? {
response: result.aiResponse.response
} : null
});
} catch (error) {
@@ -63,9 +168,27 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
// Старая логика удалена - используется guestService.js);
// Обработчик для сообщений аутентифицированных пользователей
// Обработчик для сообщений аутентифицированных пользователей (НОВАЯ ВЕРСИЯ)
router.post('/message', requireAuth, upload.array('attachments'), async (req, res) => {
try {
const { content, conversationId, recipientId } = req.body;
const userId = req.session.userId;
const files = req.files || [];
if (!content) {
return res.status(400).json({
success: false,
error: 'Текст сообщения обязателен'
});
}
if (!userId) {
return res.status(401).json({
success: false,
error: 'Пользователь не авторизован'
});
}
// Проверяем готовность системы
if (!botManager.isReady()) {
return res.status(503).json({
@@ -74,18 +197,83 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
});
}
// Получаем WebBot
const webBot = botManager.getBot('web');
if (!webBot || !webBot.isInitialized) {
return res.status(503).json({
const encryptedDb = require('../services/encryptedDatabaseService');
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
const identityService = require('../services/identity-service');
// Получаем информацию о пользователе
const users = await encryptedDb.getData('users', { id: userId }, 1);
// ✨ НОВОЕ: Валидация прав через adminLogicService
const adminLogicService = require('../services/adminLogicService');
const sessionUserId = req.session.userId;
const targetUserId = userId;
const isAdmin = req.session.isAdmin || false;
const canWrite = adminLogicService.canWriteToConversation({
isAdmin: isAdmin,
userId: sessionUserId,
conversationUserId: targetUserId
});
if (!canWrite) {
logger.warn(`[Chat] Пользователь ${sessionUserId} пытался писать в беседу ${targetUserId} без прав`);
return res.status(403).json({
success: false,
error: 'Нет прав для отправки сообщений в эту беседу'
});
}
if (!users || users.length === 0) {
return res.status(404).json({
success: false,
error: 'Web Bot не инициализирован'
error: 'Пользователь не найден'
});
}
// Обрабатываем сообщение через новую архитектуру
await webBot.handleMessage(req, res, async (messageData) => {
return await botManager.processMessage(messageData);
const user = users[0];
// Находим wallet идентификатор пользователя
const walletIdentity = await identityService.findIdentity(userId, 'wallet');
if (!walletIdentity) {
return res.status(403).json({
success: false,
error: 'Требуется подключение кошелька'
});
}
// Создаем identifier для пользователя
const identifier = `wallet:${walletIdentity.provider_id}`;
// Обработка вложений
const attachments = files.map(file => ({
filename: file.originalname,
mimetype: file.mimetype,
size: file.size,
data: file.buffer
}));
const messageData = {
identifier: identifier,
content: content,
channel: 'web',
attachments: attachments,
conversationId: conversationId || null,
recipientId: recipientId || null,
userId: userId
};
// Обработка через unified processor
const result = await unifiedMessageProcessor.processMessage(messageData);
res.json({
success: true,
userMessageId: result.userMessageId,
conversationId: result.conversationId,
aiResponse: result.aiResponse ? {
response: result.aiResponse.response
} : null,
noAiResponse: result.noAiResponse
});
} catch (error) {
@@ -223,15 +411,21 @@ router.post('/process-guest', requireAuth, async (req, res) => {
return res.status(400).json({ success: false, error: 'guestId is required' });
}
try {
const guestMessageService = require('../services/guestMessageService');
const result = await guestMessageService.processGuestMessages(userId, guestId);
if (result && result.conversationId) {
return res.json({ success: true, conversationId: result.conversationId });
const universalGuestService = require('../services/UniversalGuestService');
const identifier = `web:${guestId}`; // Старые гости всегда из web
const result = await universalGuestService.migrateToUser(identifier, userId);
if (result && result.success) {
return res.json({
success: true,
conversationId: result.conversationId,
migratedMessages: result.migratedCount
});
} else {
return res.json({ success: false, error: result.error || 'No conversation created' });
return res.json({ success: false, error: result.error || 'Migration failed' });
}
} catch (error) {
logger.error('Error in /process-guest:', error);
logger.error('Error in /migrate-guest-messages:', error);
return res.status(500).json({ success: false, error: 'Internal error' });
}
});

View File

@@ -199,4 +199,45 @@ router.put('/db-settings', requireAuth, async (req, res, next) => {
}
});
/**
* Проверка статуса токена связывания
* Используется на странице /connect-wallet для валидации токена
*/
router.get('/link-status/:token', async (req, res) => {
try {
const { token } = req.params;
if (!token) {
return res.status(400).json({
success: false,
error: 'Токен не указан'
});
}
const identityLinkService = require('../services/IdentityLinkService');
const tokenData = await identityLinkService.verifyLinkToken(token);
if (!tokenData) {
return res.json({
valid: false,
error: 'Токен недействителен или истек'
});
}
res.json({
valid: true,
provider: tokenData.source_provider,
expiresAt: tokenData.expires_at,
isUsed: tokenData.is_used
});
} catch (error) {
logger.error('[Identity] Ошибка проверки статуса токена:', error);
res.status(500).json({
success: false,
error: 'Внутренняя ошибка сервера'
});
}
});
module.exports = router;

View File

@@ -27,6 +27,49 @@ router.get('/', async (req, res) => {
const encryptionKey = encryptionUtils.getEncryptionKey();
try {
// Проверяем, это гостевой идентификатор (формат: channel:rawId)
if (userId && userId.includes(':')) {
const guestResult = await db.getQuery()(
`SELECT
id,
decrypt_text(identifier_encrypted, $2) as user_id,
channel,
decrypt_text(content_encrypted, $2) as content,
content_type,
attachments,
media_metadata,
is_ai,
created_at
FROM unified_guest_messages
WHERE decrypt_text(identifier_encrypted, $2) = $1
ORDER BY created_at ASC`,
[userId, encryptionKey]
);
// Преобразуем формат для совместимости с фронтендом
const messages = guestResult.rows.map(msg => ({
id: msg.id,
user_id: msg.user_id,
sender_type: msg.is_ai ? 'bot' : 'user',
content: msg.content,
channel: msg.channel,
role: 'guest',
direction: msg.is_ai ? 'incoming' : 'outgoing',
created_at: msg.created_at,
attachment_filename: null,
attachment_mimetype: null,
attachment_size: null,
attachment_data: null,
// Дополнительные поля для медиа
content_type: msg.content_type,
attachments: msg.attachments,
media_metadata: msg.media_metadata
}));
return res.json(messages);
}
// Стандартная логика для зарегистрированных пользователей
let result;
if (conversationId) {
result = await db.getQuery()(
@@ -279,6 +322,24 @@ router.post('/broadcast', async (req, res) => {
return res.status(400).json({ error: 'user_id и content обязательны' });
}
// ✨ Проверка прав через adminLogicService (только editor может делать рассылку!)
const encryptedDb = require('../services/encryptedDatabaseService');
const users = await encryptedDb.getData('users', { id: req.session.userId }, 1);
const userRole = users && users.length > 0 ? users[0].role : 'user';
const adminLogicService = require('../services/adminLogicService');
const canBroadcast = adminLogicService.canPerformAdminAction({
role: userRole, // Передаем точную роль ('editor', 'readonly', 'user')
action: 'broadcast_message'
});
if (!canBroadcast) {
logger.warn(`[Messages] Пользователь ${req.session.userId} (роль: ${userRole}) пытался сделать broadcast без прав`);
return res.status(403).json({
error: 'Только редакторы (editor) могут делать массовую рассылку'
});
}
// Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();

View File

@@ -19,8 +19,11 @@ const aiCache = require('../services/ai-cache');
const logger = require('../utils/logger');
const ollamaConfig = require('../services/ollamaConfig');
const TIMEOUTS = ollamaConfig.getTimeouts();
router.get('/', async (req, res) => {
const results = {};
// Backend
results.backend = { status: 'ok' };
@@ -28,7 +31,7 @@ router.get('/', async (req, res) => {
try {
const baseUrl = process.env.VECTOR_SEARCH_URL || 'http://vector-search:8001';
const healthUrl = baseUrl.replace(/\/$/, '') + '/health';
const vs = await axios.get(healthUrl, { timeout: 2000 });
const vs = await axios.get(healthUrl, { timeout: TIMEOUTS.vectorHealth });
results.vectorSearch = { status: 'ok', ...vs.data };
} catch (e) {
console.log('Vector Search error:', e.message, 'Status:', e.response?.status);
@@ -37,8 +40,7 @@ router.get('/', async (req, res) => {
// Ollama
try {
const ollamaConfig = require('../services/ollamaConfig');
const ollama = await axios.get(ollamaConfig.getApiUrl('tags'), { timeout: 2000 });
const ollama = await axios.get(ollamaConfig.getApiUrl('tags'), { timeout: TIMEOUTS.ollamaHealth });
results.ollama = { status: 'ok', models: ollama.data.models?.length || 0 };
} catch (e) {
results.ollama = { status: 'error', error: e.message };
@@ -52,6 +54,37 @@ router.get('/', async (req, res) => {
results.postgres = { status: 'error', error: e.message };
}
// ✨ НОВОЕ: AI Cache статистика
try {
const ragService = require('../services/ragService');
const cacheStats = ragService.getCacheStats();
results.aiCache = {
status: 'ok',
size: cacheStats.size,
maxSize: cacheStats.maxSize,
hitRate: `${(cacheStats.hitRate * 100).toFixed(2)}%`,
byType: cacheStats.byType
};
} catch (e) {
results.aiCache = { status: 'error', error: e.message };
}
// ✨ НОВОЕ: AI Queue статистика
try {
const ragService = require('../services/ragService');
const queueStats = ragService.getQueueStats();
results.aiQueue = {
status: 'ok',
currentSize: queueStats.currentQueueSize,
totalProcessed: queueStats.totalProcessed,
totalFailed: queueStats.totalFailed,
avgResponseTime: `${Math.round(queueStats.averageProcessingTime)}ms`,
uptime: `${Math.round(queueStats.uptime / 1000)}s`
};
} catch (e) {
results.aiQueue = { status: 'error', error: e.message };
}
res.json({ status: 'ok', services: results, timestamp: new Date().toISOString() });
});

View File

@@ -15,20 +15,23 @@ const router = express.Router();
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
const axios = require('axios');
const logger = require('../utils/logger');
const { requireAuth } = require('../middleware/auth');
const ollamaConfig = require('../services/ollamaConfig');
// Инициализируем один раз
const TIMEOUTS = ollamaConfig.getTimeouts();
// Проверка статуса подключения к Ollama
router.get('/status', requireAuth, async (req, res) => {
try {
const axios = require('axios');
const ollamaConfig = require('../services/ollamaConfig');
const ollamaUrl = ollamaConfig.getBaseUrl();
// Проверяем API Ollama через HTTP запрос
try {
const response = await axios.get(`${ollamaUrl}/api/tags`, {
timeout: 5000 // 5 секунд таймаут
timeout: TIMEOUTS.ollamaTags // Централизованный таймаут
});
const models = response.data.models || [];
@@ -54,12 +57,10 @@ router.get('/status', requireAuth, async (req, res) => {
// Получение списка установленных моделей
router.get('/models', requireAuth, async (req, res) => {
try {
const axios = require('axios');
const ollamaConfig = require('../services/ollamaConfig');
const ollamaUrl = ollamaConfig.getBaseUrl();
const response = await axios.get(`${ollamaUrl}/api/tags`, {
timeout: 5000
timeout: TIMEOUTS.ollamaTags
});
const models = (response.data.models || []).map(model => ({

View File

@@ -17,6 +17,7 @@ const logger = require('../utils/logger');
const { ethers } = require('ethers');
const db = require('../db');
const rpcProviderService = require('../services/rpcProviderService');
const encryptedDb = require('../services/encryptedDatabaseService');
// Функция для получения информации о сети по chain_id
function getNetworkInfo(chainId) {
@@ -494,10 +495,13 @@ router.delete('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) =>
// Получить текущие настройки Email (для страницы Email)
router.get('/email-settings', requireAdmin, async (req, res) => {
try {
const settings = await botsSettings.getEmailSettings();
logger.info('[Settings] Запрос getBotSettings(email)');
const settings = await botsSettings.getBotSettings('email');
logger.info('[Settings] getBotSettings(email) успешно:', settings);
res.json({ success: true, settings });
} catch (error) {
res.status(404).json({ success: false, error: error.message });
logger.error('[Settings] Ошибка getBotSettings(email):', error);
res.status(500).json({ success: false, error: error.message });
}
});
@@ -540,7 +544,7 @@ router.put('/email-settings', requireAdmin, async (req, res, next) => {
updated_at: new Date()
};
const result = await botsSettings.saveEmailSettings(settings);
const result = await botsSettings.saveBotSettings('email', settings);
res.json({ success: true, data: result });
} catch (error) {
logger.error('Ошибка при обновлении email настроек:', error);
@@ -599,20 +603,27 @@ router.post('/email-settings/test-smtp', requireAdmin, async (req, res, next) =>
// Получить список всех email (для ассистента)
router.get('/email-settings/list', requireAdmin, async (req, res) => {
try {
const emails = await botsSettings.getAllEmailSettings();
logger.info('[Settings] Запрос списка email');
const emails = await encryptedDb.getData('email_settings', {}, 1000, 'id ASC');
logger.info('[Settings] Получено email:', emails ? emails.length : 0);
res.json({ success: true, items: emails });
} catch (error) {
res.status(404).json({ success: false, error: error.message });
logger.error('[Settings] Ошибка получения списка email:', error);
logger.error('[Settings] Stack:', error.stack);
res.status(500).json({ success: false, error: error.message });
}
});
// Получить текущие настройки Telegram-бота (для страницы Telegram)
router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
try {
const settings = await botsSettings.getTelegramSettings();
logger.info('[Settings] Запрос getBotSettings(telegram)');
const settings = await botsSettings.getBotSettings('telegram');
logger.info('[Settings] getBotSettings успешно:', settings);
res.json({ success: true, settings });
} catch (error) {
res.status(404).json({ success: false, error: error.message });
logger.error('[Settings] Ошибка getBotSettings(telegram):', error);
res.status(500).json({ success: false, error: error.message });
}
});
@@ -637,7 +648,7 @@ router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
updated_at: new Date()
};
const result = await botsSettings.saveTelegramSettings(settings);
const result = await botsSettings.saveBotSettings('telegram', settings);
res.json({ success: true, data: result });
} catch (error) {
logger.error('Ошибка при обновлении настроек Telegram:', error);
@@ -648,10 +659,14 @@ router.put('/telegram-settings', requireAdmin, async (req, res, next) => {
// Получить список всех Telegram-ботов (для ассистента)
router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => {
try {
const bots = await botsSettings.getAllTelegramBots();
logger.info('[Settings] Запрос списка telegram ботов');
const bots = await encryptedDb.getData('telegram_settings', {}, 1000, 'id ASC');
logger.info('[Settings] Получено telegram ботов:', bots ? bots.length : 0);
res.json({ success: true, items: bots });
} catch (error) {
res.status(404).json({ success: false, error: error.message });
logger.error('[Settings] Ошибка получения списка telegram:', error);
logger.error('[Settings] Stack:', error.stack);
res.status(500).json({ success: false, error: error.message });
}
});

View File

@@ -208,7 +208,7 @@ router.get('/', requireAuth, async (req, res, next) => {
});
}
// --- Формируем ответ ---
// --- Формируем ответ для зарегистрированных пользователей ---
const contacts = users.map(u => ({
id: u.id,
name: [u.first_name, u.last_name].filter(Boolean).join(' ').trim() || null,
@@ -222,7 +222,61 @@ router.get('/', requireAuth, async (req, res, next) => {
role: u.role || 'user'
}));
res.json({ success: true, contacts });
// --- Добавляем гостевые контакты ---
const guestContactsResult = await db.getQuery()(
`WITH decrypted_guests AS (
SELECT
decrypt_text(identifier_encrypted, $1) as guest_identifier,
channel,
created_at,
user_id
FROM unified_guest_messages
WHERE user_id IS NULL
)
SELECT
guest_identifier,
channel,
MIN(created_at) as created_at,
MAX(created_at) as last_message_at,
COUNT(*) as message_count
FROM decrypted_guests
GROUP BY guest_identifier, channel
ORDER BY MAX(created_at) DESC`,
[encryptionKey]
);
const guestContacts = guestContactsResult.rows.map(g => {
const channelMap = {
'web': '🌐',
'telegram': '📱',
'email': '✉️'
};
const icon = channelMap[g.channel] || '👤';
const rawId = g.guest_identifier.replace(`${g.channel}:`, '');
return {
id: g.guest_identifier, // Используем unified identifier как ID
name: `${icon} ${g.channel === 'web' ? 'Гость' : g.channel} (${rawId.substring(0, 8)}...)`,
email: g.channel === 'email' ? rawId : null,
telegram: g.channel === 'telegram' ? rawId : null,
wallet: null,
created_at: g.created_at,
preferred_language: [],
is_blocked: false,
contact_type: 'guest',
role: 'guest',
guest_info: {
channel: g.channel,
message_count: parseInt(g.message_count),
last_message_at: g.last_message_at
}
};
});
// Объединяем списки
const allContacts = [...contacts, ...guestContacts];
res.json({ success: true, contacts: allContacts });
} catch (error) {
logger.error('Error fetching contacts:', error);
next(error);
@@ -401,9 +455,64 @@ router.get('/:id', async (req, res, next) => {
const encryptionKey = encryptionUtils.getEncryptionKey();
try {
const query = db.getQuery();
// Получаем пользователя
// Проверяем, это гостевой идентификатор (формат: channel:rawId)
if (userId.includes(':')) {
const guestResult = await query(
`WITH decrypted_guest AS (
SELECT
decrypt_text(identifier_encrypted, $2) as guest_identifier,
channel,
created_at
FROM unified_guest_messages
WHERE decrypt_text(identifier_encrypted, $2) = $1
)
SELECT
guest_identifier,
channel,
MIN(created_at) as created_at,
MAX(created_at) as last_message_at,
COUNT(*) as message_count
FROM decrypted_guest
GROUP BY guest_identifier, channel`,
[userId, encryptionKey]
);
if (guestResult.rows.length === 0) {
return res.status(404).json({ error: 'Guest contact not found' });
}
const guest = guestResult.rows[0];
const rawId = userId.replace(`${guest.channel}:`, '');
const channelMap = {
'web': '🌐',
'telegram': '📱',
'email': '✉️'
};
const icon = channelMap[guest.channel] || '👤';
return res.json({
id: userId,
name: `${icon} ${guest.channel === 'web' ? 'Гость' : guest.channel} (${rawId.substring(0, 8)}...)`,
email: guest.channel === 'email' ? rawId : null,
telegram: guest.channel === 'telegram' ? rawId : null,
wallet: null,
created_at: guest.created_at,
preferred_language: [],
is_blocked: false,
contact_type: 'guest',
role: 'guest',
guest_info: {
channel: guest.channel,
message_count: parseInt(guest.message_count),
last_message_at: guest.last_message_at,
raw_identifier: rawId
}
});
}
// Получаем пользователя (зарегистрированный)
const userResult = await query('SELECT id, created_at, preferred_language, is_blocked FROM users WHERE id = $1', [userId]);
if (userResult.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });