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));
}
}