ваше сообщение коммита
This commit is contained in:
@@ -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-ссылки <a href="...">
|
||||
text = text.replace(/<a\s+([^>]*?)href=["']([^"']*)["']([^>]*)>/gi, (match, beforeAttrs, url, afterAttrs) => {
|
||||
if (!url) return match;
|
||||
|
||||
const newUrl = addParamsToUrl(url, FRONTEND_URL);
|
||||
if (newUrl === url) return match; // URL не изменился
|
||||
|
||||
return `<a ${beforeAttrs || ''}href="${newUrl}"${afterAttrs || ''}>`;
|
||||
});
|
||||
|
||||
// Обрабатываем обычные текстовые ссылки (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('<a');
|
||||
const lastCloseTag = beforeMatch.lastIndexOf('</a>');
|
||||
|
||||
if (lastOpenTag > lastCloseTag) {
|
||||
// Мы внутри открытого тега <a>, пропускаем
|
||||
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('<a');
|
||||
const lastCloseTag = beforeMatch.lastIndexOf('</a>');
|
||||
|
||||
if (lastOpenTag > lastCloseTag) {
|
||||
// Мы внутри открытого тега <a>, пропускаем
|
||||
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();
|
||||
|
||||
@@ -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) {
|
||||
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: 'Требуется аутентификация' });
|
||||
}
|
||||
if (!req.session.address) {
|
||||
return res.status(403).json({ error: 'Требуется подключение кошелька' });
|
||||
}
|
||||
|
||||
const tableName = `admin_pages_simple`;
|
||||
@@ -517,9 +579,38 @@ router.get('/:id', async (req, res) => {
|
||||
const page = rows[0];
|
||||
|
||||
// Проверяем доступ к странице в зависимости от её видимости
|
||||
// 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');
|
||||
const userAccessLevel = await authService.getUserAccessLevel(req.session.address);
|
||||
const role = userAccessLevel.level; // 'user' | 'readonly' | 'editor'
|
||||
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: 'Доступ запрещен: требуются права читателя' });
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user