/** * 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;