Files
DLE/backend/services/consentService.js
2026-03-01 22:03:48 +03:00

326 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2024-2026 Тарабанов Александр Викторович
* All rights reserved.
*
* This software is proprietary and confidential.
* Unauthorized copying, modification, or distribution is prohibited.
*
* For licensing inquiries: info@hb3-accelerator.com
* Website: https://hb3-accelerator.com
* GitHub: https://github.com/VC-HB3-Accelerator
*/
const db = require('../db');
const logger = require('../utils/logger');
const encryptedDb = require('./encryptedDatabaseService');
// Маппинг названий документов на типы согласий
const DOCUMENT_CONSENT_MAP = {
'Политика конфиденциальности': 'privacy_policy',
'Согласие на использование файлов cookie': 'cookies',
'Согласие на обработку персональных данных': 'personal_data_processing'
};
// Кэш для домена
let cachedDomain = null;
let domainCacheTime = 0;
const DOMAIN_CACHE_TTL = 5 * 60 * 1000; // 5 минут
/**
* Сбросить кэш домена (вызывается при сохранении нового домена)
*/
function clearDomainCache() {
cachedDomain = null;
domainCacheTime = 0;
}
/**
* Получить домен из настроек VDS
* @returns {Promise<string>} - Базовый URL (https://domain.com)
*/
async function getBaseUrl() {
try {
// Проверяем кэш
const now = Date.now();
if (cachedDomain && (now - domainCacheTime) < DOMAIN_CACHE_TTL) {
return cachedDomain;
}
// Проверяем process.env
if (process.env.BASE_URL) {
cachedDomain = process.env.BASE_URL;
domainCacheTime = now;
return cachedDomain;
}
// Загружаем из БД
const settings = await encryptedDb.getData('vds_settings', {}, 1);
if (settings.length > 0 && settings[0].domain) {
const domain = settings[0].domain;
cachedDomain = `https://${domain}`;
domainCacheTime = now;
// Обновляем process.env для текущего процесса
process.env.BASE_URL = cachedDomain;
return cachedDomain;
}
// Возвращаем дефолтное значение
const defaultUrl = 'http://localhost:9000';
cachedDomain = defaultUrl;
domainCacheTime = now;
return defaultUrl;
} catch (error) {
logger.error('[ConsentService] Ошибка получения домена:', error);
return process.env.BASE_URL || 'http://localhost:9000';
}
}
/**
* Проверить согласия пользователя или гостя
* @param {Object} params - Параметры проверки
* @param {number|null} params.userId - ID пользователя (если авторизован)
* @param {string|null} params.walletAddress - Адрес кошелька или guest_ID
* @returns {Promise<Object>} - Результат проверки с информацией о недостающих согласиях
*/
async function checkConsents({ userId = null, walletAddress = null }) {
try {
const requiredConsentTypes = Object.values(DOCUMENT_CONSENT_MAP);
// Строим запрос в зависимости от наличия userId
let consentCheckQuery;
let queryParams;
if (userId) {
// Для авторизованного пользователя
consentCheckQuery = `
SELECT consent_type, COUNT(*) as count
FROM consent_logs
WHERE status = 'granted'
AND (user_id = $1 OR wallet_address = $2)
AND consent_type = ANY($3)
GROUP BY consent_type
`;
queryParams = [userId, walletAddress, requiredConsentTypes];
} else if (walletAddress) {
// Для гостя
consentCheckQuery = `
SELECT consent_type, COUNT(*) as count
FROM consent_logs
WHERE wallet_address = $1
AND status = 'granted'
AND consent_type = ANY($2)
GROUP BY consent_type
`;
queryParams = [walletAddress, requiredConsentTypes];
} else {
// Если нет ни userId, ни walletAddress - все согласия отсутствуют
return {
needsConsent: true,
missingConsents: requiredConsentTypes,
grantedConsents: []
};
}
const consentResult = await db.getQuery()(consentCheckQuery, queryParams);
const grantedConsentTypes = consentResult.rows.map(r => r.consent_type);
const missingConsents = requiredConsentTypes.filter(type => !grantedConsentTypes.includes(type));
const needsConsent = missingConsents.length > 0;
return {
needsConsent,
missingConsents,
grantedConsents: grantedConsentTypes
};
} catch (error) {
logger.error('[ConsentService] Ошибка проверки согласий:', error);
// В случае ошибки считаем, что согласия нужны для безопасности
return {
needsConsent: true,
missingConsents: Object.values(DOCUMENT_CONSENT_MAP),
grantedConsents: []
};
}
}
/**
* Получить список документов для подписания
* @param {Array<string>} missingConsents - Типы недостающих согласий
* @returns {Promise<Array>} - Массив документов с информацией
*/
async function getConsentDocuments(missingConsents = []) {
try {
// Определяем, какие документы нужно подписать (по недостающим типам согласий)
const documentsToShow = Object.entries(DOCUMENT_CONSENT_MAP)
.filter(([title, consentType]) => missingConsents.includes(consentType))
.map(([title]) => title);
if (documentsToShow.length === 0) {
return [];
}
// Получаем список документов для подписания
const { rows: documents } = await db.getQuery()(`
SELECT id, title, summary
FROM admin_pages_simple
WHERE status = 'published'
AND visibility = 'public'
AND title = ANY($1)
ORDER BY created_at DESC
`, [documentsToShow]);
return documents.map(doc => ({
id: doc.id,
title: doc.title,
summary: doc.summary,
consentType: DOCUMENT_CONSENT_MAP[doc.title],
url: `/content/published/${doc.id}`
}));
} catch (error) {
logger.error('[ConsentService] Ошибка получения документов:', error);
return [];
}
}
/**
* Сформировать системное сообщение о необходимости согласия
* @param {Object} params - Параметры
* @param {string} params.channel - Канал (web/telegram/email)
* @param {Array} params.missingConsents - Типы недостающих согласий
* @param {Array} params.consentDocuments - Документы для подписания
* @param {string} params.baseUrl - Базовый URL сайта (для формирования ссылок)
* @returns {Object} - Системное сообщение, отформатированное для канала
*/
function formatConsentMessage({ channel = 'web', missingConsents = [], consentDocuments = [], baseUrl = 'http://localhost:9000' }) {
const baseMessage = 'Для полноценного использования сервиса необходимо предоставить согласие на обработку персональных данных.';
const autoConsentMessage = '\n\n⚠ Внимание: При ответе на это сообщение вы автоматически подтверждаете ознакомление с документами и даете согласие на обработку персональных данных.';
switch (channel) {
case 'web':
// Для веб-чата возвращаем структурированные данные
return {
content: baseMessage + autoConsentMessage,
consentRequired: true,
missingConsents,
consentDocuments,
autoConsentOnReply: true, // Флаг для автоматического подписания при ответе
format: 'structured'
};
case 'telegram':
// Для Telegram форматируем как текст с ссылками
let telegramMessage = `${baseMessage}\n\nДля продолжения работы ознакомьтесь со следующими документами:\n\n`;
consentDocuments.forEach((doc, index) => {
telegramMessage += `${index + 1}. ${doc.title}\n`;
if (doc.summary) {
telegramMessage += ` ${doc.summary}\n`;
}
telegramMessage += ` ${baseUrl}${doc.url}\n\n`;
});
telegramMessage += `⚠️ Внимание: При ответе на это сообщение вы автоматически подтверждаете ознакомление с документами и даете согласие на обработку персональных данных.`;
return {
content: telegramMessage,
consentRequired: true,
missingConsents,
consentDocuments,
autoConsentOnReply: true,
format: 'text'
};
case 'email':
// Для email форматируем как HTML
let emailHtml = `<p>${baseMessage}</p>`;
emailHtml += '<p>Для продолжения работы ознакомьтесь со следующими документами:</p>';
emailHtml += '<ul>';
consentDocuments.forEach((doc) => {
emailHtml += `<li><strong>${doc.title}</strong><br>`;
if (doc.summary) {
emailHtml += `${doc.summary}<br>`;
}
emailHtml += `<a href="${baseUrl}${doc.url}">Открыть документ</a></li>`;
});
emailHtml += '</ul>';
emailHtml += `<p><strong>⚠️ Внимание:</strong> При ответе на это письмо вы автоматически подтверждаете ознакомление с документами и даете согласие на обработку персональных данных.</p>`;
emailHtml += `<p><a href="${baseUrl}/consent">Также вы можете подписать документы на сайте</a></p>`;
const textContent = baseMessage + '\n\n' +
consentDocuments.map(doc => `${doc.title}: ${baseUrl}${doc.url}`).join('\n') +
'\n\n⚠ Внимание: При ответе на это письмо вы автоматически подтверждаете ознакомление с документами и даете согласие на обработку персональных данных.';
return {
content: emailHtml,
textContent: textContent,
consentRequired: true,
missingConsents,
consentDocuments,
autoConsentOnReply: true,
format: 'html'
};
default:
return {
content: baseMessage + autoConsentMessage,
consentRequired: true,
missingConsents,
consentDocuments,
autoConsentOnReply: true,
format: 'text'
};
}
}
/**
* Получить полную информацию о согласиях и сформировать системное сообщение
* @param {Object} params - Параметры
* @param {number|null} params.userId - ID пользователя
* @param {string|null} params.walletAddress - Адрес кошелька или guest_ID
* @param {string} params.channel - Канал (web/telegram/email)
* @param {string} params.baseUrl - Базовый URL сайта (опционально, если не указан - загружается из БД)
* @returns {Promise<Object|null>} - Системное сообщение или null, если согласия есть
*/
async function getConsentSystemMessage({ userId = null, walletAddress = null, channel = 'web', baseUrl = null }) {
try {
// Если baseUrl не указан, загружаем из БД
if (!baseUrl) {
baseUrl = await getBaseUrl();
}
// Проверяем согласия
const consentCheck = await checkConsents({ userId, walletAddress });
if (!consentCheck.needsConsent) {
return null; // Все согласия есть
}
// Получаем документы для подписания
const consentDocuments = await getConsentDocuments(consentCheck.missingConsents);
// Формируем системное сообщение для канала
return formatConsentMessage({
channel,
missingConsents: consentCheck.missingConsents,
consentDocuments,
baseUrl
});
} catch (error) {
logger.error('[ConsentService] Ошибка формирования системного сообщения:', error);
return null;
}
}
module.exports = {
checkConsents,
getConsentDocuments,
formatConsentMessage,
getConsentSystemMessage,
getBaseUrl,
clearDomainCache,
DOCUMENT_CONSENT_MAP
};