ваше сообщение коммита

This commit is contained in:
2025-12-09 15:00:57 +03:00
parent d716e3f077
commit 13ac707844
13 changed files with 1458 additions and 62 deletions

View File

@@ -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(/:([^:@]+)@/, ':***@'));

View File

@@ -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",

View File

@@ -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);
// Подготавливаем аргументы конструктора

View File

@@ -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 pool = db.getPool();
client = await pool.connect();
const { rows } = await db.getQuery()(
'SELECT file_data, file_name, mime_type, file_size FROM content_media WHERE id = $1',
// Сначала получаем метаданные без 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
});
}
}
});

View File

@@ -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
});
}
});

View File

@@ -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"

View File

@@ -0,0 +1,884 @@
<!--
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
-->
# Обзор приложения 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 <previous-version-tag>
# 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

View File

@@ -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;

View File

@@ -22,14 +22,24 @@ import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import api from '../../api/axios';
// Импортируем и регистрируем модуль изменения размера изображений
let ImageResize;
// Функция для загрузки и регистрации модуля изменения размера изображений
async function loadImageResizeModule() {
try {
ImageResize = require('quill-image-resize-module').default || require('quill-image-resize-module');
// Используем динамический импорт для совместимости с 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({
modelValue: {
@@ -63,26 +73,36 @@ const toolbarOptions = [
['clean']
];
onMounted(() => {
onMounted(async () => {
if (!editorContainer.value) return;
// Инициализация Quill
quill = new Quill(editorContainer.value, {
theme: 'snow',
placeholder: props.placeholder,
modules: {
// Загружаем модуль изменения размера изображений перед инициализацией
const imageResizeLoaded = await loadImageResizeModule();
// Конфигурация модулей
const modulesConfig = {
toolbar: {
container: toolbarOptions,
handlers: {
'image': handleImageClick,
'video': handleVideoClick
}
},
imageResize: {
}
};
// Добавляем 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: 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;
}
</style>

View File

@@ -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;

View File

@@ -396,10 +396,16 @@
<div class="ssl-section">
<div class="section-header">
<h2>SSL сертификат</h2>
<div v-if="isDevelopment" style="font-size: 12px; color: #666; margin-top: 5px;">
Debug: isEditor={{ isEditor }}, currentRole={{ currentRole }}, isLoadingSsl={{ isLoadingSsl }}
</div>
</div>
<div v-if="!isEditor" class="access-denied-message">
<p> Управление SSL доступно только пользователям с ролью "Редактор"</p>
<p v-if="isDevelopment" style="font-size: 12px; color: #666;">
Текущая роль: {{ currentRole }}
</p>
</div>
<div v-else>
@@ -415,15 +421,24 @@
:key="cert.name"
class="ssl-info-item"
>
<label>{{ cert.name }}</label>
<label>{{ cert.name || 'Без имени' }}</label>
<span :class="{ 'expiring-soon': isCertExpiringSoon(cert.expiryDate) }">
{{ cert.expiryDate || 'Без данных' }}
{{ cert.expiryDate ? formatDate(cert.expiryDate) : 'Без данных' }}
</span>
<div v-if="cert.domains && cert.domains.length" class="ssl-domains">
Домены: {{ cert.domains.join(', ') }}
</div>
</div>
</div>
</div>
<div v-else class="ssl-no-cert">
SSL сертификат не найден для текущего домена.
<p>SSL сертификат не найден для текущего домена.</p>
<p v-if="sslStatus && sslStatus.domain" class="ssl-domain-info">
Домен: {{ sslStatus.domain }}
</p>
<p v-if="sslStatus && !sslStatus.success" class="ssl-error-info">
Ошибка: {{ sslStatus.error || 'Неизвестная ошибка' }}
</p>
</div>
</div>
</div>
@@ -441,9 +456,13 @@
class="action-btn ssl-btn renew"
:disabled="isLoading"
@click="renewSslCertificate"
:title="isLoading ? 'Выполняется...' : 'Получить или обновить SSL сертификат'"
>
🔐 Получить / обновить SSL
</button>
<div v-if="!isEditor && isDevelopment" style="font-size: 12px; color: #f00; margin-top: 5px;">
Кнопка скрыта: isEditor=false, currentRole={{ currentRole }}
</div>
</div>
</div>
</div>
@@ -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 секунд

View File

@@ -239,10 +239,27 @@ function getStatusText(status) {
}
function formatContent(content) {
// Простое форматирование контента
// Форматирование контента
if (!content) return '';
// Заменяем переносы строк на <br>
// Если контент уже содержит HTML теги (например, из RichTextEditor), обрабатываем его
if (/<[a-z][\s\S]*>/i.test(content)) {
// Преобразуем iframe с локальными видео-файлами обратно в тег video
// Quill может преобразовывать video в iframe, но для локальных файлов нужен тег video
content = content.replace(/<iframe([^>]*?)src=["']([^"']+)["']([^>]*?)><\/iframe>/gi, (match, attrs1, url, attrs2) => {
// Проверяем, является ли это видео-файл из нашей системы
if (url.includes('/api/uploads/media/') && url.includes('/file')) {
// Преобразуем в тег video для локальных видео-файлов
return `<video controls class="ql-video" style="max-width: 100%; width: 100%; height: auto; min-height: 400px; border-radius: 8px; margin: 1.5rem 0; display: block;" src="${url}"></video>`;
}
// Оставляем iframe для внешних видео (YouTube, Vimeo и т.д.)
return match;
});
return content;
}
// Иначе заменяем переносы строк на <br>
return content.replace(/\n/g, '<br>');
}
@@ -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); }

View File

@@ -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) })"]