From 8fe63beb8f94bccfcfdd2df10b4e19a6a398b1a4 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 12 Dec 2025 19:30:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D0=B0=D1=88=D0=B5=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/messages.js | 174 +++++++++++++++++- backend/routes/pages.js | 108 ++++++++++- .../src/views/contacts/ContactDetailsView.vue | 3 + 3 files changed, 277 insertions(+), 8 deletions(-) diff --git a/backend/routes/messages.js b/backend/routes/messages.js index 226bab3..ed31215 100644 --- a/backend/routes/messages.js +++ b/backend/routes/messages.js @@ -226,7 +226,7 @@ router.get('/public', requireAuth, async (req, res) => { (m.message_type = 'public' AND ((m.user_id = $1 AND m.sender_id = $5) OR (m.user_id = $5 AND m.sender_id = $1))) OR (m.message_type = 'user_chat' AND m.user_id = $1) ) - ORDER BY m.created_at DESC + ORDER BY m.created_at ASC LIMIT $3 OFFSET $4`, [targetUserId, encryptionKey, limit, offset, currentUserId] ); @@ -685,6 +685,178 @@ router.post('/send', requireAuth, async (req, res) => { } }); + // Отправляем сообщение через Telegram/Email, если у получателя есть эти идентификаторы + try { + const encryptionUtils = require('../utils/encryptionUtils'); + const encryptionKey = encryptionUtils.getEncryptionKey(); + const botManager = require('../services/botManager'); + const FRONTEND_URL = process.env.FRONTEND_URL || 'https://xn--80aqc0am6d.xn--p1ai'; + + // Получаем все идентификаторы получателя + const identitiesRes = await db.getQuery()( + 'SELECT decrypt_text(provider_encrypted, $2) as provider, decrypt_text(provider_id_encrypted, $2) as provider_id FROM user_identities WHERE user_id = $1', + [recipientIdNum, encryptionKey] + ); + const identities = identitiesRes.rows; + + // Функция для добавления параметров к ссылкам на страницы контента + // Редактор копирует URL из адресной строки и отправляет его в чат + // Функция находит все ссылки на /content/page/ или /public/page/ и добавляет параметры авторизации + // Best practice: используем встроенный URL API для надежной обработки + const addAuthParamsToLinks = (text, telegramId, email) => { + if (!text) return text; + + const params = {}; + if (telegramId) { + params.telegramId = telegramId; + } + if (email) { + params.email = email; + } + + if (Object.keys(params).length === 0) return text; + + // Вспомогательная функция для добавления параметров к URL + const addParamsToUrl = (urlString, baseUrl = null) => { + try { + // Парсим URL (полный или относительный) + const url = baseUrl ? new URL(urlString, baseUrl) : new URL(urlString); + + // Проверяем, является ли это страницей контента + const pathname = url.pathname; + if (!pathname.match(/^\/content\/page\/\d+$/) && !pathname.match(/^\/public\/page\/\d+$/)) { + return urlString; // Не страница контента, возвращаем как есть + } + + // Проверяем, не добавлены ли уже параметры авторизации + if (url.searchParams.has('telegramId') || url.searchParams.has('email')) { + return urlString; // Параметры уже есть + } + + // Добавляем параметры + Object.entries(params).forEach(([key, value]) => { + url.searchParams.set(key, value); + }); + + return url.toString(); + } catch (error) { + // Если URL некорректный, возвращаем как есть + return urlString; + } + }; + + // Обрабатываем HTML-ссылки + text = text.replace(/]*?)href=["']([^"']*)["']([^>]*)>/gi, (match, beforeAttrs, url, afterAttrs) => { + if (!url) return match; + + const newUrl = addParamsToUrl(url, FRONTEND_URL); + if (newUrl === url) return match; // URL не изменился + + return ``; + }); + + // Обрабатываем обычные текстовые ссылки (URL из адресной строки браузера) + // Ищем все варианты: полные URL и относительные пути + // Используем два отдельных паттерна для надежности + + // Паттерн для полных URL: https://domain.com/content/page/38 + const fullUrlPattern = /https?:\/\/[^\s<>"']+?(?:\/content\/page\/\d+|\/public\/page\/\d+)[^\s<>"']*/gi; + + text = text.replace(fullUrlPattern, (match, offset) => { + // Проверяем, не находимся ли мы внутри HTML-тега + const beforeMatch = text.substring(0, offset); + const lastOpenTag = beforeMatch.lastIndexOf(''); + + if (lastOpenTag > lastCloseTag) { + // Мы внутри открытого тега , пропускаем + return match; + } + + // Обрабатываем URL + const newUrl = addParamsToUrl(match); + return newUrl === match ? match : newUrl; + }); + + // Паттерн для относительных путей: /content/page/38 + const relativePathPattern = /\/content\/page\/\d+[^\s<>"']*|\/public\/page\/\d+[^\s<>"']*/g; + + text = text.replace(relativePathPattern, (match, offset) => { + // Пропускаем, если это уже полный URL (начинается с http) + if (offset > 0 && text.substring(Math.max(0, offset - 7), offset).match(/https?:\/\//i)) { + return match; + } + + // Проверяем, не находимся ли мы внутри HTML-тега + const beforeMatch = text.substring(0, offset); + const lastOpenTag = beforeMatch.lastIndexOf(''); + + if (lastOpenTag > lastCloseTag) { + // Мы внутри открытого тега , пропускаем + return match; + } + + // Обрабатываем относительный путь + const newUrl = addParamsToUrl(match, FRONTEND_URL); + return newUrl === match ? match : newUrl; + }); + + return text; + }; + + // Отправка через Telegram + const telegramIdentity = identities.find(i => i.provider === 'telegram'); + if (telegramIdentity && telegramIdentity.provider_id) { + try { + const telegramBot = botManager.getBot('telegram'); + if (telegramBot && telegramBot.isInitialized) { + // Добавляем параметры к ссылкам для автоматической авторизации + const contentWithLinks = addAuthParamsToLinks(content, telegramIdentity.provider_id, null); + await telegramBot.getBot().telegram.sendMessage(telegramIdentity.provider_id, contentWithLinks); + logger.info(`[messages/send] Сообщение отправлено через Telegram пользователю ${recipientIdNum}`); + } else { + logger.warn('[messages/send] Telegram Bot не инициализирован, сообщение сохранено только в истории'); + } + } catch (telegramError) { + logger.error('[messages/send] Ошибка отправки через Telegram:', telegramError); + } + } + + // Отправка через Email + const emailIdentity = identities.find(i => i.provider === 'email'); + if (emailIdentity && emailIdentity.provider_id) { + try { + const emailBot = botManager.getBot('email'); + if (emailBot && emailBot.isInitialized) { + // Добавляем параметры к ссылкам для автоматической авторизации + const contentWithLinks = addAuthParamsToLinks(content, null, emailIdentity.provider_id); + + // Проверяем, содержит ли контент HTML-теги + const isHtml = /<[a-z][\s\S]*>/i.test(contentWithLinks); + + if (isHtml) { + // Если контент содержит HTML, отправляем как HTML с текстовой версией + // Создаем текстовую версию (удаляем HTML-теги для простоты) + const textVersion = contentWithLinks.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); + await emailBot.sendEmailWithHtml(emailIdentity.provider_id, 'Новое сообщение', textVersion, contentWithLinks); + } else { + // Если контент текстовый, отправляем как текст + await emailBot.sendEmail(emailIdentity.provider_id, 'Новое сообщение', contentWithLinks); + } + logger.info(`[messages/send] Сообщение отправлено через Email пользователю ${recipientIdNum}`); + } else { + logger.warn('[messages/send] Email Bot не инициализирован, сообщение сохранено только в истории'); + } + } catch (emailError) { + logger.error('[messages/send] Ошибка отправки через Email:', emailError); + } + } + } catch (deliveryError) { + // Не критично, если не удалось отправить через внешние каналы - сообщение уже сохранено в БД + logger.warn('[messages/send] Ошибка отправки через внешние каналы (не критично):', deliveryError); + } + if (markAsRead) { try { const lastReadAt = new Date().toISOString(); diff --git a/backend/routes/pages.js b/backend/routes/pages.js index f61def8..2eec0fd 100644 --- a/backend/routes/pages.js +++ b/backend/routes/pages.js @@ -17,6 +17,7 @@ const fs = require('fs'); const path = require('path'); const multer = require('multer'); const vectorSearchClient = require('../services/vectorSearchClient'); +const logger = require('../utils/logger'); const FIELDS_TO_EXCLUDE = ['image', 'tags']; @@ -495,11 +496,72 @@ router.get('/categories', async (req, res) => { // Получить одну страницу по id (с проверкой прав доступа) router.get('/:id', async (req, res) => { try { + // Если пользователь не авторизован, проверяем параметры для автоматической авторизации if (!req.session || !req.session.authenticated) { - return res.status(401).json({ error: 'Требуется аутентификация' }); - } - if (!req.session.address) { - return res.status(403).json({ error: 'Требуется подключение кошелька' }); + const telegramId = req.query.telegramId; + const email = req.query.email; + + // Пытаемся автоматически авторизовать пользователя через Telegram/Email + if (telegramId || email) { + const identityService = require('../services/identity-service'); + const authService = require('../services/auth-service'); + + let user = null; + if (telegramId) { + user = await identityService.findUserByIdentity('telegram', telegramId); + } else if (email) { + user = await identityService.findUserByIdentity('email', email); + } + + if (user) { + // Автоматически создаем сессию для пользователя + req.session.userId = user.id; + req.session.authenticated = true; + if (telegramId) { + req.session.telegramId = telegramId; + req.session.authType = 'telegram'; + } else if (email) { + req.session.email = email; + req.session.authType = 'email'; + } + + // Проверяем, есть ли у пользователя связанный кошелек + const { getLinkedWallet } = require('../services/wallet-service'); + const linkedWallet = await getLinkedWallet(user.id); + + if (linkedWallet) { + // Если есть кошелек - проверяем токены и определяем роль по балансу + try { + req.session.address = linkedWallet; + const userAccessLevel = await authService.getUserAccessLevel(linkedWallet); + req.session.userAccessLevel = userAccessLevel; + logger.info(`[pages/:id] Автоматическая авторизация с кошельком: ${telegramId ? 'telegram' : 'email'}, user: ${user.id}, wallet: ${linkedWallet}, role: ${userAccessLevel.level}`); + } catch (walletError) { + // Если ошибка при проверке токенов, используем роль из БД + logger.warn(`[pages/:id] Ошибка проверки токенов для кошелька ${linkedWallet}, используем роль из БД:`, walletError); + req.session.userAccessLevel = { + level: user.role || 'user', + tokenCount: 0, + hasAccess: true + }; + } + } else { + // Если кошелька нет - используем роль из БД + req.session.userAccessLevel = { + level: user.role || 'user', + tokenCount: 0, + hasAccess: true + }; + logger.info(`[pages/:id] Автоматическая авторизация без кошелька: ${telegramId ? 'telegram' : 'email'}, user: ${user.id}, role: ${user.role || 'user'}`); + } + + await req.session.save(); + } else { + return res.status(401).json({ error: 'Требуется аутентификация' }); + } + } else { + return res.status(401).json({ error: 'Требуется аутентификация' }); + } } const tableName = `admin_pages_simple`; @@ -517,9 +579,38 @@ router.get('/:id', async (req, res) => { const page = rows[0]; // Проверяем доступ к странице в зависимости от её видимости - const authService = require('../services/auth-service'); - const userAccessLevel = await authService.getUserAccessLevel(req.session.address); - const role = userAccessLevel.level; // 'user' | 'readonly' | 'editor' + // authService уже объявлен выше при автоматической авторизации, используем его + let role = 'user'; + let userAccessLevel = { level: 'user', tokenCount: 0, hasAccess: true }; + + // Используем userAccessLevel из сессии, если он уже установлен (при автоматической авторизации) + if (req.session.userAccessLevel) { + userAccessLevel = req.session.userAccessLevel; + role = userAccessLevel.level; + } else if (req.session.address) { + // Для пользователей с кошельком проверяем токены + const authService = require('../services/auth-service'); + userAccessLevel = await authService.getUserAccessLevel(req.session.address); + role = userAccessLevel.level; + } else if (req.session.userId) { + // Для Telegram/Email пользователей без кошелька используем роль из БД + const roleResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [ + req.session.userId, + ]); + + if (roleResult.rows.length > 0) { + role = roleResult.rows[0].role; + // Преобразуем роль в формат userAccessLevel + if (role === 'editor') { + userAccessLevel = { level: 'editor', tokenCount: 5999998, hasAccess: true }; + } else if (role === 'readonly') { + userAccessLevel = { level: 'readonly', tokenCount: 100, hasAccess: true }; + } else { + // Для роли 'user' даем доступ к внутренним документам + userAccessLevel = { level: 'user', tokenCount: 0, hasAccess: true }; + } + } + } // Публичные страницы доступны всем if (page.visibility === 'public' && page.status === 'published') { @@ -545,6 +636,9 @@ router.get('/:id', async (req, res) => { // VIEW_BASIC_DOCS доступно всем аутентифицированным пользователям (user, readonly, editor) // VIEW_LEGAL_DOCS требует readonly или editor // MANAGE_LEGAL_DOCS требует editor + if (page.required_permission === PERMISSIONS.VIEW_BASIC_DOCS && !hasPermission(role, PERMISSIONS.VIEW_BASIC_DOCS)) { + return res.status(403).json({ error: 'Доступ запрещен: требуется авторизация' }); + } if (page.required_permission === PERMISSIONS.VIEW_LEGAL_DOCS && !hasPermission(role, PERMISSIONS.VIEW_LEGAL_DOCS)) { return res.status(403).json({ error: 'Доступ запрещен: требуются права читателя' }); } diff --git a/frontend/src/views/contacts/ContactDetailsView.vue b/frontend/src/views/contacts/ContactDetailsView.vue index 32b1ef0..080adfb 100644 --- a/frontend/src/views/contacts/ContactDetailsView.vue +++ b/frontend/src/views/contacts/ContactDetailsView.vue @@ -456,10 +456,13 @@ async function loadMessages() { } else { // Для других пользователей загружаем публичные сообщения между текущим пользователем и выбранным контактом + // И личные сообщения с ИИ целевого пользователя (для Telegram/Email пользователей) console.log('[ContactDetailsView] 🔍 Loading public messages between current user and contact:', contact.value.id); const response = await getPublicMessages(contact.value.id, { limit: 50, offset: 0 }); if (response.success && response.messages) { allMessages = response.messages; + // Сортируем по времени создания (от старых к новым) + allMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); } }