feat: новая функция
This commit is contained in:
@@ -25,6 +25,9 @@ const emailAuth = require('../services/emailAuth');
|
||||
const verificationService = require('../services/verification-service');
|
||||
const identityService = require('../services/identity-service');
|
||||
const sessionService = require('../services/session-service');
|
||||
// Используем централизованный сервис для работы с согласиями
|
||||
const consentService = require('../services/consentService');
|
||||
const { DOCUMENT_CONSENT_MAP } = consentService;
|
||||
|
||||
// Создаем лимитер для попыток аутентификации
|
||||
const authLimiter = rateLimit({
|
||||
@@ -173,30 +176,60 @@ router.post('/verify', async (req, res) => {
|
||||
const origin = req.get('origin') || 'http://localhost:5173';
|
||||
const domain = new URL(origin).host; // Извлекаем домен из origin
|
||||
|
||||
// Получаем список документов для подписания и добавляем их в resources
|
||||
const documentTitles = Object.keys(DOCUMENT_CONSENT_MAP);
|
||||
const tableName = 'admin_pages_simple';
|
||||
const tableExistsRes = await db.getQuery()(
|
||||
`SELECT to_regclass($1) as exists`, [tableName]
|
||||
);
|
||||
|
||||
let resources = [`${origin}/api/auth/verify`];
|
||||
if (tableExistsRes.rows[0].exists) {
|
||||
const { rows: documents } = await db.getQuery()(`
|
||||
SELECT id FROM ${tableName}
|
||||
WHERE status = 'published'
|
||||
AND visibility = 'public'
|
||||
AND title = ANY($1)
|
||||
`, [documentTitles]);
|
||||
|
||||
// Добавляем ссылки на документы в resources
|
||||
documents.forEach(doc => {
|
||||
resources.push(`${origin}/content/published/${doc.id}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Сортируем resources для консистентности (должно совпадать с фронтендом)
|
||||
resources = resources.sort();
|
||||
|
||||
// Используем issuedAt из запроса, если он есть, иначе создаем новый
|
||||
const messageIssuedAt = issuedAt || new Date().toISOString();
|
||||
|
||||
const { SiweMessage } = require('siwe');
|
||||
const message = new SiweMessage({
|
||||
domain,
|
||||
address: normalizedAddress,
|
||||
statement: 'Sign in with Ethereum to the app.',
|
||||
statement: 'Sign in with Ethereum to the app.\n\nПодписывая это сообщение, вы подтверждаете ознакомление с документами, указанными в Resources, и согласие на обработку персональных данных.',
|
||||
uri: origin,
|
||||
version: '1',
|
||||
chainId: 1,
|
||||
nonce: nonce,
|
||||
issuedAt: issuedAt || new Date().toISOString(),
|
||||
resources: [`${origin}/api/auth/verify`],
|
||||
issuedAt: messageIssuedAt,
|
||||
resources: resources,
|
||||
});
|
||||
|
||||
const messageToSign = message.prepareMessage();
|
||||
|
||||
logger.info(`[verify] SIWE message for verification: ${messageToSign}`);
|
||||
logger.info(`[verify] Resources: ${JSON.stringify(resources)}`);
|
||||
logger.info(`[verify] IssuedAt: ${messageIssuedAt}`);
|
||||
logger.info(`[verify] Domain: ${domain}, Origin: ${origin}`);
|
||||
logger.info(`[verify] Normalized address: ${normalizedAddress}`);
|
||||
logger.info(`[verify] Request headers origin: ${req.get('origin')}`);
|
||||
logger.info(`[verify] Request headers host: ${req.get('host')}`);
|
||||
logger.info(`[verify] Request headers referer: ${req.get('referer')}`);
|
||||
|
||||
// Проверяем подпись
|
||||
const isValid = await authService.verifySignature(messageToSign, signature, normalizedAddress);
|
||||
// Проверяем подпись через SiweMessage.verify() (передаем объект сообщения, а не строку)
|
||||
const isValid = await authService.verifySignature(message, signature, normalizedAddress);
|
||||
if (!isValid) {
|
||||
logger.error(`[verify] Invalid signature for address: ${normalizedAddress}`);
|
||||
return res.status(401).json({ success: false, error: 'Invalid signature' });
|
||||
@@ -257,6 +290,15 @@ router.post('/verify', async (req, res) => {
|
||||
// Связываем гостевые сообщения с пользователем
|
||||
await sessionService.linkGuestMessages(req.session, userId);
|
||||
|
||||
// Проверяем согласия пользователя через централизованный сервис
|
||||
const consentCheck = await consentService.checkConsents({
|
||||
userId,
|
||||
walletAddress: normalizedAddress
|
||||
});
|
||||
|
||||
const needsConsent = consentCheck.needsConsent;
|
||||
const missingConsents = consentCheck.missingConsents;
|
||||
|
||||
// Возвращаем успешный ответ
|
||||
userAccessLevel = await authService.getUserAccessLevel(normalizedAddress);
|
||||
return res.json({
|
||||
@@ -265,6 +307,8 @@ router.post('/verify', async (req, res) => {
|
||||
address: normalizedAddress, // Возвращаем нормализованный адрес
|
||||
userAccessLevel: userAccessLevel,
|
||||
authenticated: true,
|
||||
needsConsent: needsConsent,
|
||||
missingConsents: missingConsents,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[verify] Error:', error);
|
||||
|
||||
@@ -25,6 +25,14 @@ const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
||||
const botManager = require('../services/botManager');
|
||||
const universalMediaProcessor = require('../services/UniversalMediaProcessor');
|
||||
|
||||
// Маппинг названий документов на типы согласий
|
||||
const DOCUMENT_CONSENT_MAP = {
|
||||
'Политика конфиденциальности': 'privacy_policy',
|
||||
'Права субъектов персональных данных и отзыв согласия': 'personal_data',
|
||||
'Согласие на использование файлов cookie': 'cookies',
|
||||
'Согласие на обработку персональных данных': 'personal_data_processing',
|
||||
};
|
||||
|
||||
// Настройка multer для обработки файлов в памяти
|
||||
const storage = multer.memoryStorage();
|
||||
const upload = multer({ storage: storage });
|
||||
@@ -143,6 +151,7 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
|
||||
};
|
||||
|
||||
// Обработка через unified processor
|
||||
// Системное сообщение о согласиях будет добавлено к ответу ИИ внутри процессора
|
||||
const result = await unifiedMessageProcessor.processMessage(messageData);
|
||||
|
||||
logger.info('[Chat] Результат обработки:', {
|
||||
@@ -151,13 +160,25 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
|
||||
aiResponseType: typeof result.aiResponse?.response
|
||||
});
|
||||
|
||||
res.json({
|
||||
// Формируем ответ
|
||||
// Системное сообщение уже включено в ответ ИИ (если нужно)
|
||||
const response = {
|
||||
success: true,
|
||||
guestId: webGuestId,
|
||||
aiResponse: result.aiResponse ? {
|
||||
response: result.aiResponse.response
|
||||
} : null
|
||||
});
|
||||
};
|
||||
|
||||
// Добавляем информацию о согласиях из результата (если есть)
|
||||
if (result.consentRequired) {
|
||||
response.consentRequired = result.consentRequired;
|
||||
response.missingConsents = result.missingConsents;
|
||||
response.consentDocuments = result.consentDocuments;
|
||||
response.autoConsentOnReply = result.autoConsentOnReply;
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[Chat] Ошибка обработки гостевого сообщения:', error);
|
||||
@@ -268,9 +289,11 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
};
|
||||
|
||||
// Обработка через unified processor
|
||||
// Системное сообщение о согласиях будет добавлено к ответу ИИ внутри процессора
|
||||
const result = await unifiedMessageProcessor.processMessage(messageData);
|
||||
|
||||
res.json({
|
||||
// Формируем ответ с информацией о согласиях
|
||||
const response = {
|
||||
success: true,
|
||||
userMessageId: result.userMessageId,
|
||||
conversationId: result.conversationId,
|
||||
@@ -278,7 +301,17 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
|
||||
response: result.aiResponse.response
|
||||
} : null,
|
||||
noAiResponse: result.noAiResponse
|
||||
});
|
||||
};
|
||||
|
||||
// Добавляем информацию о согласиях из результата (если есть)
|
||||
if (result.consentRequired) {
|
||||
response.consentRequired = result.consentRequired;
|
||||
response.missingConsents = result.missingConsents;
|
||||
response.consentDocuments = result.consentDocuments;
|
||||
response.autoConsentOnReply = result.autoConsentOnReply;
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[Chat] Ошибка обработки сообщения:', error);
|
||||
|
||||
289
backend/routes/consent.js
Normal file
289
backend/routes/consent.js
Normal file
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* 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 express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const consentService = require('../services/consentService');
|
||||
const { DOCUMENT_CONSENT_MAP } = consentService;
|
||||
|
||||
// Получить список документов для подписания
|
||||
router.get('/documents', async (req, res) => {
|
||||
try {
|
||||
const tableName = 'admin_pages_simple';
|
||||
|
||||
// Проверяем, есть ли таблица
|
||||
const existsRes = await db.getQuery()(
|
||||
`SELECT to_regclass($1) as exists`, [tableName]
|
||||
);
|
||||
|
||||
if (!existsRes.rows[0].exists) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
// Получаем документы для подписания по названиям
|
||||
const documentTitles = Object.keys(DOCUMENT_CONSENT_MAP);
|
||||
const { rows } = await db.getQuery()(`
|
||||
SELECT id, title, summary, content, created_at, updated_at
|
||||
FROM ${tableName}
|
||||
WHERE status = 'published'
|
||||
AND visibility = 'public'
|
||||
AND title = ANY($1)
|
||||
ORDER BY created_at DESC
|
||||
`, [documentTitles]);
|
||||
|
||||
// Добавляем тип согласия к каждому документу
|
||||
const documents = rows.map(doc => ({
|
||||
...doc,
|
||||
consentType: DOCUMENT_CONSENT_MAP[doc.title] || null,
|
||||
}));
|
||||
|
||||
res.json(documents);
|
||||
} catch (error) {
|
||||
logger.error('Error fetching consent documents:', error);
|
||||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Проверить, подписал ли пользователь необходимые документы
|
||||
router.get('/status', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.session.userId;
|
||||
const walletAddress = req.session.address;
|
||||
|
||||
if (!userId && !walletAddress) {
|
||||
return res.status(400).json({ error: 'Требуется авторизация' });
|
||||
}
|
||||
|
||||
// Получаем все необходимые типы согласий
|
||||
const requiredConsentTypes = Object.values(DOCUMENT_CONSENT_MAP);
|
||||
|
||||
// Получаем активные согласия пользователя
|
||||
let query = `
|
||||
SELECT consent_type, status, signed_at, document_id, document_title
|
||||
FROM consent_logs
|
||||
WHERE status = 'granted'
|
||||
AND (
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
if (userId) {
|
||||
query += `user_id = $${params.length + 1}`;
|
||||
params.push(userId);
|
||||
}
|
||||
if (walletAddress) {
|
||||
if (params.length > 0) query += ' OR ';
|
||||
query += `wallet_address = $${params.length + 1}`;
|
||||
params.push(walletAddress);
|
||||
}
|
||||
|
||||
query += `)
|
||||
AND consent_type = ANY($${params.length + 1})
|
||||
ORDER BY signed_at DESC
|
||||
`;
|
||||
params.push(requiredConsentTypes);
|
||||
|
||||
const { rows } = await db.getQuery()(query, params);
|
||||
|
||||
// Формируем статус для каждого типа согласия
|
||||
const status = {};
|
||||
requiredConsentTypes.forEach(type => {
|
||||
const consent = rows.find(r => r.consent_type === type);
|
||||
status[type] = consent ? {
|
||||
granted: true,
|
||||
signedAt: consent.signed_at,
|
||||
documentId: consent.document_id,
|
||||
documentTitle: consent.document_title,
|
||||
} : {
|
||||
granted: false,
|
||||
};
|
||||
});
|
||||
|
||||
// Проверяем, все ли согласия предоставлены
|
||||
const allGranted = requiredConsentTypes.every(type => status[type].granted);
|
||||
|
||||
res.json({
|
||||
allGranted,
|
||||
status,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error checking consent status:', error);
|
||||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Сохранить согласие пользователя
|
||||
router.post('/grant', async (req, res) => {
|
||||
try {
|
||||
// Разрешаем подпись как для авторизованных, так и для гостей
|
||||
const userId = req.session?.userId || null;
|
||||
const walletAddress = req.session?.address || null;
|
||||
const guestId = req.session?.guestId || null;
|
||||
const { documentIds, consentTypes } = req.body; // Массивы ID документов и типов согласий
|
||||
|
||||
// Если нет ни userId, ни walletAddress, используем guestId для идентификации
|
||||
if (!userId && !walletAddress && !guestId) {
|
||||
return res.status(400).json({ error: 'Требуется идентификация (авторизация или гостевая сессия)' });
|
||||
}
|
||||
|
||||
if (!documentIds || !Array.isArray(documentIds) || documentIds.length === 0) {
|
||||
return res.status(400).json({ error: 'Требуется указать документы для подписания' });
|
||||
}
|
||||
|
||||
// Получаем информацию о документах
|
||||
const { rows: documents } = await db.getQuery()(`
|
||||
SELECT id, title
|
||||
FROM admin_pages_simple
|
||||
WHERE id = ANY($1) AND status = 'published'
|
||||
`, [documentIds]);
|
||||
|
||||
if (documents.length !== documentIds.length) {
|
||||
return res.status(400).json({ error: 'Некоторые документы не найдены' });
|
||||
}
|
||||
|
||||
const ipAddress = req.ip || req.connection.remoteAddress;
|
||||
const userAgent = req.get('user-agent');
|
||||
|
||||
// Сохраняем согласия для каждого документа
|
||||
const results = [];
|
||||
for (let i = 0; i < documents.length; i++) {
|
||||
const doc = documents[i];
|
||||
const consentType = consentTypes && consentTypes[i]
|
||||
? consentTypes[i]
|
||||
: DOCUMENT_CONSENT_MAP[doc.title];
|
||||
|
||||
if (!consentType) {
|
||||
logger.warn(`Unknown consent type for document: ${doc.title}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем, есть ли уже активное согласие
|
||||
let checkQuery = `
|
||||
SELECT id FROM consent_logs
|
||||
WHERE status = 'granted' AND consent_type = $1 AND (
|
||||
`;
|
||||
const checkParams = [consentType];
|
||||
if (userId) {
|
||||
checkQuery += `user_id = $${checkParams.length + 1}`;
|
||||
checkParams.push(userId);
|
||||
}
|
||||
if (walletAddress) {
|
||||
if (checkParams.length > 1) checkQuery += ' OR ';
|
||||
checkQuery += `wallet_address = $${checkParams.length + 1}`;
|
||||
checkParams.push(walletAddress);
|
||||
}
|
||||
// Для гостей проверяем по формату guest_${guestId}
|
||||
if (guestId && !walletAddress) {
|
||||
if (checkParams.length > 1) checkQuery += ' OR ';
|
||||
checkQuery += `wallet_address = $${checkParams.length + 1}`;
|
||||
checkParams.push(`guest_${guestId}`);
|
||||
}
|
||||
checkQuery += ')';
|
||||
|
||||
const existing = await db.getQuery()(checkQuery, checkParams);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
// Обновляем существующее согласие
|
||||
await db.getQuery()(`
|
||||
UPDATE consent_logs
|
||||
SET document_id = $1,
|
||||
document_title = $2,
|
||||
signed_at = NOW(),
|
||||
revoked_at = NULL,
|
||||
ip_address = $3,
|
||||
user_agent = $4,
|
||||
updated_at = NOW()
|
||||
WHERE id = $5
|
||||
`, [doc.id, doc.title, ipAddress, userAgent, existing.rows[0].id]);
|
||||
|
||||
results.push({ documentId: doc.id, consentType, action: 'updated' });
|
||||
} else {
|
||||
// Для гостей используем guestId как wallet_address для последующей миграции
|
||||
const consentWalletAddress = walletAddress || (guestId ? `guest_${guestId}` : null);
|
||||
|
||||
// Создаем новое согласие
|
||||
await db.getQuery()(`
|
||||
INSERT INTO consent_logs (
|
||||
user_id, wallet_address, document_id, document_title,
|
||||
consent_type, status, ip_address, user_agent, channel
|
||||
) VALUES ($1, $2, $3, $4, $5, 'granted', $6, $7, 'web')
|
||||
`, [userId, consentWalletAddress, doc.id, doc.title, consentType, ipAddress, userAgent]);
|
||||
|
||||
results.push({ documentId: doc.id, consentType, action: 'created' });
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Consent granted: userId=${userId}, walletAddress=${walletAddress}, documents=${documentIds.join(',')}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
results,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error granting consent:', error);
|
||||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
// Отозвать согласие
|
||||
router.post('/revoke', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.session.userId;
|
||||
const walletAddress = req.session.address;
|
||||
const { consentTypes } = req.body; // Массив типов согласий для отзыва
|
||||
|
||||
if (!userId && !walletAddress) {
|
||||
return res.status(400).json({ error: 'Требуется авторизация' });
|
||||
}
|
||||
|
||||
if (!consentTypes || !Array.isArray(consentTypes) || consentTypes.length === 0) {
|
||||
return res.status(400).json({ error: 'Требуется указать типы согласий для отзыва' });
|
||||
}
|
||||
|
||||
let query = `
|
||||
UPDATE consent_logs
|
||||
SET status = 'revoked',
|
||||
revoked_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE consent_type = ANY($1) AND status = 'granted' AND (
|
||||
`;
|
||||
|
||||
const params = [consentTypes];
|
||||
if (userId) {
|
||||
query += `user_id = $${params.length + 1}`;
|
||||
params.push(userId);
|
||||
}
|
||||
if (walletAddress) {
|
||||
if (params.length > 1) query += ' OR ';
|
||||
query += `wallet_address = $${params.length + 1}`;
|
||||
params.push(walletAddress);
|
||||
}
|
||||
query += ')';
|
||||
|
||||
const { rowCount } = await db.getQuery()(query, params);
|
||||
|
||||
logger.info(`Consent revoked: userId=${userId}, walletAddress=${walletAddress}, types=${consentTypes.join(',')}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
revokedCount: rowCount,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error revoking consent:', error);
|
||||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user