diff --git a/backend/db.js b/backend/db.js index 429503b..86b4994 100644 --- a/backend/db.js +++ b/backend/db.js @@ -10,10 +10,30 @@ * GitHub: https://github.com/VC-HB3-Accelerator */ -const { Pool } = require('pg'); +const { Pool, types } = require('pg'); require('dotenv').config(); const axios = require('axios'); +// Настройка парсера для BYTEA - возвращаем Buffer напрямую без конвертации в строку +// OID для BYTEA в PostgreSQL: 17 +types.setTypeParser(17, (value) => { + // value уже является Buffer при использовании binary формата + // Но если это строка, конвертируем её в Buffer + if (Buffer.isBuffer(value)) { + return value; + } + // Если это строка (hex или base64), конвертируем + if (typeof value === 'string') { + // Проверяем, является ли это hex строка + if (/^[0-9a-fA-F]+$/.test(value)) { + return Buffer.from(value, 'hex'); + } + // Иначе считаем binary строкой + return Buffer.from(value, 'binary'); + } + return value; +}); + // Убираем избыточное логирование настроек подключения // console.log('Настройки подключения к базе данных:'); // console.log('DATABASE_URL:', process.env.DATABASE_URL?.replace(/:([^:@]+)@/, ':***@')); diff --git a/backend/package.json b/backend/package.json index f3a2b09..0f48246 100644 --- a/backend/package.json +++ b/backend/package.json @@ -65,6 +65,7 @@ "nodemailer": "^6.10.0", "openai": "^4.102.0", "pg": "^8.10.0", + "pg-large-object": "^2.0.0", "semver": "^7.7.1", "session-file-store": "^1.5.0", "siwe": "^2.1.4", diff --git a/backend/routes/dleModules.js b/backend/routes/dleModules.js index 8d5a9d9..e21df9e 100644 --- a/backend/routes/dleModules.js +++ b/backend/routes/dleModules.js @@ -14,7 +14,13 @@ const express = require('express'); const router = express.Router(); const { ethers } = require('ethers'); const { Interface, AbiCoder } = ethers; -const hre = require('hardhat'); +// Hardhat опционален - используется только для компиляции в dev режиме +let hre = null; +try { + hre = require('hardhat'); +} catch (e) { + // Hardhat не установлен в production - это нормально +} const rpcProviderService = require('../services/rpcProviderService'); const { spawn } = require('child_process'); const path = require('path'); @@ -2181,6 +2187,9 @@ router.post('/deploy-module-all-networks', async (req, res) => { console.log(`[DLE Modules] Текущий nonce для сети ${network.chainId}: ${currentNonce}`); // Получаем фабрику контракта + if (!hre) { + throw new Error('Hardhat не установлен. Установите hardhat для компиляции контрактов.'); + } const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName); // Подготавливаем аргументы конструктора diff --git a/backend/routes/uploads.js b/backend/routes/uploads.js index 0ce5474..fe77d21 100644 --- a/backend/routes/uploads.js +++ b/backend/routes/uploads.js @@ -93,17 +93,39 @@ router.post('/media', auth.requireAuth, async (req, res) => { // Используем middleware для загрузки файла mediaUpload.single('media')(req, res, async (err) => { if (err) { + console.error('[uploads/media] Ошибка multer:', err); return res.status(400).json({ success: false, message: err.message }); } try { - if (!req.file || !req.file.buffer) return res.status(400).json({ success: false, message: 'Файл не получен' }); + if (!req.file) { + console.error('[uploads/media] Файл не получен в req.file'); + return res.status(400).json({ success: false, message: 'Файл не получен' }); + } + + if (!req.file.buffer) { + console.error('[uploads/media] Буфер файла отсутствует'); + return res.status(400).json({ success: false, message: 'Буфер файла отсутствует' }); + } + + if (!req.file.mimetype) { + console.error('[uploads/media] MIME тип отсутствует'); + return res.status(400).json({ success: false, message: 'MIME тип файла не определен' }); + } const db = require('../db'); const mediaType = req.file.mimetype.startsWith('image/') ? 'image' : 'video'; + console.log('[uploads/media] Начало обработки файла:', { + originalname: req.file.originalname, + mimetype: req.file.mimetype, + size: req.file.size, + mediaType: mediaType + }); + // Вычисляем SHA-256 хеш файла для дедупликации const fileHash = crypto.createHash('sha256').update(req.file.buffer).digest('hex'); + console.log('[uploads/media] Хеш файла вычислен:', fileHash.substring(0, 16) + '...'); // Проверяем, не загружен ли уже такой файл const existingFile = await db.getQuery()( @@ -116,10 +138,22 @@ router.post('/media', auth.requireAuth, async (req, res) => { if (existingFile.rows.length > 0) { // Файл уже существует - возвращаем существующую запись + console.log('[uploads/media] Файл уже существует, используем существующую запись:', existingFile.rows[0].id); mediaId = existingFile.rows[0].id; fileName = existingFile.rows[0].file_name; } else { // Сохраняем новый файл в базу данных + console.log('[uploads/media] Сохранение нового файла в БД...'); + + // Нормализуем page_id: преобразуем в число или null + let pageId = null; + if (req.body.page_id) { + const parsedPageId = parseInt(req.body.page_id); + if (!isNaN(parsedPageId) && parsedPageId > 0) { + pageId = parsedPageId; + } + } + const { rows } = await db.getQuery()(` INSERT INTO content_media ( file_data, @@ -140,11 +174,12 @@ router.post('/media', auth.requireAuth, async (req, res) => { fileHash, mediaType, req.session.address, - req.body.page_id || null + pageId ]); mediaId = rows[0].id; fileName = rows[0].file_name; + console.log('[uploads/media] Файл успешно сохранен в БД, ID:', mediaId); } // URL для доступа к файлу через API @@ -153,6 +188,7 @@ router.post('/media', auth.requireAuth, async (req, res) => { // Это позволяет работать с разными портами (frontend на 9000, backend на 8000) const fullUrl = fileUrl; + console.log('[uploads/media] Успешная загрузка, возвращаем ответ'); return res.json({ success: true, data: { @@ -168,39 +204,212 @@ router.post('/media', auth.requireAuth, async (req, res) => { } }); } catch (e) { - console.error('Ошибка сохранения медиа в БД:', e); - return res.status(500).json({ success: false, message: e.message }); + console.error('[uploads/media] Ошибка сохранения медиа в БД:', { + message: e.message, + stack: e.stack, + name: e.name, + code: e.code, + detail: e.detail, + constraint: e.constraint, + table: e.table, + column: e.column + }); + return res.status(500).json({ + success: false, + message: e.message || 'Внутренняя ошибка сервера', + error: process.env.NODE_ENV === 'development' ? { + name: e.name, + code: e.code, + detail: e.detail, + constraint: e.constraint + } : undefined + }); } }); }); -// GET /api/uploads/media/:id/file - получить файл по ID +// GET /api/uploads/media/:id/file - получить файл по ID с поддержкой Range requests router.get('/media/:id/file', async (req, res) => { + let client = null; + const mediaId = parseInt(req.params.id); + // Увеличиваем chunk size до 1MB для больших файлов - меньше запросов к БД + const chunkSize = 1048576; // 1MB chunks для оптимальной производительности стриминга + try { const db = require('../db'); - const mediaId = parseInt(req.params.id); - - const { rows } = await db.getQuery()( - 'SELECT file_data, file_name, mime_type, file_size FROM content_media WHERE id = $1', + const pool = db.getPool(); + client = await pool.connect(); + + // Сначала получаем метаданные без file_data + const metaResult = await client.query( + 'SELECT file_name, mime_type, file_size FROM content_media WHERE id = $1', [mediaId] ); - - if (rows.length === 0) { + + if (metaResult.rows.length === 0) { + client.release(); return res.status(404).json({ success: false, message: 'Медиа-файл не найден' }); } - - const media = rows[0]; - - // Устанавливаем заголовки для правильной отдачи файла + + const media = metaResult.rows[0]; + const fileSize = parseInt(media.file_size) || 0; + + // Поддержка HTTP Range requests для стриминга (как на YouTube/Vimeo) + const range = req.headers.range; + let start = 0; + let end = fileSize - 1; + let statusCode = 200; + + if (range) { + // Парсим Range заголовок (например: "bytes=0-1023" или "bytes=1024-") + const parts = range.replace(/bytes=/, '').split('-'); + start = parseInt(parts[0], 10); + end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; + + // Валидация диапазона + if (start >= fileSize || end >= fileSize) { + res.setHeader('Content-Range', `bytes */${fileSize}`); + client.release(); + return res.status(416).end(); // Range Not Satisfiable + } + + statusCode = 206; // Partial Content + } + + const contentLength = end - start + 1; + + // Устанавливаем заголовки для правильной отдачи файла с поддержкой Range res.setHeader('Content-Type', media.mime_type); - res.setHeader('Content-Length', media.file_size); - res.setHeader('Content-Disposition', `inline; filename="${media.file_name}"`); + res.setHeader('Accept-Ranges', 'bytes'); // Указываем, что поддерживаем Range requests + res.setHeader('Content-Length', contentLength); res.setHeader('Cache-Control', 'public, max-age=31536000'); // Кеширование на 1 год - // Отправляем бинарные данные - res.send(media.file_data); + if (range) { + res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`); + res.status(statusCode); + } else { + res.setHeader('Content-Disposition', `inline; filename="${media.file_name}"`); + } + + // Используем прямой стриминг BYTEA данных частями через SQL substring + // Начинаем с нужной позиции (для Range requests) + let offset = start + 1; // PostgreSQL substring использует 1-based индексацию + const endOffset = end + 1; + + const streamChunk = async () => { + try { + // Проверяем, не достигли ли мы конца запрошенного диапазона + if (offset > endOffset) { + // Достигнут конец запрошенного диапазона + client.release(); + res.end(); + return; + } + + // Вычисляем размер текущего chunk (может быть меньше chunkSize для последнего chunk) + const currentChunkSize = Math.min(chunkSize, endOffset - offset + 1); + + // Читаем следующий chunk данных, используя encode для получения hex-строки + const chunkResult = await client.query( + `SELECT encode(substring(file_data FROM $1 FOR $2), 'hex') as chunk_hex FROM content_media WHERE id = $3`, + [offset, currentChunkSize, mediaId] + ); + + if (chunkResult.rows.length === 0 || !chunkResult.rows[0] || !chunkResult.rows[0].chunk_hex) { + // Достигнут конец файла или данные отсутствуют + client.release(); + res.end(); + return; + } + + const chunkHex = chunkResult.rows[0].chunk_hex; + + // Если chunk пустой, значит достигнут конец + if (!chunkHex || chunkHex.length === 0) { + client.release(); + res.end(); + return; + } + + // Преобразуем hex-строку в Buffer + const buffer = Buffer.from(chunkHex, 'hex'); + + // Отправляем chunk клиенту + if (!res.write(buffer)) { + // Буфер переполнен, ждем события 'drain' + res.once('drain', () => { + offset += currentChunkSize; + streamChunk(); + }); + } else { + // Продолжаем отправку следующего chunk + offset += currentChunkSize; + streamChunk(); + } + } catch (chunkErr) { + console.error('[uploads/media/:id/file] Ошибка чтения chunk:', { + message: chunkErr.message, + stack: chunkErr.stack, + offset: offset, + endOffset: endOffset, + fileSize: fileSize + }); + if (client) { + client.release(); + } + if (!res.headersSent) { + res.status(500).json({ success: false, message: 'Ошибка чтения файла' }); + } else { + res.end(); + } + } + }; + + // Начинаем стриминг + streamChunk(); + + // Обработка ошибок HTTP ответа + res.on('error', (resErr) => { + console.error('[uploads/media/:id/file] Ошибка HTTP ответа:', resErr); + if (client) { + client.release(); + } + }); + + // Обработка закрытия соединения клиентом + res.on('close', () => { + if (client) { + client.release(); + } + }); + } catch (e) { - return res.status(500).json({ success: false, message: e.message }); + if (client) { + client.release(); + } + + console.error('[uploads/media/:id/file] Ошибка получения файла:', { + message: e.message, + stack: e.stack, + name: e.name, + code: e.code, + detail: e.detail, + constraint: e.constraint, + table: e.table, + column: e.column + }); + if (!res.headersSent) { + return res.status(500).json({ + success: false, + message: e.message || 'Внутренняя ошибка сервера', + error: process.env.NODE_ENV === 'development' ? { + name: e.name, + code: e.code, + detail: e.detail, + constraint: e.constraint + } : undefined + }); + } } }); diff --git a/backend/routes/vds.js b/backend/routes/vds.js index 5cc6188..19fd88f 100644 --- a/backend/routes/vds.js +++ b/backend/routes/vds.js @@ -1575,16 +1575,27 @@ router.get('/ssl/status', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT } } + logger.info(`[VDS] SSL статус проверен: найдено сертификатов: ${allCertificates.length}, домен: ${domain}`); res.json({ success: true, certificates: checkResult.stdout, allCertificates: allCertificates, domain: domain, - certInfo: certInfo + certInfo: certInfo, + hasCertificates: allCertificates.length > 0 }); } catch (error) { logger.error('[VDS] Ошибка проверки SSL сертификата:', error); - res.status(500).json({ success: false, error: error.message }); + logger.error('[VDS] Детали ошибки:', { + message: error.message, + stack: error.stack, + code: error.code + }); + res.status(500).json({ + success: false, + error: error.message, + details: process.env.NODE_ENV === 'development' ? error.stack : undefined + }); } }); diff --git a/backend/yarn.lock b/backend/yarn.lock index d9ac022..bf39702 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -5262,6 +5262,11 @@ pg-int8@1.0.1: resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== +pg-large-object@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pg-large-object/-/pg-large-object-2.0.0.tgz#19e863aa5aee64ee3735238b740b2cb250eb5b7e" + integrity sha512-SiqyK3G4Qv4WJLsSCh7N59om9Ga3t6ysAqyguTIojYG/SIU+YO1wQIRp7SM4Bov3sikGtRP8GZ9iqDJv8h/xXA== + pg-pool@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.10.1.tgz#481047c720be2d624792100cac1816f8850d31b2" diff --git a/docs/application-overview.md b/docs/application-overview.md new file mode 100644 index 0000000..ef68549 --- /dev/null +++ b/docs/application-overview.md @@ -0,0 +1,884 @@ + + +# Обзор приложения Digital Legal Entity (DLE) + +## 📋 Содержание + +1. [Установка приложения](#установка-приложения) +2. [Настройка безопасности](#настройка-безопасности) +3. [Модули и микросервисная архитектура](#модули-и-микросервисная-архитектура) +4. [Корпоративный мессенджер с ИИ ассистентом](#корпоративный-мессенджер-с-ии-ассистентом) +5. [Настройки приложения для работы с интернет пользователями](#настройки-приложения-для-работы-с-интернет-пользователями) +6. [Требования регулятора](#требования-регулятора) +7. [Обновления софта](#обновления-софта) + +--- + +## 🚀 Установка приложения + +### Быстрый старт + +Digital Legal Entity (DLE) разворачивается с помощью Docker Compose, что обеспечивает простую и надежную установку на любой платформе. + +### Системные требования + +**Минимальные требования**: +- **CPU**: 4 ядра +- **RAM**: 12 GB (4 GB приложение + 6 GB AI + 2 GB Vector Search) +- **Хранилище**: 100 GB SSD +- **ОС**: Ubuntu 20.04+, Debian 11+, CentOS 8+, любая Linux с Docker +- **Docker**: версия 20.10+ +- **Docker Compose**: версия 2.0+ + +### Процесс установки + +Для Linux/macOS/WSL: +```bash +curl -fsSL https://raw.githubusercontent.com/VC-HB3-Accelerator/DLE/main/setup.sh | bash +``` + +Скрипт автоматически: +- Скачивает последние артефакты из релиза +- Разворачивает `docker-data` +- Настраивает необходимые конфигурации + + +### Компоненты системы + +После установки запускаются следующие сервисы: + +| Сервис | Описание | Порт | +|--------|----------|------| +| **PostgreSQL** | База данных с расширением pgvector | Внутренний | +| **Ollama** | Локальный AI сервер | Внутренний (11434) | +| **Vector Search** | Сервис векторного поиска (RAG) | Внутренний (8001) | +| **Backend** | Node.js API сервер | Внутренний (8000) | +| **Frontend (Nginx)** | Веб-интерфейс | 9000 (HTTP) + +### Доступ к приложению + +После успешного запуска приложение доступно по адресу: +- **Production**: `http://localhost:9000` (HTTP) + +### Первоначальная настройка + +1. Откройте приложение в браузере +2. Подключите крипто-кошелек (MetaMask, WalletConnect) +3. Настройте RPC провайдеры в разделе **Настройки → Безопасность** +4. Настройте смарт-контракты в разделе **Настройки → Блокчейн** + +> 💡 **Подробная инструкция**: См. [Инструкция по установке](./setup-instruction.md) для пошаговой настройки всех компонентов. + +--- + +## 🔒 Настройка безопасности + +### Многоуровневая модель безопасности + +DLE использует комплексный подход к безопасности на всех уровнях архитектуры: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Уровни защиты DLE │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Уровень 1: Блокчейн (Неизменяемая база) │ +│ • Смарт-контракт DLE (проверен, иммутабельный) │ +│ • Токены управления (ERC20Votes) │ +│ • История всех операций на блокчейне │ +│ • Невозможность изменения правил без голосования │ +│ │ +│ Уровень 2: Веб-приложение (Backend) │ +│ • Проверка токенов в реальном времени │ +│ • Аутентификация через кошелек (SIWE) │ +│ • Шифрование данных (AES-256) │ +│ • Rate limiting и защита от DDoS │ +│ │ +│ Уровень 3: Frontend (Vue.js) │ +│ • Подключение к кошельку │ +│ • Подпись транзакций │ +│ • XSS защита (DOMPurify) │ +│ • CSRF токены │ +│ │ +│ Уровень 4: Пользователь │ +│ • Приватный ключ кошелька (MetaMask, WalletConnect) │ +│ • Подтверждение каждой операции │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Настройка безопасности + +#### 1. Настройка RPC провайдеров + +**Путь**: Настройки → Безопасность → RPC провайдеры + +Для каждой блокчейн-сети необходимо добавить RPC провайдера: +- **Network Name**: Название сети (Ethereum, Polygon, BSC и т.д.) +- **RPC URL**: URL подключения (например: `https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY`) +- **Network ID**: Chain ID сети + +> ⚠️ **Важно**: Получите API ключи от провайдеров (Alchemy, Infura, Quicknode и т.д.) перед добавлением. + +#### 2. Настройка смарт-контрактов + +**Путь**: Настройки → Блокчейн + +Настройте адреса смарт-контрактов для каждой сети: +- **Factory Address**: Адрес фабрики контрактов +- **Core Contract Address**: Адрес основного контракта DLE + +#### 3. Настройка шифрования + +Все чувствительные данные шифруются с использованием AES-256: +- Персональные данные пользователей +- Идентификаторы гостей +- Приватные сообщения +- Ключи шифрования хранятся в защищенном хранилище + +#### 4. Управление доступом + +Система ролей и разрешений: +- **Guest**: Базовый доступ, чат с AI +- **User**: Полный доступ к коммуникациям +- **ReadOnly**: Просмотр данных без редактирования +- **Editor**: Полный доступ ко всем функциям + +#### 5. Аудит и мониторинг + +- Все действия логируются +- Audit trail для критичных операций +- Мониторинг подозрительной активности +- Уведомления о важных событиях + +> 💡 **Подробная информация**: См. [Безопасность DLE](./security.md) для детального описания всех уровней защиты, сценариев атак и рекомендаций. + +--- + +## 🏗️ Модули и микросервисная архитектура + +### Архитектура системы + +DLE построен на принципах микросервисной архитектуры, что обеспечивает: +- ✅ Легкое масштабирование отдельных компонентов +- ✅ Независимое развертывание сервисов +- ✅ Высокую отказоустойчивость +- ✅ Гибкость в разработке и поддержке + +### Основные сервисы + +#### 1. PostgreSQL (База данных) +- **Назначение**: Хранение всех данных приложения +- **Особенности**: Расширение pgvector для векторного поиска +- **Масштабирование**: Репликация, шардирование + +#### 2. Ollama (AI сервер) +- **Назначение**: Локальный AI для генерации ответов +- **Модель**: qwen2.5:7b (настраивается) +- **Особенности**: 100% конфиденциальность, работа offline +- **Ресурсы**: 6 GB RAM, 2 CPU cores + +#### 3. Vector Search (RAG сервис) +- **Назначение**: Векторный поиск для контекстных ответов AI +- **Технология**: FAISS, pgvector +- **Модель эмбеддингов**: mxbai-embed-large:latest +- **Особенности**: Семантический поиск по документам + +#### 4. Backend (Node.js API) +- **Назначение**: Основной API сервер +- **Технологии**: Express.js, WebSocket +- **Функции**: + - Обработка запросов + - Управление сообщениями + - Интеграция с блокчейном + - Управление пользователями + +#### 5. Frontend (Vue.js) +- **Назначение**: Пользовательский интерфейс +- **Технологии**: Vue 3, Vite, Element Plus +- **Особенности**: SPA, адаптивный дизайн + +### Модульная система + +DLE поддерживает расширение функциональности через модули: + +#### Доступные модули + +1. **HierarchicalVotingModule** + - Иерархическое голосование + - Делегирование голосов + - Сложные сценарии управления + +2. **Custom Modules** + - Возможность добавления собственных модулей + - Интеграция через смарт-контракты + - Управление через голосование токен-холдеров + +#### Добавление модулей + +Модули добавляются через: +1. Голосование токен-холдеров +2. Деплой смарт-контракта модуля +3. Регистрация в основном контракте DLE + +### Коммуникация между сервисами + +``` +┌─────────────┐ +│ Frontend │ +│ (Vue.js) │ +└──────┬──────┘ + │ HTTP/WebSocket + ▼ +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ Backend │──────│ PostgreSQL │ │ Ollama │ +│ (Node.js) │ │ (pgvector) │ │ (AI Server)│ +└──────┬──────┘ └──────────────┘ └─────────────┘ + │ + │ HTTP + ▼ +┌─────────────┐ +│Vector Search│ +│ (RAG) │ +└─────────────┘ +``` + +### Масштабирование + +Каждый сервис может масштабироваться независимо: +- **Горизонтальное масштабирование**: Добавление инстансов +- **Вертикальное масштабирование**: Увеличение ресурсов +- **Балансировка нагрузки**: Nginx, HAProxy + +> 💡 **Подробная информация**: См. [Техническая документация по блокчейну](./blockchain-integration-technical.md) для информации о модульной системе смарт-контрактов. + +--- + +## 💬 Корпоративный мессенджер с ИИ ассистентом + +### Обзор функциональности + +DLE включает полнофункциональный корпоративный мессенджер с интегрированным AI ассистентом, который обеспечивает: +- Единый интерфейс для всех каналов коммуникации +- Автоматические ответы с помощью AI +- Контекстное понимание истории общения +- Многоканальную поддержку клиентов + +### Каналы коммуникации + +#### 1. Веб-чат +- Публичный чат на сайте +- Приватные сообщения между пользователями +- Административный чат для связи с клиентами + +#### 2. Telegram бот +- Интеграция с Telegram +- Автоматические ответы через AI +- Синхронизация с веб-интерфейсом + +#### 3. Email +- Обработка входящих писем +- Автоматические ответы +- Интеграция с CRM + + +### ИИ ассистент + +#### Возможности AI ассистента + +**Локальный AI на вашем сервере**: +- Модель: qwen2.5:7b (настраивается) +- Технология: Ollama + Vector Search (RAG) +- Конфиденциальность: 100% (данные не покидают сервер) +- Стоимость: $0 (без лимитов на запросы) + +#### Функции AI ассистента + +1. **Автоматические ответы** + - Генерация ответов на основе контекста + - Обучение на ваших документах + - Персонализация под ваш бизнес + +2. **Анализ настроения** + - Определение эмоционального тона сообщений + - Приоритизация запросов + - Эскалация критичных ситуаций + +3. **Контекстный поиск** + - Поиск информации в базе знаний + - Использование истории общения + - Ссылки на релевантные документы + +4. **Многозадачность** + - Обработка нескольких запросов одновременно + - Очередь запросов + - Приоритизация важных сообщений + +#### Настройка AI ассистента + +**Путь**: Настройки → AI Ассистент + +1. **Базовая настройка** + - Выбор модели AI + - Настройка температуры генерации + - Лимиты токенов + +2. **Правила и контекст** + - Добавление правил поведения + - Загрузка документов для обучения + - Настройка тона общения + +3. **Векторный поиск** + - Индексация документов + - Настройка релевантности + - Обновление базы знаний + +### Типы чатов + +#### 1. Публичный чат +- Доступен всем пользователям +- AI может отвечать автоматически +- Модерация сообщений + +#### 2. Приватный чат +- Личные сообщения между пользователями +- Шифрование контента +- История переписки + +#### 3. Административный чат +- Связь администраторов с клиентами +- Управление через веб-интерфейс +- Интеграция с CRM + +#### 4. Гостевой чат +- Для неавторизованных пользователей +- Ограниченный функционал +- Требуется согласие на обработку данных + +### Управление сообщениями + +- **История**: Полная история всех сообщений +- **Поиск**: Поиск по содержимому, отправителю, дате +- **Фильтры**: По каналам, типам, статусам +- **Экспорт**: Выгрузка переписки в различных форматах + +> 💡 **Подробная информация**: См. [AI Ассистент](./ai-assistant.md) для полного описания возможностей и [Настройка AI ассистента](./setup-ai-assistant.md) для инструкций по настройке. + +--- + +## 🌐 Настройки приложения для работы с интернет пользователями + +### Работа с гостями (неавторизованными пользователями) + +DLE поддерживает работу с интернет-пользователями без обязательной регистрации, обеспечивая при этом соответствие требованиям регуляторов. + +#### Роли и права доступа + +**Guest (Гость)**: +- ✅ Просмотр главной страницы +- ✅ Чат с AI ассистентом +- ❌ Отправка сообщений пользователям +- ❌ Доступ к CRM и данным + +**User (Пользователь)**: +- ✅ Все права гостя +- ✅ Получение сообщений +- ✅ Отправка сообщений другим пользователям +- ✅ Просмотр контактов +- ✅ Приватный чат с администраторами +- ✅ Просмотр базовых документов + +#### Настройка гостевого доступа + +**Путь**: Настройки → Пользователи → Гостевой доступ + +1. **Включение гостевого режима** + - Разрешить доступ без регистрации + - Настройка ограничений + - Лимиты на использование + +2. **Сбор согласий** + - Автоматический запрос согласия на обработку ПД + - Политика конфиденциальности + - Пользовательское соглашение + - Политика использования cookies + +3. **Идентификация гостей** + - Уникальные идентификаторы + - Шифрование данных + - Анонимизация при необходимости + +### Обработка персональных данных + +#### Согласие на обработку ПД + +При первом обращении гостя система: +1. Показывает политику конфиденциальности +2. Запрашивает согласие на обработку ПД +3. Сохраняет факт согласия с временной меткой +4. Предоставляет доступ к функциям + +#### Управление согласиями + +**Путь**: Настройки → Контент → Юридические документы + +- Просмотр всех согласий +- Экспорт данных по запросу +- Удаление данных при отзыве согласия +- История изменений согласий + +### Публичные документы + +#### Обязательные документы для публичного доступа + +1. **Политика конфиденциальности** + - Описание обработки ПД + - Права пользователей + - Контакты для обращений + +2. **Пользовательское соглашение** + - Условия использования сервиса + - Права и обязанности + - Ограничения ответственности + +3. **Согласие на обработку персональных данных** + - Форма согласия + - Информация о целях обработки + - Возможность отзыва + +4. **Политика использования cookies** + - Типы используемых cookies + - Цели использования + - Управление настройками + +#### Публикация документов + +**Путь**: Контент → Шаблоны + +1. Выберите необходимые шаблоны +2. Предварительный просмотр с автозаполнением +3. Редактирование специфичных параметров +4. Публикация: + - **Публичное использование**: Доступно на сайте + - **Внутреннее использование**: Только в CRM + - **Печать**: Экспорт в PDF + +### Настройки веб-интерфейса + +#### Публичные страницы + +- **Главная страница**: Информация о сервисе +- **Чат**: Публичный чат с AI +- **Контакты**: Форма обратной связи +- **Документы**: Публичные юридические документы + +#### Интеграция с сайтом + +DLE может быть интегрирован на ваш сайт: +- Виджет чата +- Формы обратной связи +- API для кастомных решений + +### Управление контактами + +#### Автоматическое создание контактов + +При обращении гостя система: +1. Создает контакт в CRM +2. Присваивает уникальный идентификатор +3. Сохраняет канал коммуникации +4. Записывает историю взаимодействий + +#### Объединение контактов + +Если один пользователь обращается через разные каналы: +- Автоматическое определение дубликатов +- Объединение в единый контакт +- Единая история коммуникаций + +> 💡 **Важно**: Все настройки для работы с интернет-пользователями должны соответствовать требованиям регуляторов (см. раздел "Требования регулятора"). + +--- + +## ⚖️ Требования регулятора + +### Соответствие законодательству + +DLE разработан с учетом требований основных регуляторов по защите персональных данных и обеспечению прозрачности работы с пользователями. + +### GDPR (General Data Protection Regulation) + +**Европейское законодательство о защите данных** + +#### Основные требования и их реализация: + +1. **Право на информацию** ✅ + - Политика конфиденциальности доступна на сайте + - Прозрачная информация о целях обработки данных + - Контакты контролера данных + +2. **Право на доступ** ✅ + - Пользователи могут запросить копию своих данных + - Экспорт данных в структурированном формате + - История обработки данных + +3. **Право на исправление** ✅ + - Редактирование персональных данных + - Обновление информации в профиле + - Корректировка неточных данных + +4. **Право на удаление ("право быть забытым")** ✅ + - Удаление данных по запросу + - Отзыв согласия на обработку + - Полное удаление из системы + +5. **Право на ограничение обработки** ✅ + - Временная блокировка обработки + - Сохранение данных без использования + - Уведомление о снятии ограничений + +6. **Право на переносимость данных** ✅ + - Экспорт данных в машиночитаемом формате + - Передача данных другому контролеру + - Структурированные форматы (JSON, CSV) + +7. **Право на возражение** ✅ + - Отказ от обработки данных + - Отзыв согласия + - Остановка маркетинговых рассылок + +8. **Автоматизированное принятие решений** ✅ + - Прозрачность использования AI + - Возможность человеческого вмешательства + - Объяснение логики решений + +### CCPA (California Consumer Privacy Act) + +**Калифорнийский закон о защите конфиденциальности** + +#### Основные требования: + +1. **Право знать** ✅ + - Информация о собираемых данных + - Цели использования данных + - Категории третьих сторон + +2. **Право на удаление** ✅ + - Удаление персональных данных + - Исключения для бизнес-нужд + - Подтверждение удаления + +3. **Право на отказ от продажи** ✅ + - DLE не продает данные пользователей + - Прозрачная политика + - Механизм отказа + +4. **Недискриминация** ✅ + - Равный доступ к сервису + - Отсутствие штрафов за использование прав + - Справедливое ценообразование + +### 152-ФЗ (Российское законодательство) + +**Федеральный закон "О персональных данных"** + +#### Основные требования: + +1. **Согласие на обработку** ✅ + - Явное согласие субъекта ПД + - Информированное согласие + - Возможность отзыва + +2. **Уведомление Роскомнадзора** ✅ + - Документация для уведомления + - Описание целей обработки + - Меры безопасности + +3. **Локализация данных** ✅ + - Хранение на серверах в РФ (опционально) + - Контроль местоположения данных + - Соответствие требованиям + +4. **Права субъектов ПД** ✅ + - Доступ к данным + - Исправление данных + - Удаление данных + - Отзыв согласия + +5. **Меры безопасности** ✅ + - Шифрование данных + - Контроль доступа + - Аудит действий + - Резервное копирование + +### Готовые документы для регулятора + +DLE включает готовый пакет документов, необходимых для соответствия требованиям: + +#### 1. Политика конфиденциальности +- Описание обработки ПД +- Права пользователей +- Контакты контролера +- Механизмы реализации прав + +#### 2. Пользовательское соглашение +- Условия использования +- Права и обязанности сторон +- Ограничения ответственности +- Разрешение споров + +#### 3. Согласие на обработку персональных данных +- Форма согласия +- Цели обработки +- Сроки хранения +- Право отзыва + +#### 4. Политика использования cookies +- Типы cookies +- Цели использования +- Управление настройками +- Отключение cookies + +#### 5. Документация для Роскомнадзора +- Описание системы обработки ПД +- Меры безопасности +- Технические характеристики +- Процедуры обработки запросов + +### Аудит и соответствие + +#### Регулярные проверки + +- Проверка актуальности документов +- Обновление политик при изменении законодательства +- Аудит обработки данных +- Тестирование механизмов реализации прав + +#### Рекомендации + +1. **Регулярное обновление** + - Следите за изменениями в законодательстве + - Обновляйте документы при необходимости + - Проводите аудит соответствия + +2. **Консультации** + - Рекомендуется консультация с юристом + - Проверка соответствия локальному законодательству + - Адаптация под специфику вашего региона + +3. **Документирование** + - Ведите журнал обработки данных + - Сохраняйте историю согласий + - Документируйте запросы пользователей + +> 💡 **Подробная информация**: См. [Безопасность DLE](./security.md) для детального описания мер безопасности и соответствия требованиям регуляторов. + +--- + +## 🔄 Обновления софта + +### Политика обновлений + +DLE предоставляет бесплатные обновления в течение 5 лет для держателей лицензионных токенов. Это включает: +- ✅ Исправления ошибок (bug fixes) +- ✅ Новые функции (features) +- ✅ Улучшения безопасности (security updates) +- ✅ Обновления зависимостей (dependencies) +- ✅ Оптимизация производительности + +### Типы обновлений + +#### 1. Патч-обновления (Patch) +- **Формат версии**: X.Y.**Z** (например, 1.0.1 → 1.0.2) +- **Содержание**: Исправления ошибок, мелкие улучшения +- **Частота**: По мере необходимости +- **Критичность**: Обычно низкая-средняя + +#### 2. Минорные обновления (Minor) +- **Формат версии**: X.**Y**.Z (например, 1.0.2 → 1.1.0) +- **Содержание**: Новые функции, улучшения +- **Частота**: Ежеквартально или по мере готовности +- **Критичность**: Средняя + +#### 3. Мажорные обновления (Major) +- **Формат версии**: **X**.Y.Z (например, 1.1.0 → 2.0.0) +- **Содержание**: Крупные изменения, breaking changes +- **Частота**: Раз в год или реже +- **Критичность**: Высокая (требуется миграция) + +### Процесс обновления + +#### Автоматическое обновление (рекомендуется) + +```bash +# 1. Остановка текущей версии +docker-compose down + +# 2. Резервное копирование данных +docker run --rm -v dle_postgres_data:/data -v $(pwd):/backup \ + alpine tar czf /backup/postgres_backup.tar.gz -C /data . + +# 3. Обновление кода +git pull origin main + +# 4. Обновление образов +docker-compose pull + +# 5. Пересборка (если необходимо) +docker-compose build + +# 6. Запуск обновленной версии +docker-compose up -d + +# 7. Проверка статуса +docker-compose ps +docker-compose logs -f +``` + +#### Ручное обновление + +1. **Подготовка** + - Проверьте текущую версию + - Изучите changelog обновления + - Создайте резервную копию + +2. **Резервное копирование** + ```bash + # База данных + docker-compose exec postgres pg_dump -U dapp_user dapp_db > backup.sql + + # Файлы загрузок + tar -czf uploads_backup.tar.gz backend/uploads/ + + # Конфигурации + cp .env .env.backup + ``` + +3. **Обновление** + - Скачайте новую версию + - Обновите зависимости + - Примените миграции БД (если есть) + +4. **Проверка** + - Проверьте работоспособность + - Протестируйте ключевые функции + - Проверьте логи на ошибки + +### Миграции базы данных + +При обновлении могут потребоваться миграции БД: + +```bash +# Автоматическое применение миграций +docker-compose exec backend npm run migrate + +# Или вручную через SQL +docker-compose exec postgres psql -U dapp_user -d dapp_db -f migrations/version_X.X.X.sql +``` + +### Откат обновления + +В случае проблем можно откатиться к предыдущей версии: + +```bash +# 1. Остановка текущей версии +docker-compose down + +# 2. Восстановление из резервной копии +docker run --rm -v dle_postgres_data:/data -v $(pwd):/backup \ + alpine tar xzf /backup/postgres_backup.tar.gz -C /data + +# 3. Переключение на предыдущую версию +git checkout + +# 4. Запуск предыдущей версии +docker-compose up -d +``` + +### Уведомления об обновлениях + +#### Каналы уведомлений + +1. **GitHub Releases** + - Официальные релизы + - Changelog изменений + - Инструкции по обновлению + +2. **Email рассылка** + - Уведомления о важных обновлениях + - Информация о breaking changes + - Рекомендации по обновлению + +3. **В приложении** + - Уведомления о доступных обновлениях + - Информация о новых функциях + - Ссылки на документацию + +### Рекомендации по обновлениям + +1. **Регулярность** + - Обновляйтесь регулярно для получения исправлений безопасности + - Не откладывайте критичные обновления + - Следите за changelog + +2. **Тестирование** + - Тестируйте обновления на тестовой среде + - Проверяйте совместимость с вашими данными + - Убедитесь в работоспособности интеграций + +3. **Резервное копирование** + - Всегда создавайте резервные копии перед обновлением + - Храните несколько версий бэкапов + - Тестируйте восстановление из бэкапа + +4. **Документирование** + - Ведите журнал обновлений + - Записывайте возникшие проблемы + - Документируйте кастомные изменения + +### Долгосрочная поддержка (LTS) + +Для стабильных версий может быть доступна долгосрочная поддержка: +- Расширенный период поддержки +- Приоритетные исправления безопасности +- Гарантированная совместимость + +> 💡 **Важно**: Обновления предоставляются бесплатно в течение 5 лет для держателей лицензионных токенов. После этого периода обновления могут быть платными или доступны через сообщество. + +--- + +## 📚 Дополнительные ресурсы + +### Документация + +- [Описание приложения](./application-description.md) - Полный обзор возможностей и преимуществ +- [Инструкция по установке](./setup-instruction.md) - Пошаговая настройка +- [Настройка AI ассистента](./setup-ai-assistant.md) - Конфигурация AI +- [Безопасность DLE](./security.md) - Детальная информация о безопасности +- [FAQ](./FAQ.md) - Ответы на частые вопросы + +### Поддержка + +- **Email**: info@hb3-accelerator.com +- **Сайт**: https://hb3-accelerator.com +- **GitHub**: https://github.com/VC-HB3-Accelerator + +--- + +**© 2024-2025 Тарабанов Александр Викторович. Все права защищены.** + +**Digital Legal Entity (DLE)** - комплексное решение для управления бизнесом с блокчейн и AI. + +**Версия документа**: 1.0.0 +**Последнее обновление**: January 2025 + diff --git a/frontend/src/components/docs/DocsContent.vue b/frontend/src/components/docs/DocsContent.vue index 774ad95..c165205 100644 --- a/frontend/src/components/docs/DocsContent.vue +++ b/frontend/src/components/docs/DocsContent.vue @@ -452,6 +452,7 @@ onMounted(() => { margin: 0 auto; padding: 40px; min-height: 100%; + width: 100%; } .back-btn { @@ -713,7 +714,7 @@ onMounted(() => { max-width: 100%; width: 100%; height: auto; - min-height: 300px; + min-height: 400px; border-radius: 8px; margin: 1.5rem 0; display: block; @@ -721,6 +722,12 @@ onMounted(() => { background: #000; } +.content-text :deep(video.ql-video) { + width: 100%; + max-width: 100%; + min-height: 400px; +} + .content-text :deep(video:focus) { outline: 2px solid var(--color-primary); outline-offset: 2px; diff --git a/frontend/src/components/editor/RichTextEditor.vue b/frontend/src/components/editor/RichTextEditor.vue index 971f5e5..3aa1d2f 100644 --- a/frontend/src/components/editor/RichTextEditor.vue +++ b/frontend/src/components/editor/RichTextEditor.vue @@ -22,13 +22,23 @@ import Quill from 'quill'; import 'quill/dist/quill.snow.css'; import api from '../../api/axios'; -// Импортируем и регистрируем модуль изменения размера изображений -let ImageResize; -try { - ImageResize = require('quill-image-resize-module').default || require('quill-image-resize-module'); - Quill.register('modules/imageResize', ImageResize); -} catch (error) { - console.warn('[RichTextEditor] Не удалось загрузить модуль изменения размера изображений:', error); +// Функция для загрузки и регистрации модуля изменения размера изображений +async function loadImageResizeModule() { + try { + // Используем динамический импорт для совместимости с Vite + const module = await import('quill-image-resize-module'); + const ImageResize = module.default || module.ImageResize || module; + if (ImageResize && typeof ImageResize === 'function') { + Quill.register('modules/imageResize', ImageResize); + return true; + } else if (ImageResize && ImageResize.default && typeof ImageResize.default === 'function') { + Quill.register('modules/imageResize', ImageResize.default); + return true; + } + } catch (error) { + console.warn('[RichTextEditor] Не удалось загрузить модуль изменения размера изображений:', error); + } + return false; } const props = defineProps({ @@ -63,26 +73,36 @@ const toolbarOptions = [ ['clean'] ]; -onMounted(() => { +onMounted(async () => { if (!editorContainer.value) return; + // Загружаем модуль изменения размера изображений перед инициализацией + const imageResizeLoaded = await loadImageResizeModule(); + + // Конфигурация модулей + const modulesConfig = { + toolbar: { + container: toolbarOptions, + handlers: { + 'image': handleImageClick, + 'video': handleVideoClick + } + } + }; + + // Добавляем imageResize только если модуль загружен + if (imageResizeLoaded) { + modulesConfig.imageResize = { + parchment: Quill.import('parchment'), + modules: ['Resize', 'DisplaySize', 'Toolbar'] + }; + } + // Инициализация Quill quill = new Quill(editorContainer.value, { theme: 'snow', placeholder: props.placeholder, - modules: { - toolbar: { - container: toolbarOptions, - handlers: { - 'image': handleImageClick, - 'video': handleVideoClick - } - }, - imageResize: { - parchment: Quill.import('parchment'), - modules: ['Resize', 'DisplaySize', 'Toolbar'] - } - } + modules: modulesConfig }); // Устанавливаем начальное значение @@ -368,14 +388,55 @@ defineExpose({ padding: 4px 8px; } -:deep(.ql-snow img), -:deep(.ql-snow video) { +:deep(.ql-snow img) { max-width: 100%; height: auto; border-radius: 4px; margin: 10px 0; } +/* Стили для видео в редакторе - на всю ширину */ +:deep(.ql-snow video), +:deep(.ql-editor video) { + max-width: 100%; + width: 100%; + height: auto; + min-height: 400px; + border-radius: 8px; + margin: 1.5rem 0; + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #000; +} + +:deep(.ql-snow video.ql-video), +:deep(.ql-editor video.ql-video) { + width: 100%; + max-width: 100%; + min-height: 400px; +} + +/* Стили для iframe в редакторе */ +:deep(.ql-snow iframe), +:deep(.ql-editor iframe) { + max-width: 100%; + width: 100%; + height: auto; + min-height: 400px; + border-radius: 8px; + margin: 1.5rem 0; + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #000; + border: none; +} + +:deep(.ql-snow iframe.ql-video), +:deep(.ql-editor iframe.ql-video) { + min-height: 400px; + aspect-ratio: 16 / 9; +} + /* Стили для изменения размера изображений */ :deep(.ql-image-resize) { display: inline-block; @@ -394,13 +455,5 @@ defineExpose({ cursor: nwse-resize; box-shadow: 0 0 2px rgba(0, 0, 0, 0.3); } - -:deep(.ql-snow img), -:deep(.ql-snow video) { - max-width: 100%; - height: auto; - border-radius: 4px; - margin: 10px 0; -} diff --git a/frontend/src/views/ContentPageView.vue b/frontend/src/views/ContentPageView.vue index 42f8691..0fef261 100644 --- a/frontend/src/views/ContentPageView.vue +++ b/frontend/src/views/ContentPageView.vue @@ -569,6 +569,7 @@ onMounted(async () => { border: 1px solid #e9ecef; max-width: 1000px; margin: 0 auto; + width: 100%; } .form-section { @@ -635,6 +636,64 @@ onMounted(async () => { color: var(--color-grey-dark); } +/* Стили для видео в редакторе */ +.content-form :deep(video) { + max-width: 100%; + width: 100%; + height: auto; + min-height: 400px; + border-radius: 8px; + margin: 1.5rem 0; + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #000; +} + +.content-form :deep(video.ql-video) { + width: 100%; + max-width: 100%; + min-height: 400px; +} + +.content-form :deep(video:focus) { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Стили для iframe в редакторе (для внешних видео) */ +.content-form :deep(iframe) { + max-width: 100%; + width: 100%; + height: auto; + min-height: 400px; + border-radius: 8px; + margin: 1.5rem 0; + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #000; + border: none; +} + +.content-form :deep(iframe.ql-video) { + min-height: 400px; + aspect-ratio: 16 / 9; +} + +.content-form :deep(iframe:focus) { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Стили для изображений в редакторе */ +.content-form :deep(img) { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 1.5rem 0; + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + .category-select-wrapper { display: flex; gap: 10px; diff --git a/frontend/src/views/VdsManagementView.vue b/frontend/src/views/VdsManagementView.vue index 66f699f..bd2e58e 100644 --- a/frontend/src/views/VdsManagementView.vue +++ b/frontend/src/views/VdsManagementView.vue @@ -396,10 +396,16 @@

SSL сертификат

+
+ Debug: isEditor={{ isEditor }}, currentRole={{ currentRole }}, isLoadingSsl={{ isLoadingSsl }} +

⚠️ Управление SSL доступно только пользователям с ролью "Редактор"

+

+ Текущая роль: {{ currentRole }} +

@@ -407,7 +413,7 @@
Загрузка статуса SSL...
-
+
- + - {{ cert.expiryDate || 'Без данных' }} + {{ cert.expiryDate ? formatDate(cert.expiryDate) : 'Без данных' }} +
+ Домены: {{ cert.domains.join(', ') }} +
- SSL сертификат не найден для текущего домена. +

SSL сертификат не найден для текущего домена.

+

+ Домен: {{ sslStatus.domain }} +

+

+ Ошибка: {{ sslStatus.error || 'Неизвестная ошибка' }} +

@@ -441,9 +456,13 @@ class="action-btn ssl-btn renew" :disabled="isLoading" @click="renewSslCertificate" + :title="isLoading ? 'Выполняется...' : 'Получить или обновить SSL сертификат'" > 🔐 Получить / обновить SSL +
+ Кнопка скрыта: isEditor=false, currentRole={{ currentRole }} +
@@ -550,6 +569,9 @@ const router = useRouter(); const { currentRole, canManageSettings } = usePermissions(); const isEditor = computed(() => currentRole.value === ROLES.EDITOR); +// Отладочная информация (только для разработки) +const isDevelopment = computed(() => import.meta.env.DEV || import.meta.env.MODE === 'development'); + // Состояние const domain = ref(null); const isOnline = ref(false); @@ -1223,13 +1245,21 @@ const sendBackup = async () => { const loadSslStatus = async () => { if (!isEditor.value) { // Не показываем ошибку, если пользователь не редактор - просто не загружаем статус + console.log('[VDS] Пользователь не является редактором, пропускаем загрузку SSL статуса'); return; } + console.log('[VDS] Загрузка SSL статуса...'); isLoadingSsl.value = true; try { const response = await axios.get('/vds/ssl/status'); + console.log('[VDS] Ответ от /vds/ssl/status:', response.data); if (response.data.success) { sslStatus.value = response.data; + console.log('[VDS] SSL статус загружен:', { + hasCertificates: response.data.allCertificates?.length > 0, + certificatesCount: response.data.allCertificates?.length || 0, + domain: response.data.domain + }); } else { console.warn('[VDS] Получение статуса SSL не успешно:', response.data); sslStatus.value = null; @@ -1238,16 +1268,24 @@ const loadSslStatus = async () => { } } catch (error) { console.error('Ошибка получения статуса SSL:', error); + console.error('Детали ошибки:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + message: error.message + }); const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка'; // Если VDS не настроена, это нормальная ситуация - не показываем ошибку if (errorMessage.includes('VDS не настроена') || error.response?.status === 400) { + console.log('[VDS] VDS не настроена, это нормально'); sslStatus.value = null; return; } // Если ошибка аутентификации (401), это нормальная ситуация - пользователь не авторизован if (error.response?.status === 401 || errorMessage.includes('Требуется аутентификация') || errorMessage.includes('аутентификация')) { + console.log('[VDS] Ошибка аутентификации, это нормально'); sslStatus.value = null; return; } @@ -1293,26 +1331,38 @@ const checkSslStatus = async () => { }; const renewSslCertificate = async () => { + console.log('[VDS] renewSslCertificate вызвана, isEditor:', isEditor.value); if (!isEditor.value) { + console.warn('[VDS] Пользователь не является редактором, доступ запрещен'); alert('Только пользователи с ролью "Редактор" могут получать SSL сертификаты'); return; } if (!confirm('Получить/обновить SSL сертификат от Let\'s Encrypt? Это может занять некоторое время.')) { + console.log('[VDS] Пользователь отменил получение SSL сертификата'); return; } + console.log('[VDS] Начинаем получение SSL сертификата...'); isLoading.value = true; try { const response = await axios.post('/vds/ssl/renew', { sslProvider: 'letsencrypt' }); + console.log('[VDS] Ответ от /vds/ssl/renew:', response.data); if (response.data.success) { alert('SSL сертификат успешно получен/обновлен'); await loadSslStatus(); } else { + console.error('[VDS] Ошибка получения SSL сертификата:', response.data); alert('Ошибка получения SSL сертификата: ' + (response.data.error || 'Неизвестная ошибка')); } } catch (error) { console.error('Ошибка получения SSL сертификата:', error); + console.error('Детали ошибки:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + message: error.message + }); const errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка'; const errorDetails = error.response?.data?.details || ''; @@ -1519,6 +1569,7 @@ const updateCharts = () => { // Жизненный цикл onMounted(async () => { + console.log('[VDS] Компонент монтирован, isEditor:', isEditor.value, 'currentRole:', currentRole.value); await loadSettings(); await loadContainers(); await initCharts(); @@ -1526,8 +1577,11 @@ onMounted(async () => { // Загружаем пользователей только для редакторов if (isEditor.value) { + console.log('[VDS] Пользователь является редактором, загружаем пользователей и SSL статус'); await loadUsers(); await loadSslStatus(); + } else { + console.log('[VDS] Пользователь НЕ является редактором, пропускаем загрузку пользователей и SSL'); } // Обновляем статистику каждые 5 секунд diff --git a/frontend/src/views/content/PageView.vue b/frontend/src/views/content/PageView.vue index 2b7e916..19d9178 100644 --- a/frontend/src/views/content/PageView.vue +++ b/frontend/src/views/content/PageView.vue @@ -239,10 +239,27 @@ function getStatusText(status) { } function formatContent(content) { - // Простое форматирование контента + // Форматирование контента if (!content) return ''; - // Заменяем переносы строк на
+ // Если контент уже содержит HTML теги (например, из RichTextEditor), обрабатываем его + if (/<[a-z][\s\S]*>/i.test(content)) { + // Преобразуем iframe с локальными видео-файлами обратно в тег video + // Quill может преобразовывать video в iframe, но для локальных файлов нужен тег video + content = content.replace(/]*?)src=["']([^"']+)["']([^>]*?)><\/iframe>/gi, (match, attrs1, url, attrs2) => { + // Проверяем, является ли это видео-файл из нашей системы + if (url.includes('/api/uploads/media/') && url.includes('/file')) { + // Преобразуем в тег video для локальных видео-файлов + return ``; + } + // Оставляем iframe для внешних видео (YouTube, Vimeo и т.д.) + return match; + }); + + return content; + } + + // Иначе заменяем переносы строк на
return content.replace(/\n/g, '
'); } @@ -349,6 +366,8 @@ onMounted(() => { border-radius: var(--radius-lg); padding: 25px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); + width: 100%; + max-width: 100%; } .page-content { @@ -356,6 +375,8 @@ onMounted(() => { border-radius: var(--radius-sm); padding: 25px; border: 1px solid #e9ecef; + width: 100%; + max-width: 100%; } .content-section { @@ -364,6 +385,8 @@ onMounted(() => { padding: 25px; margin-bottom: 20px; border: 1px solid #e9ecef; + width: 100%; + max-width: 100%; } .content-section:last-child { @@ -387,6 +410,64 @@ onMounted(() => { color: #333; } +/* Стили для видео в контенте */ +.main-content :deep(video) { + max-width: 100%; + width: 100%; + height: auto; + min-height: 400px; + border-radius: 8px; + margin: 1.5rem 0; + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #000; +} + +.main-content :deep(video.ql-video) { + width: 100%; + max-width: 100%; + min-height: 400px; +} + +.main-content :deep(video:focus) { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Стили для iframe в контенте (для внешних видео) */ +.main-content :deep(iframe) { + max-width: 100%; + width: 100%; + height: auto; + min-height: 400px; + border-radius: 8px; + margin: 1.5rem 0; + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: #000; + border: none; +} + +.main-content :deep(iframe.ql-video) { + min-height: 400px; + aspect-ratio: 16 / 9; +} + +.main-content :deep(iframe:focus) { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* Стили для изображений в контенте */ +.main-content :deep(img) { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 1.5rem 0; + display: block; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + .file-preview { display: flex; flex-direction: column; gap: 12px; } .pdf-embed { width: 100%; height: 70vh; border: 1px solid #e9ecef; border-radius: var(--radius-sm); } .image-preview { max-width: 100%; border: 1px solid #e9ecef; border-radius: var(--radius-sm); } diff --git a/webssh-agent/docker-compose.prod.yml b/webssh-agent/docker-compose.prod.yml index 6517483..2b9dcbe 100644 --- a/webssh-agent/docker-compose.prod.yml +++ b/webssh-agent/docker-compose.prod.yml @@ -126,6 +126,7 @@ services: vector-search: condition: service_started volumes: + - ./backend:/app - backend_node_modules:/app/node_modules - ./ssl:/app/ssl:ro # Доступ к Docker socket для управления контейнерами на VDS @@ -160,6 +161,8 @@ services: - OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-mxbai-embed-large:latest} # 🆕 Исправленный URL для Vector Search - VECTOR_SEARCH_URL=http://dapp-vector-search:8001 + # Команда запуска для production + command: ["yarn", "run", "start"] # НЕ открываем порт 8000 наружу - только nginx подключается healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:8000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]