ваше сообщение коммита
This commit is contained in:
@@ -10,10 +10,30 @@
|
|||||||
* GitHub: https://github.com/VC-HB3-Accelerator
|
* GitHub: https://github.com/VC-HB3-Accelerator
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { Pool } = require('pg');
|
const { Pool, types } = require('pg');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const axios = require('axios');
|
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('Настройки подключения к базе данных:');
|
||||||
// console.log('DATABASE_URL:', process.env.DATABASE_URL?.replace(/:([^:@]+)@/, ':***@'));
|
// console.log('DATABASE_URL:', process.env.DATABASE_URL?.replace(/:([^:@]+)@/, ':***@'));
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"openai": "^4.102.0",
|
"openai": "^4.102.0",
|
||||||
"pg": "^8.10.0",
|
"pg": "^8.10.0",
|
||||||
|
"pg-large-object": "^2.0.0",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
"session-file-store": "^1.5.0",
|
"session-file-store": "^1.5.0",
|
||||||
"siwe": "^2.1.4",
|
"siwe": "^2.1.4",
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { ethers } = require('ethers');
|
const { ethers } = require('ethers');
|
||||||
const { Interface, AbiCoder } = 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 rpcProviderService = require('../services/rpcProviderService');
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const path = require('path');
|
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}`);
|
console.log(`[DLE Modules] Текущий nonce для сети ${network.chainId}: ${currentNonce}`);
|
||||||
|
|
||||||
// Получаем фабрику контракта
|
// Получаем фабрику контракта
|
||||||
|
if (!hre) {
|
||||||
|
throw new Error('Hardhat не установлен. Установите hardhat для компиляции контрактов.');
|
||||||
|
}
|
||||||
const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName);
|
const ContractFactory = await hre.ethers.getContractFactory(moduleConfig.contractName);
|
||||||
|
|
||||||
// Подготавливаем аргументы конструктора
|
// Подготавливаем аргументы конструктора
|
||||||
|
|||||||
@@ -93,17 +93,39 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
|||||||
// Используем middleware для загрузки файла
|
// Используем middleware для загрузки файла
|
||||||
mediaUpload.single('media')(req, res, async (err) => {
|
mediaUpload.single('media')(req, res, async (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
console.error('[uploads/media] Ошибка multer:', err);
|
||||||
return res.status(400).json({ success: false, message: err.message });
|
return res.status(400).json({ success: false, message: err.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 db = require('../db');
|
||||||
const mediaType = req.file.mimetype.startsWith('image/') ? 'image' : 'video';
|
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 хеш файла для дедупликации
|
// Вычисляем SHA-256 хеш файла для дедупликации
|
||||||
const fileHash = crypto.createHash('sha256').update(req.file.buffer).digest('hex');
|
const fileHash = crypto.createHash('sha256').update(req.file.buffer).digest('hex');
|
||||||
|
console.log('[uploads/media] Хеш файла вычислен:', fileHash.substring(0, 16) + '...');
|
||||||
|
|
||||||
// Проверяем, не загружен ли уже такой файл
|
// Проверяем, не загружен ли уже такой файл
|
||||||
const existingFile = await db.getQuery()(
|
const existingFile = await db.getQuery()(
|
||||||
@@ -116,10 +138,22 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
if (existingFile.rows.length > 0) {
|
if (existingFile.rows.length > 0) {
|
||||||
// Файл уже существует - возвращаем существующую запись
|
// Файл уже существует - возвращаем существующую запись
|
||||||
|
console.log('[uploads/media] Файл уже существует, используем существующую запись:', existingFile.rows[0].id);
|
||||||
mediaId = existingFile.rows[0].id;
|
mediaId = existingFile.rows[0].id;
|
||||||
fileName = existingFile.rows[0].file_name;
|
fileName = existingFile.rows[0].file_name;
|
||||||
} else {
|
} 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()(`
|
const { rows } = await db.getQuery()(`
|
||||||
INSERT INTO content_media (
|
INSERT INTO content_media (
|
||||||
file_data,
|
file_data,
|
||||||
@@ -140,11 +174,12 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
|||||||
fileHash,
|
fileHash,
|
||||||
mediaType,
|
mediaType,
|
||||||
req.session.address,
|
req.session.address,
|
||||||
req.body.page_id || null
|
pageId
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mediaId = rows[0].id;
|
mediaId = rows[0].id;
|
||||||
fileName = rows[0].file_name;
|
fileName = rows[0].file_name;
|
||||||
|
console.log('[uploads/media] Файл успешно сохранен в БД, ID:', mediaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL для доступа к файлу через API
|
// URL для доступа к файлу через API
|
||||||
@@ -153,6 +188,7 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
|||||||
// Это позволяет работать с разными портами (frontend на 9000, backend на 8000)
|
// Это позволяет работать с разными портами (frontend на 9000, backend на 8000)
|
||||||
const fullUrl = fileUrl;
|
const fullUrl = fileUrl;
|
||||||
|
|
||||||
|
console.log('[uploads/media] Успешная загрузка, возвращаем ответ');
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -168,39 +204,212 @@ router.post('/media', auth.requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Ошибка сохранения медиа в БД:', e);
|
console.error('[uploads/media] Ошибка сохранения медиа в БД:', {
|
||||||
return res.status(500).json({ success: false, message: e.message });
|
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) => {
|
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 {
|
try {
|
||||||
const db = require('../db');
|
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]
|
[mediaId]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (metaResult.rows.length === 0) {
|
||||||
|
client.release();
|
||||||
return res.status(404).json({ success: false, message: 'Медиа-файл не найден' });
|
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-Type', media.mime_type);
|
||||||
res.setHeader('Content-Length', media.file_size);
|
res.setHeader('Accept-Ranges', 'bytes'); // Указываем, что поддерживаем Range requests
|
||||||
res.setHeader('Content-Disposition', `inline; filename="${media.file_name}"`);
|
res.setHeader('Content-Length', contentLength);
|
||||||
res.setHeader('Cache-Control', 'public, max-age=31536000'); // Кеширование на 1 год
|
res.setHeader('Cache-Control', 'public, max-age=31536000'); // Кеширование на 1 год
|
||||||
|
|
||||||
// Отправляем бинарные данные
|
if (range) {
|
||||||
res.send(media.file_data);
|
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) {
|
} 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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1575,16 +1575,27 @@ router.get('/ssl/status', requireAuth, requirePermission(PERMISSIONS.MANAGE_SETT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(`[VDS] SSL статус проверен: найдено сертификатов: ${allCertificates.length}, домен: ${domain}`);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
certificates: checkResult.stdout,
|
certificates: checkResult.stdout,
|
||||||
allCertificates: allCertificates,
|
allCertificates: allCertificates,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
certInfo: certInfo
|
certInfo: certInfo,
|
||||||
|
hasCertificates: allCertificates.length > 0
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[VDS] Ошибка проверки SSL сертификата:', 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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5262,6 +5262,11 @@ pg-int8@1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
|
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
|
||||||
integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
|
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:
|
pg-pool@^3.10.1:
|
||||||
version "3.10.1"
|
version "3.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.10.1.tgz#481047c720be2d624792100cac1816f8850d31b2"
|
resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.10.1.tgz#481047c720be2d624792100cac1816f8850d31b2"
|
||||||
|
|||||||
884
docs/application-overview.md
Normal file
884
docs/application-overview.md
Normal 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
|
||||||
|
|
||||||
@@ -452,6 +452,7 @@ onMounted(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
@@ -713,7 +714,7 @@ onMounted(() => {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 300px;
|
min-height: 400px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem 0;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -721,6 +722,12 @@ onMounted(() => {
|
|||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-text :deep(video.ql-video) {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
.content-text :deep(video:focus) {
|
.content-text :deep(video:focus) {
|
||||||
outline: 2px solid var(--color-primary);
|
outline: 2px solid var(--color-primary);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
|
|||||||
@@ -22,13 +22,23 @@ import Quill from 'quill';
|
|||||||
import 'quill/dist/quill.snow.css';
|
import 'quill/dist/quill.snow.css';
|
||||||
import api from '../../api/axios';
|
import api from '../../api/axios';
|
||||||
|
|
||||||
// Импортируем и регистрируем модуль изменения размера изображений
|
// Функция для загрузки и регистрации модуля изменения размера изображений
|
||||||
let ImageResize;
|
async function loadImageResizeModule() {
|
||||||
try {
|
try {
|
||||||
ImageResize = require('quill-image-resize-module').default || require('quill-image-resize-module');
|
// Используем динамический импорт для совместимости с Vite
|
||||||
Quill.register('modules/imageResize', ImageResize);
|
const module = await import('quill-image-resize-module');
|
||||||
} catch (error) {
|
const ImageResize = module.default || module.ImageResize || module;
|
||||||
console.warn('[RichTextEditor] Не удалось загрузить модуль изменения размера изображений:', error);
|
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({
|
const props = defineProps({
|
||||||
@@ -63,26 +73,36 @@ const toolbarOptions = [
|
|||||||
['clean']
|
['clean']
|
||||||
];
|
];
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
if (!editorContainer.value) return;
|
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
|
||||||
quill = new Quill(editorContainer.value, {
|
quill = new Quill(editorContainer.value, {
|
||||||
theme: 'snow',
|
theme: 'snow',
|
||||||
placeholder: props.placeholder,
|
placeholder: props.placeholder,
|
||||||
modules: {
|
modules: modulesConfig
|
||||||
toolbar: {
|
|
||||||
container: toolbarOptions,
|
|
||||||
handlers: {
|
|
||||||
'image': handleImageClick,
|
|
||||||
'video': handleVideoClick
|
|
||||||
}
|
|
||||||
},
|
|
||||||
imageResize: {
|
|
||||||
parchment: Quill.import('parchment'),
|
|
||||||
modules: ['Resize', 'DisplaySize', 'Toolbar']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Устанавливаем начальное значение
|
// Устанавливаем начальное значение
|
||||||
@@ -368,14 +388,55 @@ defineExpose({
|
|||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ql-snow img),
|
:deep(.ql-snow img) {
|
||||||
:deep(.ql-snow video) {
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin: 10px 0;
|
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) {
|
:deep(.ql-image-resize) {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -394,13 +455,5 @@ defineExpose({
|
|||||||
cursor: nwse-resize;
|
cursor: nwse-resize;
|
||||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
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>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -569,6 +569,7 @@ onMounted(async () => {
|
|||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section {
|
.form-section {
|
||||||
@@ -635,6 +636,64 @@ onMounted(async () => {
|
|||||||
color: var(--color-grey-dark);
|
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 {
|
.category-select-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -396,10 +396,16 @@
|
|||||||
<div class="ssl-section">
|
<div class="ssl-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>SSL сертификат</h2>
|
<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>
|
||||||
|
|
||||||
<div v-if="!isEditor" class="access-denied-message">
|
<div v-if="!isEditor" class="access-denied-message">
|
||||||
<p>⚠️ Управление SSL доступно только пользователям с ролью "Редактор"</p>
|
<p>⚠️ Управление SSL доступно только пользователям с ролью "Редактор"</p>
|
||||||
|
<p v-if="isDevelopment" style="font-size: 12px; color: #666;">
|
||||||
|
Текущая роль: {{ currentRole }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
@@ -407,7 +413,7 @@
|
|||||||
<div v-if="isLoadingSsl">
|
<div v-if="isLoadingSsl">
|
||||||
Загрузка статуса SSL...
|
Загрузка статуса SSL...
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div v-if="sslStatus && sslStatus.success && sslStatus.allCertificates && sslStatus.allCertificates.length">
|
<div v-if="sslStatus && sslStatus.success && sslStatus.allCertificates && sslStatus.allCertificates.length">
|
||||||
<div class="ssl-info">
|
<div class="ssl-info">
|
||||||
<div
|
<div
|
||||||
@@ -415,15 +421,24 @@
|
|||||||
:key="cert.name"
|
:key="cert.name"
|
||||||
class="ssl-info-item"
|
class="ssl-info-item"
|
||||||
>
|
>
|
||||||
<label>{{ cert.name }}</label>
|
<label>{{ cert.name || 'Без имени' }}</label>
|
||||||
<span :class="{ 'expiring-soon': isCertExpiringSoon(cert.expiryDate) }">
|
<span :class="{ 'expiring-soon': isCertExpiringSoon(cert.expiryDate) }">
|
||||||
{{ cert.expiryDate || 'Без данных' }}
|
{{ cert.expiryDate ? formatDate(cert.expiryDate) : 'Без данных' }}
|
||||||
</span>
|
</span>
|
||||||
|
<div v-if="cert.domains && cert.domains.length" class="ssl-domains">
|
||||||
|
Домены: {{ cert.domains.join(', ') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="ssl-no-cert">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -441,9 +456,13 @@
|
|||||||
class="action-btn ssl-btn renew"
|
class="action-btn ssl-btn renew"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
@click="renewSslCertificate"
|
@click="renewSslCertificate"
|
||||||
|
:title="isLoading ? 'Выполняется...' : 'Получить или обновить SSL сертификат'"
|
||||||
>
|
>
|
||||||
🔐 Получить / обновить SSL
|
🔐 Получить / обновить SSL
|
||||||
</button>
|
</button>
|
||||||
|
<div v-if="!isEditor && isDevelopment" style="font-size: 12px; color: #f00; margin-top: 5px;">
|
||||||
|
Кнопка скрыта: isEditor=false, currentRole={{ currentRole }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -550,6 +569,9 @@ const router = useRouter();
|
|||||||
const { currentRole, canManageSettings } = usePermissions();
|
const { currentRole, canManageSettings } = usePermissions();
|
||||||
const isEditor = computed(() => currentRole.value === ROLES.EDITOR);
|
const isEditor = computed(() => currentRole.value === ROLES.EDITOR);
|
||||||
|
|
||||||
|
// Отладочная информация (только для разработки)
|
||||||
|
const isDevelopment = computed(() => import.meta.env.DEV || import.meta.env.MODE === 'development');
|
||||||
|
|
||||||
// Состояние
|
// Состояние
|
||||||
const domain = ref(null);
|
const domain = ref(null);
|
||||||
const isOnline = ref(false);
|
const isOnline = ref(false);
|
||||||
@@ -1223,13 +1245,21 @@ const sendBackup = async () => {
|
|||||||
const loadSslStatus = async () => {
|
const loadSslStatus = async () => {
|
||||||
if (!isEditor.value) {
|
if (!isEditor.value) {
|
||||||
// Не показываем ошибку, если пользователь не редактор - просто не загружаем статус
|
// Не показываем ошибку, если пользователь не редактор - просто не загружаем статус
|
||||||
|
console.log('[VDS] Пользователь не является редактором, пропускаем загрузку SSL статуса');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log('[VDS] Загрузка SSL статуса...');
|
||||||
isLoadingSsl.value = true;
|
isLoadingSsl.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/vds/ssl/status');
|
const response = await axios.get('/vds/ssl/status');
|
||||||
|
console.log('[VDS] Ответ от /vds/ssl/status:', response.data);
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
sslStatus.value = response.data;
|
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 {
|
} else {
|
||||||
console.warn('[VDS] Получение статуса SSL не успешно:', response.data);
|
console.warn('[VDS] Получение статуса SSL не успешно:', response.data);
|
||||||
sslStatus.value = null;
|
sslStatus.value = null;
|
||||||
@@ -1238,16 +1268,24 @@ const loadSslStatus = async () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка получения статуса SSL:', 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 errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
|
||||||
|
|
||||||
// Если VDS не настроена, это нормальная ситуация - не показываем ошибку
|
// Если VDS не настроена, это нормальная ситуация - не показываем ошибку
|
||||||
if (errorMessage.includes('VDS не настроена') || error.response?.status === 400) {
|
if (errorMessage.includes('VDS не настроена') || error.response?.status === 400) {
|
||||||
|
console.log('[VDS] VDS не настроена, это нормально');
|
||||||
sslStatus.value = null;
|
sslStatus.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если ошибка аутентификации (401), это нормальная ситуация - пользователь не авторизован
|
// Если ошибка аутентификации (401), это нормальная ситуация - пользователь не авторизован
|
||||||
if (error.response?.status === 401 || errorMessage.includes('Требуется аутентификация') || errorMessage.includes('аутентификация')) {
|
if (error.response?.status === 401 || errorMessage.includes('Требуется аутентификация') || errorMessage.includes('аутентификация')) {
|
||||||
|
console.log('[VDS] Ошибка аутентификации, это нормально');
|
||||||
sslStatus.value = null;
|
sslStatus.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1293,26 +1331,38 @@ const checkSslStatus = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renewSslCertificate = async () => {
|
const renewSslCertificate = async () => {
|
||||||
|
console.log('[VDS] renewSslCertificate вызвана, isEditor:', isEditor.value);
|
||||||
if (!isEditor.value) {
|
if (!isEditor.value) {
|
||||||
|
console.warn('[VDS] Пользователь не является редактором, доступ запрещен');
|
||||||
alert('Только пользователи с ролью "Редактор" могут получать SSL сертификаты');
|
alert('Только пользователи с ролью "Редактор" могут получать SSL сертификаты');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!confirm('Получить/обновить SSL сертификат от Let\'s Encrypt? Это может занять некоторое время.')) {
|
if (!confirm('Получить/обновить SSL сертификат от Let\'s Encrypt? Это может занять некоторое время.')) {
|
||||||
|
console.log('[VDS] Пользователь отменил получение SSL сертификата');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log('[VDS] Начинаем получение SSL сертификата...');
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/vds/ssl/renew', {
|
const response = await axios.post('/vds/ssl/renew', {
|
||||||
sslProvider: 'letsencrypt'
|
sslProvider: 'letsencrypt'
|
||||||
});
|
});
|
||||||
|
console.log('[VDS] Ответ от /vds/ssl/renew:', response.data);
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
alert('SSL сертификат успешно получен/обновлен');
|
alert('SSL сертификат успешно получен/обновлен');
|
||||||
await loadSslStatus();
|
await loadSslStatus();
|
||||||
} else {
|
} else {
|
||||||
|
console.error('[VDS] Ошибка получения SSL сертификата:', response.data);
|
||||||
alert('Ошибка получения SSL сертификата: ' + (response.data.error || 'Неизвестная ошибка'));
|
alert('Ошибка получения SSL сертификата: ' + (response.data.error || 'Неизвестная ошибка'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка получения SSL сертификата:', 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 errorMessage = error.response?.data?.error || error.message || 'Неизвестная ошибка';
|
||||||
const errorDetails = error.response?.data?.details || '';
|
const errorDetails = error.response?.data?.details || '';
|
||||||
|
|
||||||
@@ -1519,6 +1569,7 @@ const updateCharts = () => {
|
|||||||
|
|
||||||
// Жизненный цикл
|
// Жизненный цикл
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
console.log('[VDS] Компонент монтирован, isEditor:', isEditor.value, 'currentRole:', currentRole.value);
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
await loadContainers();
|
await loadContainers();
|
||||||
await initCharts();
|
await initCharts();
|
||||||
@@ -1526,8 +1577,11 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// Загружаем пользователей только для редакторов
|
// Загружаем пользователей только для редакторов
|
||||||
if (isEditor.value) {
|
if (isEditor.value) {
|
||||||
|
console.log('[VDS] Пользователь является редактором, загружаем пользователей и SSL статус');
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
await loadSslStatus();
|
await loadSslStatus();
|
||||||
|
} else {
|
||||||
|
console.log('[VDS] Пользователь НЕ является редактором, пропускаем загрузку пользователей и SSL');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем статистику каждые 5 секунд
|
// Обновляем статистику каждые 5 секунд
|
||||||
|
|||||||
@@ -239,10 +239,27 @@ function getStatusText(status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatContent(content) {
|
function formatContent(content) {
|
||||||
// Простое форматирование контента
|
// Форматирование контента
|
||||||
if (!content) return '';
|
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>');
|
return content.replace(/\n/g, '<br>');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,6 +366,8 @@ onMounted(() => {
|
|||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-content {
|
.page-content {
|
||||||
@@ -356,6 +375,8 @@ onMounted(() => {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-section {
|
.content-section {
|
||||||
@@ -364,6 +385,8 @@ onMounted(() => {
|
|||||||
padding: 25px;
|
padding: 25px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-section:last-child {
|
.content-section:last-child {
|
||||||
@@ -387,6 +410,64 @@ onMounted(() => {
|
|||||||
color: #333;
|
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; }
|
.file-preview { display: flex; flex-direction: column; gap: 12px; }
|
||||||
.pdf-embed { width: 100%; height: 70vh; border: 1px solid #e9ecef; border-radius: var(--radius-sm); }
|
.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); }
|
.image-preview { max-width: 100%; border: 1px solid #e9ecef; border-radius: var(--radius-sm); }
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ services:
|
|||||||
vector-search:
|
vector-search:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
- backend_node_modules:/app/node_modules
|
- backend_node_modules:/app/node_modules
|
||||||
- ./ssl:/app/ssl:ro
|
- ./ssl:/app/ssl:ro
|
||||||
# Доступ к Docker socket для управления контейнерами на VDS
|
# Доступ к Docker socket для управления контейнерами на VDS
|
||||||
@@ -160,6 +161,8 @@ services:
|
|||||||
- OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-mxbai-embed-large:latest}
|
- OLLAMA_EMBEDDINGS_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-mxbai-embed-large:latest}
|
||||||
# 🆕 Исправленный URL для Vector Search
|
# 🆕 Исправленный URL для Vector Search
|
||||||
- VECTOR_SEARCH_URL=http://dapp-vector-search:8001
|
- VECTOR_SEARCH_URL=http://dapp-vector-search:8001
|
||||||
|
# Команда запуска для production
|
||||||
|
command: ["yarn", "run", "start"]
|
||||||
# НЕ открываем порт 8000 наружу - только nginx подключается
|
# НЕ открываем порт 8000 наружу - только nginx подключается
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:8000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:8000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
|
||||||
|
|||||||
Reference in New Issue
Block a user