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

This commit is contained in:
2025-07-27 03:30:13 +03:00
parent 057fe6254c
commit 1835632be9
141 changed files with 32514 additions and 6661 deletions

14
.gitignore vendored
View File

@@ -131,6 +131,20 @@ ssh_config
# Docker
.dockerignore
# Git tools
bfg-*.jar
# Backup files
*.bak
*.backup
*~
backups/
# Debug and temporary files
check_*.js
debug_*.js
test_*.js
# ========================================
# ПАТЕНТНЫЕ ДОКУМЕНТЫ - НЕ ПУБЛИКОВАТЬ!
# ========================================

View File

@@ -75,10 +75,16 @@ const chatRoutes = require('./routes/chat');
const adminRoutes = require('./routes/admin');
const tokensRouter = require('./routes/tokens');
const isicRoutes = require('./routes/isic'); // Добавленный импорт
const kppRoutes = require('./routes/kpp'); // Добавленный импорт КПП кодов
const geocodingRoutes = require('./routes/geocoding'); // Добавленный импорт
const dleRoutes = require('./routes/dle'); // Добавляем импорт DLE маршрутов
const dleV2Routes = require('./routes/dleV2'); // Добавляем импорт DLE v2 маршрутов
const settingsRoutes = require('./routes/settings'); // Добавляем импорт маршрутов настроек
const tablesRoutes = require('./routes/tables'); // Добавляем импорт таблиц
const countriesRoutes = require('./routes/countries'); // Добавляем импорт маршрутов стран
const russianClassifiersRoutes = require('./routes/russian-classifiers'); // Добавляем импорт российских классификаторов
const ollamaRoutes = require('./routes/ollama'); // Добавляем импорт Ollama маршрутов
const aiQueueRoutes = require('./routes/ai-queue'); // Добавляем импорт AI Queue маршрутов
const app = express();
@@ -192,9 +198,15 @@ app.use('/api/chat', chatRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/tokens', tokensRouter);
app.use('/api/isic', isicRoutes); // Добавленное использование роута
app.use('/api/kpp', kppRoutes); // Добавленное использование роута КПП кодов
app.use('/api/geocoding', geocodingRoutes); // Добавленное использование роута
app.use('/api/dle', dleRoutes); // Добавляем маршрут DLE
app.use('/api/dle-v2', dleV2Routes); // Добавляем маршрут DLE v2
app.use('/api/settings', settingsRoutes); // Добавляем маршрут настроек
app.use('/api/countries', countriesRoutes); // Добавляем маршрут стран
app.use('/api/russian-classifiers', russianClassifiersRoutes); // Добавляем маршрут российских классификаторов
app.use('/api/ollama', ollamaRoutes); // Добавляем маршрут Ollama
app.use('/api/ai-queue', aiQueueRoutes); // Добавляем маршрут AI Queue
app.use('/api/messages', messagesRoutes);
app.use('/api/identities', identitiesRoutes);
app.use('/api/rag', ragRoutes); // Подключаем роут

381
backend/contracts/DLE.sol Normal file
View File

@@ -0,0 +1,381 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import "@openzeppelin/contracts/governance/TimelockController.sol";
import "@openzeppelin/contracts/utils/Nonces.sol";
/**
* @title DLE (Digital Legal Entity)
* @dev Основной смарт-контракт DLE согласно требованиям SMART_CONTRACTS.md
*
* Функции:
* - ERC-20 токен управления с мультиподписью
* - Система голосования с кворумом
* - Казначейские функции
* - Коммуникационные функции
* - Настраиваемые таймлоки
* - Модульная система
*/
contract DLE is
ERC20Permit,
ERC20Votes,
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
// Структура для хранения информации о DLE
struct DLEInfo {
string name;
string symbol;
string location;
string[] isicCodes;
uint256 creationTimestamp;
bool isActive;
}
// Структура для предложений
struct Proposal {
bytes operation; // Операция для выполнения
uint256[] targetChains; // Целевые сети для исполнения
uint256 timelock; // Время исполнения (timestamp)
uint256 governanceChain; // Сеть где проходит голосование
address initiator; // Инициатор предложения
bytes[] signatures; // Подписи токен-холдеров
bool executed; // Статус исполнения
uint256 quorumRequired; // Требуемый кворум
uint256 signaturesCount; // Количество собранных подписей
}
// Информация о DLE
DLEInfo public dleInfo;
// Таймлок контроллер
TimelockController public timelockController;
// Маппинг предложений
mapping(uint256 => Proposal) public proposals;
uint256 public proposalCounter;
// Настройки кворума
uint256 public quorumPercentage;
// События
event DLEInitialized(
string name,
string symbol,
string location,
address tokenAddress,
address timelockAddress,
address governorAddress
);
event TokensDistributed(address[] partners, uint256[] amounts);
event ProposalCreated(uint256 proposalId, address initiator, bytes operation);
event ProposalSigned(uint256 proposalId, address signer, uint256 signaturesCount);
event ProposalExecuted(uint256 proposalId);
event ModuleInstalled(string moduleName, address moduleAddress);
/**
* @dev Конструктор DLE
* @param _name Название DLE
* @param _symbol Символ токена управления
* @param _location Местонахождение DLE
* @param _isicCodes Коды деятельности ISIC
* @param _votingDelay Задержка голосования в блоках
* @param _votingPeriod Период голосования в блоках
* @param _proposalThreshold Порог для создания предложений
* @param _quorumPercentage Процент кворума
* @param _minTimelockDelay Минимальная задержка таймлока в секундах
*/
constructor(
string memory _name,
string memory _symbol,
string memory _location,
string[] memory _isicCodes,
uint48 _votingDelay,
uint32 _votingPeriod,
uint256 _proposalThreshold,
uint256 _quorumPercentage,
uint256 _minTimelockDelay
)
ERC20(_name, _symbol)
ERC20Permit(_name)
Governor(_name)
GovernorSettings(_votingDelay, _votingPeriod, _proposalThreshold)
GovernorVotesQuorumFraction(_quorumPercentage)
{
// Инициализируем информацию о DLE
dleInfo = DLEInfo({
name: _name,
symbol: _symbol,
location: _location,
isicCodes: _isicCodes,
creationTimestamp: block.timestamp,
isActive: true
});
// Устанавливаем кворум
quorumPercentage = _quorumPercentage;
// Создаем таймлок контроллер
address[] memory proposers = new address[](1);
address[] memory executors = new address[](1);
proposers[0] = address(this); // DLE контракт может предлагать
executors[0] = address(0); // Любой может выполнять
timelockController = new TimelockController(
_minTimelockDelay,
proposers,
executors,
address(0) // Нет админа для децентрализации
);
// Отказываемся от роли админа в таймлоке
timelockController.renounceRole(timelockController.DEFAULT_ADMIN_ROLE(), address(this));
}
/**
* @dev Распределяет начальные токены между партнерами
* @param _partners Массив адресов партнеров
* @param _amounts Массив сумм токенов для каждого партнера
*/
function distributeInitialTokens(
address[] memory _partners,
uint256[] memory _amounts
) external {
require(_partners.length == _amounts.length, "Arrays length mismatch");
require(_partners.length > 0, "Empty arrays");
uint256 totalSupply = 0;
for (uint256 i = 0; i < _partners.length; i++) {
require(_partners[i] != address(0), "Zero address");
require(_amounts[i] > 0, "Zero amount");
totalSupply += _amounts[i];
_mint(_partners[i], _amounts[i]);
}
emit TokensDistributed(_partners, _amounts);
}
/**
* @dev Создает новое предложение
* @param _operation Операция для выполнения
* @param _targetChains Целевые сети для исполнения
* @param _timelockDelay Задержка таймлока
* @return proposalId ID созданного предложения
*/
function createProposal(
bytes calldata _operation,
uint256[] calldata _targetChains,
uint256 _timelockDelay
) external onlyTokenHolder returns (uint256 proposalId) {
require(_operation.length > 0, "Empty operation");
require(_targetChains.length > 0, "No target chains");
require(_timelockDelay > 0, "Invalid timelock");
proposalId = proposalCounter++;
proposals[proposalId] = Proposal({
operation: _operation,
targetChains: _targetChains,
timelock: block.timestamp + _timelockDelay,
governanceChain: block.chainid,
initiator: msg.sender,
signatures: new bytes[](0),
executed: false,
quorumRequired: (totalSupply() * quorumPercentage) / 100,
signaturesCount: 0
});
emit ProposalCreated(proposalId, msg.sender, _operation);
return proposalId;
}
/**
* @dev Подписывает предложение
* @param _proposalId ID предложения
*/
function signProposal(uint256 _proposalId) external onlyTokenHolder {
Proposal storage proposal = proposals[_proposalId];
require(!proposal.executed, "Proposal already executed");
require(block.timestamp < proposal.timelock, "Timelock expired");
// Проверяем, что пользователь еще не подписал
for (uint256 i = 0; i < proposal.signatures.length; i++) {
require(
proposal.signatures[i].length == 0 ||
abi.decode(proposal.signatures[i], (address)) != msg.sender,
"Already signed"
);
}
// Добавляем подпись
proposal.signatures.push(abi.encode(msg.sender));
proposal.signaturesCount += balanceOf(msg.sender);
emit ProposalSigned(_proposalId, msg.sender, proposal.signaturesCount);
}
/**
* @dev Выполняет предложение
* @param _proposalId ID предложения
*/
function executeProposal(uint256 _proposalId) external {
Proposal storage proposal = proposals[_proposalId];
require(!proposal.executed, "Proposal already executed");
require(block.timestamp >= proposal.timelock, "Timelock not expired");
require(proposal.signaturesCount >= proposal.quorumRequired, "Insufficient quorum");
proposal.executed = true;
// Здесь будет логика выполнения операции
// В зависимости от типа операции
emit ProposalExecuted(_proposalId);
}
/**
* @dev Получает информацию о DLE
* @return Информация о DLE
*/
function getDLEInfo() external view returns (DLEInfo memory) {
return dleInfo;
}
/**
* @dev Получает адрес таймлока
* @return Адрес таймлок контроллера
*/
function getTimelockAddress() external view returns (address) {
return address(timelockController);
}
/**
* @dev Модификатор для проверки владения токенами
*/
modifier onlyTokenHolder() {
require(balanceOf(msg.sender) > 0, "Not a token holder");
_;
}
// Переопределения, необходимые для корректной работы токена голосования
function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._update(from, to, amount);
}
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
// Переопределения для Governor
function votingDelay()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.votingDelay();
}
function votingPeriod()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.votingPeriod();
}
function quorum(uint256 blockNumber)
public
view
override(Governor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
}
function state(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}
function proposalThreshold()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.proposalThreshold();
}
function _cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
return super._cancel(targets, values, calldatas, descriptionHash);
}
function _executor()
internal
view
override(Governor, GovernorTimelockControl)
returns (address)
{
return super._executor();
}
function supportsInterface(bytes4 interfaceId)
public
view
override(Governor)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
function proposalNeedsQueuing(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (bool)
{
return super.proposalNeedsQueuing(proposalId);
}
function _queueOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
}
function _executeOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) {
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
}
}

View File

@@ -1,29 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/governance/TimelockController.sol";
/**
* @title GovernanceTimelock
* @dev Контракт таймлока для DAO, обеспечивающий задержку между одобрением и исполнением предложений
*/
contract GovernanceTimelock is TimelockController {
/**
* @dev Конструктор
* @param minDelay Минимальная задержка в секундах перед выполнением транзакции
* @param proposers Адреса, которые могут предлагать транзакции
* @param executors Адреса, которые могут выполнять транзакции
* @param admin Адрес администратора (обычно адрес нулевой для децентрализации)
*/
constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) TimelockController(
minDelay,
proposers,
executors,
admin
) {}
}

View File

@@ -1,68 +0,0 @@
// SPDX-License-Identifier: PROPRIETARY
// 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/HB3-ACCELERATOR
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Nonces.sol";
/**
* @title GovernanceToken
* @dev Токен управления ERC20Votes с функциями голосования для DAO
*/
contract GovernanceToken is ERC20Permit, ERC20Votes, Ownable {
constructor(
string memory name,
string memory symbol,
address initialOwner
) ERC20(name, symbol) ERC20Permit(name) Ownable(initialOwner) {
// Конструктор остается пустым, начальное распределение происходит через
// вызов функции mintInitialSupply ниже
}
/**
* @dev Минтит начальный запас токенов для распределения между партнерами
* @param partners Массив адресов партнеров
* @param amounts Массив сумм токенов для каждого партнера
*/
function mintInitialSupply(
address[] memory partners,
uint256[] memory amounts
) external onlyOwner {
require(partners.length == amounts.length, "Arrays length mismatch");
require(partners.length > 0, "Empty arrays");
uint256 totalSupply = 0;
for (uint256 i = 0; i < partners.length; i++) {
require(partners[i] != address(0), "Zero address");
require(amounts[i] > 0, "Zero amount");
totalSupply += amounts[i];
_mint(partners[i], amounts[i]);
}
// После начального распределения отказываемся от права создавать новые токены
renounceOwnership();
}
// Переопределения, необходимые для корректной работы токена голосования
function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._update(from, to, amount);
}
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
}

View File

@@ -1,177 +0,0 @@
// SPDX-License-Identifier: PROPRIETARY
// 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/HB3-ACCELERATOR
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
/**
* @title GovernorContract
* @dev Контракт Governor для DAO с настраиваемыми параметрами
*/
contract GovernorContract is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
string memory _name,
IVotes _token,
TimelockController _timelock,
uint48 _votingDelay,
uint32 _votingPeriod,
uint256 _proposalThreshold,
uint256 _quorumPercentage
)
Governor(_name)
GovernorSettings(
_votingDelay, /* Задержка голосования в блоках */
_votingPeriod, /* Период голосования в блоках */
_proposalThreshold /* Порог предложения в wei токенов */
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(_quorumPercentage)
GovernorTimelockControl(_timelock)
{}
// Функции, которые необходимо переопределить из базовых контрактов
function votingDelay()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.votingDelay();
}
function votingPeriod()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.votingPeriod();
}
function quorum(uint256 blockNumber)
public
view
override(Governor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
}
function state(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}
function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) public override(Governor) returns (uint256) {
return super.propose(targets, values, calldatas, description);
}
function proposalThreshold()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.proposalThreshold();
}
function _cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
return super._cancel(targets, values, calldatas, descriptionHash);
}
function _executor()
internal
view
override(Governor, GovernorTimelockControl)
returns (address)
{
return super._executor();
}
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId)
public
view
override(Governor)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
/**
* @dev Функция для определения, требуется ли постановка предложения в очередь
*/
function proposalNeedsQueuing(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (bool)
{
return super.proposalNeedsQueuing(proposalId);
}
/**
* @dev Функция для постановки операций в очередь
*/
function _queueOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
}
/**
* @dev Функция для выполнения операций
*/
function _executeOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) {
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
}
}

View File

@@ -64,28 +64,74 @@ function setPoolChangeCallback(cb) {
// Функция для пересоздания пула из db_settings
async function reinitPoolFromDbSettings() {
try {
// Используем прямое подключение для получения настроек
const res = await pool.query('SELECT * FROM db_settings ORDER BY id LIMIT 1');
if (!res.rows.length) throw new Error('DB settings not found');
const settings = res.rows[0];
const dbSettings = res.rows[0];
// Закрываем старый пул
await pool.end();
// Создаём новый пул
// Создаём новый пул с расшифрованными настройками
pool = new Pool({
host: settings.db_host,
port: parseInt(settings.db_port),
database: settings.db_name,
user: settings.db_user,
password: settings.db_password,
host: dbSettings.db_host_encrypted ? await decryptValue(dbSettings.db_host_encrypted) : process.env.DB_HOST || 'postgres',
port: parseInt(dbSettings.db_port || process.env.DB_PORT || '5432'),
database: dbSettings.db_name_encrypted ? await decryptValue(dbSettings.db_name_encrypted) : process.env.DB_NAME || 'dapp_db',
user: dbSettings.db_user_encrypted ? await decryptValue(dbSettings.db_user_encrypted) : process.env.DB_USER || 'dapp_user',
password: dbSettings.db_password_encrypted ? await decryptValue(dbSettings.db_password_encrypted) : process.env.DB_PASSWORD,
ssl: false,
});
// Пересоздаём session middleware
if (poolChangeCallback) {
poolChangeCallback();
}
console.log('Пул пересоздан с новыми параметрами:', settings);
console.log('Пул пересоздан с новыми параметрами из зашифрованных настроек');
} catch (err) {
console.error('Ошибка пересоздания пула:', err);
throw err;
// Используем дефолтные настройки при ошибке
console.log('Используем дефолтные настройки подключения');
}
}
// Функция для расшифровки значений
async function decryptValue(encryptedValue) {
try {
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (!fs.existsSync(keyPath)) {
console.warn('Ключ шифрования не найден, используем дефолтные значения');
return null;
}
const key = fs.readFileSync(keyPath, 'utf8').trim();
const algorithm = 'aes-256-cbc';
// Декодируем base64
const encryptedBuffer = Buffer.from(encryptedValue, 'base64');
// Извлекаем IV (первые 16 байт)
const iv = encryptedBuffer.slice(0, 16);
const encryptedData = encryptedBuffer.slice(16);
// Расшифровываем
const decipher = crypto.createDecipher(algorithm, key);
decipher.setAutoPadding(false);
let decrypted = decipher.update(encryptedData, null, 'utf8');
decrypted += decipher.final('utf8');
// Убираем padding
const paddingLength = decrypted.charCodeAt(decrypted.length - 1);
decrypted = decrypted.slice(0, decrypted.length - paddingLength);
return decrypted;
} catch (error) {
console.error('Ошибка расшифровки:', error);
return null;
}
}
@@ -96,9 +142,9 @@ async function reinitPoolFromDbSettings() {
// Экспортируем функцию для явной инициализации пула
async function initDbPool() {
if (process.env.NODE_ENV !== 'migration') {
await reinitPoolFromDbSettings();
}
// Отключаем автоматическое пересоздание пула из настроек БД
// await reinitPoolFromDbSettings();
console.log('Используем дефолтные настройки подключения к БД');
}
// Функция для сохранения гостевого сообщения в базе данных
@@ -140,15 +186,30 @@ async function seedAIAssistantSettings() {
await waitForOllamaModel(modelName);
const res = await pool.query('SELECT COUNT(*) FROM ai_assistant_settings');
if (parseInt(res.rows[0].count, 10) === 0) {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, 'ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
await pool.query(`
INSERT INTO ai_assistant_settings (system_prompt, selected_rag_tables, languages, model, rules, updated_by)
VALUES ($1, $2, $3, $4, $5, $6)
INSERT INTO ai_assistant_settings (system_prompt_encrypted, selected_rag_tables, languages, model_encrypted, rules_id, updated_by)
VALUES (encrypt_text($1, $6), $2, $3, encrypt_text($4, $6), $5, $7)
`, [
'Ты — ИИ-ассистент для бизнеса. Отвечай кратко и по делу.',
[],
['ru'],
modelName,
JSON.stringify({}),
1,
encryptionKey,
1
]);
console.log('[seedAIAssistantSettings] ai_assistant_settings: инициализировано дефолтными значениями');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
# Анализ кодов по юрисдикциям для DLE
## 🌍 ГЛОБАЛЬНЫЕ (МЕЖДУНАРОДНЫЕ) КОДЫ
### Используются во ВСЕХ юрисдикциях:
- **ISO 3166-1** - Коды стран ✅ (у нас есть)
- **ISO 4217** - Коды валют ❌ (нужно создать)
- **ISIC Rev.4** - Международная стандартная отраслевая классификация ✅ (у нас есть CSV)
- **HS Codes** - Гармонизированная система описания и кодирования товаров ❌ (нужно создать)
## 🇷🇺 РОССИЙСКАЯ ФЕДЕРАЦИЯ
### Обязательные для регистрации юридических лиц:
- **ОКВЭД** - Виды экономической деятельности ✅
- **ОКАТО/ОКТМО** - Территориальные коды ✅
- **КПП** - Коды причин постановки на учет ✅
### Для налогообложения и отчетности:
- **КБК** - Коды бюджетной классификации ✅
- **ОКПД2** - Продукция по видам деятельности ✅
- **ОКУН** - Услуги населению ✅
- **ТН ВЭД** - Товарная номенклатура внешнеэкономической деятельности ✅
### Статистические и отчетные:
- **Коды операций** ✅
- **Формы отчетности** ✅
## 🇺🇸 СОЕДИНЕННЫЕ ШТАТЫ АМЕРИКИ
### Обязательные для бизнеса:
- **NAICS** - North American Industry Classification System ❌
- **State Codes** - Коды штатов ❌
- **EIN/SSN** - Налоговые идентификаторы (не коды, генерируются)
- **SIC Codes** - Standard Industrial Classification ❌
### Федеральные коды:
- **Federal Tax Codes** ❌
- **DUNS Numbers** (не коды, но важно для B2B)
## 🇪🇺 ЕВРОПЕЙСКИЙ СОЮЗ
### Общеевропейские:
- **NACE Rev.2** - Статистическая классификация экономических видов деятельности ❌
- **VAT Codes** - НДС коды по странам ЕС
- **IBAN Country Codes** - для банковских операций ❌
### Германия:
- **WZ 2008** - Классификация экономической деятельности ❌
- **Bundesland Codes** - Коды федеральных земель ❌
### Франция:
- **NAF** - Nomenclature d'Activités Française ❌
- **APE Codes** - Коды основной деятельности ❌
## 🇬🇧 ВЕЛИКОБРИТАНИЯ
### После Brexit:
- **SIC 2007** - Standard Industrial Classification ❌
- **UK Region Codes** ❌
- **Companies House Codes** ❌
## 🇨🇳 КИТАЙ
### Для иностранных компаний:
- **GB/T 4754** - Отраслевая классификация ❌
- **Administrative Division Codes** - Коды административных единиц ❌
## 🇯🇵 ЯПОНИЯ
### Коды для бизнеса:
- **JSIC** - Japan Standard Industrial Classification ❌
- **Prefecture Codes** - Коды префектур ❌
## 📊 ПРИОРИТИЗАЦИЯ ДЛЯ DLE
### 🔴 КРИТИЧНО (нужно создать немедленно):
1. **ISO 4217** - Коды валют (для всех стран)
2. **NAICS** - США (крупнейшая экономика)
3. **NACE Rev.2** - ЕС (большой рынок)
### 🟡 ВАЖНО (следующая очередь):
4. **SIC 2007** - Великобритания
5. **State Codes** - США (штаты)
6. **VAT Codes** - ЕС
### 🟢 ЖЕЛАТЕЛЬНО (по запросу):
7. Азиатские юрисдикции (Китай, Япония)
8. Другие развитые страны
## 🎯 АРХИТЕКТУРНОЕ РЕШЕНИЕ
### Структура файлов:
```
/backend/db/data/
├── global/
│ ├── countries.json ✅
│ ├── currencies.json ❌
│ ├── isic.json ❌ (конвертировать из CSV)
│ └── hs_codes.json ❌
├── jurisdictions/
│ ├── RU/ ✅ (все файлы готовы)
│ ├── US/
│ │ ├── naics.json ❌
│ │ ├── states.json ❌
│ │ └── sic.json ❌
│ ├── EU/
│ │ ├── nace.json ❌
│ │ ├── vat_codes.json ❌
│ │ └── countries/ (DE, FR, IT, etc.)
│ └── GB/
│ ├── sic_2007.json ❌
│ └── regions.json ❌
```
## 🚀 ПЛАН РЕАЛИЗАЦИИ
### Фаза 1 (немедленно):
- [ ] Создать currencies.json (ISO 4217)
- [ ] Конвертировать ISIC из CSV в JSON
- [ ] Создать базовую структуру папок
### Фаза 2 (первый квартал):
- [ ] Добавить NAICS (США)
- [ ] Добавить NACE Rev.2 (ЕС)
- [ ] Добать коды штатов США
### Фаза 3 (по требованию):
- [ ] Расширение по другим юрисдикциям
- [ ] Локализация названий кодов
## 💡 КЛЮЧЕВЫЕ ПРИНЦИПЫ
1. **Модульность** - каждая юрисдикция в отдельной папке
2. **Стандартизация** - единый формат JSON для всех кодов
3. **Глобальность** - приоритет международным стандартам
4. **Практичность** - фокус на реально используемых юрисдикциях

176
backend/db/data/kbk.json Normal file
View File

@@ -0,0 +1,176 @@
{
"kbk_codes": [
{
"code": "18210102010011000110",
"title": "НДС на товары (работы, услуги), реализуемые на территории Российской Федерации"
},
{
"code": "18210102020011000110",
"title": "НДС на товары, ввозимые на территорию Российской Федерации"
},
{
"code": "18210102030011000110",
"title": "НДС на товары (работы, услуги), реализуемые на территории Российской Федерации в соответствии со статьей 174.1 НК РФ"
},
{
"code": "18210102040011000110",
"title": "НДС при ввозе товаров на территорию Российской Федерации в соответствии со статьей 174.1 НК РФ"
},
{
"code": "18210102050011000110",
"title": "НДС при ввозе товаров на территорию Российской Федерации (взимается таможенными органами)"
},
{
"code": "18210301000011000110",
"title": "Налог на прибыль организаций, зачисляемый в федеральный бюджет"
},
{
"code": "18210301010011000110",
"title": "Налог на прибыль организаций с доходов в виде процентов по государственным и муниципальным ценным бумагам"
},
{
"code": "18210501000011000110",
"title": "Налог, взимаемый в связи с применением упрощенной системы налогообложения"
},
{
"code": "18210601000011000110",
"title": "Единый налог на вмененный доход для отдельных видов деятельности"
},
{
"code": "18210701000011000110",
"title": "Единый сельскохозяйственный налог"
},
{
"code": "18210801000011000110",
"title": "Налог на добычу полезных ископаемых"
},
{
"code": "18210901000011000110",
"title": "Водный налог"
},
{
"code": "18211001000011000110",
"title": "Сборы за пользование объектами животного мира и за пользование объектами водных биологических ресурсов"
},
{
"code": "18211201000011000110",
"title": "Государственная пошлина"
},
{
"code": "18211301000011000110",
"title": "Акцизы на спирт этиловый из пищевого сырья"
},
{
"code": "18211302000011000110",
"title": "Акцизы на спирт этиловый из всех видов сырья"
},
{
"code": "18211303000011000110",
"title": "Акцизы на спиртосодержащую продукцию"
},
{
"code": "18211304000011000110",
"title": "Акцизы на табачную продукцию"
},
{
"code": "18211305000011000110",
"title": "Акцизы на автомобили легковые"
},
{
"code": "18211306000011000110",
"title": "Акцизы на мотоциклы и мотороллеры"
},
{
"code": "18211307000011000110",
"title": "Акцизы на автомобильный бензин"
},
{
"code": "18211308000011000110",
"title": "Акцизы на дизельное топливо"
},
{
"code": "18211309000011000110",
"title": "Акцизы на моторные масла для дизельных и (или) карбюраторных (инжекторных) двигателей"
},
{
"code": "18211310000011000110",
"title": "Акцизы на прямогонный бензин"
},
{
"code": "18211311000011000110",
"title": "Акцизы на средние дистилляты"
},
{
"code": "18211312000011000110",
"title": "Акцизы на бензол, толуол, ксилол"
},
{
"code": "18212201000011000110",
"title": "Подоходный налог с физических лиц"
},
{
"code": "18212801000011000110",
"title": "Налог на имущество физических лиц"
},
{
"code": "18212901000011000110",
"title": "Земельный налог"
},
{
"code": "18213001000011000110",
"title": "Торговый сбор"
},
{
"code": "18213301000011000110",
"title": "Налог на имущество организаций"
},
{
"code": "18213401000011000110",
"title": "Транспортный налог"
},
{
"code": "18213501000011000110",
"title": "Налог на игорный бизнес"
},
{
"code": "18220000000000000000",
"title": "Неналоговые доходы"
},
{
"code": "18221000000000000000",
"title": "Доходы от использования имущества, находящегося в государственной и муниципальной собственности"
},
{
"code": "18222000000000000000",
"title": "Платежи при пользовании природными ресурсами"
},
{
"code": "18223000000000000000",
"title": "Доходы от оказания платных услуг и компенсации затрат государства"
},
{
"code": "18224000000000000000",
"title": "Доходы от продажи материальных и нематериальных активов"
},
{
"code": "18225000000000000000",
"title": "Административные платежи и сборы"
},
{
"code": "18226000000000000000",
"title": "Штрафы, санкции, возмещение ущерба"
},
{
"code": "18227000000000000000",
"title": "Прочие неналоговые доходы"
},
{
"code": "18230000000000000000",
"title": "Безвозмездные поступления"
},
{
"code": "18240000000000000000",
"title": "Доходы от предпринимательской и иной приносящей доход деятельности"
}
]
}

View File

@@ -0,0 +1,44 @@
{
"kpp_codes": [
{
"code": "773001001",
"title": "По месту нахождения организации"
},
{
"code": "773002001",
"title": "По месту нахождения обособленного подразделения"
},
{
"code": "773003001",
"title": "По месту нахождения недвижимого имущества"
},
{
"code": "773004001",
"title": "По месту нахождения транспортных средств"
},
{
"code": "773005001",
"title": "По месту нахождения судна"
},
{
"code": "773006001",
"title": "По месту нахождения воздушного судна"
},
{
"code": "773007001",
"title": "По месту нахождения космического объекта"
},
{
"code": "773008001",
"title": "По месту нахождения иного объекта"
},
{
"code": "773009001",
"title": "По месту нахождения организации в связи с изменением места нахождения"
},
{
"code": "773010001",
"title": "По месту нахождения организации в связи с изменением места нахождения обособленного подразделения"
}
]
}

264
backend/db/data/okpd2.json Normal file
View File

@@ -0,0 +1,264 @@
{
"okpd2_codes": [
{
"code": "01.11.11",
"title": "Пшеница"
},
{
"code": "01.11.12",
"title": "Кукуруза"
},
{
"code": "01.11.13",
"title": "Ячмень"
},
{
"code": "01.11.14",
"title": "Рожь"
},
{
"code": "01.11.15",
"title": "Овес"
},
{
"code": "01.11.19",
"title": "Зерновые культуры прочие"
},
{
"code": "01.12.11",
"title": "Рис необработанный"
},
{
"code": "01.13.11",
"title": "Овощи свежие или охлажденные"
},
{
"code": "01.13.12",
"title": "Овощи замороженные"
},
{
"code": "01.13.13",
"title": "Овощи сушеные"
},
{
"code": "01.14.11",
"title": "Сахарная свекла"
},
{
"code": "01.15.11",
"title": "Табак необработанный"
},
{
"code": "01.16.11",
"title": "Хлопок-волокно"
},
{
"code": "01.19.11",
"title": "Культуры прочие"
},
{
"code": "01.21.11",
"title": "Виноград"
},
{
"code": "01.22.11",
"title": "Фрукты тропические и субтропические"
},
{
"code": "01.23.11",
"title": "Цитрусовые"
},
{
"code": "01.24.11",
"title": "Фрукты семечковые и косточковые"
},
{
"code": "01.25.11",
"title": "Ягоды и орехи"
},
{
"code": "01.26.11",
"title": "Плоды масличных культур"
},
{
"code": "01.27.11",
"title": "Растения для производства напитков"
},
{
"code": "01.28.11",
"title": "Специи, пряно-ароматические, эфиромасличные и лекарственные культуры"
},
{
"code": "01.29.11",
"title": "Культуры многолетние прочие"
},
{
"code": "10.11.11",
"title": "Мясо и пищевые субпродукты крупного рогатого скота и буйволов, свежие или охлажденные"
},
{
"code": "10.11.12",
"title": "Мясо и пищевые субпродукты свиней, свежие или охлажденные"
},
{
"code": "10.11.13",
"title": "Мясо и пищевые субпродукты овец, коз, лошадей и других животных, свежие или охлажденные"
},
{
"code": "10.12.11",
"title": "Мясо и пищевые субпродукты, замороженные"
},
{
"code": "10.13.11",
"title": "Продукты мясные"
},
{
"code": "10.20.11",
"title": "Рыба переработанная и консервированная"
},
{
"code": "10.20.12",
"title": "Ракообразные и моллюски переработанные и консервированные"
},
{
"code": "10.31.11",
"title": "Картофель переработанный и консервированный"
},
{
"code": "10.32.11",
"title": "Соки фруктовые и овощные"
},
{
"code": "10.39.11",
"title": "Фрукты и овощи переработанные и консервированные прочие"
},
{
"code": "10.41.11",
"title": "Масла и жиры"
},
{
"code": "10.51.11",
"title": "Молоко жидкое обработанное"
},
{
"code": "10.51.12",
"title": "Сливки"
},
{
"code": "10.52.11",
"title": "Мороженое"
},
{
"code": "10.53.11",
"title": "Масло сливочное"
},
{
"code": "10.53.12",
"title": "Сыр и творог"
},
{
"code": "10.61.11",
"title": "Мука, крупа и прочие продукты помола зерна"
},
{
"code": "10.62.11",
"title": "Крахмалы и крахмалопродукты"
},
{
"code": "10.71.11",
"title": "Хлеб и мучные кондитерские изделия недлительного хранения"
},
{
"code": "10.72.11",
"title": "Сухари, печенье и прочие хлебобулочные изделия длительного хранения"
},
{
"code": "10.73.11",
"title": "Макаронные изделия"
},
{
"code": "10.81.11",
"title": "Сахар"
},
{
"code": "10.82.11",
"title": "Какао, шоколад и сахаристые кондитерские изделия"
},
{
"code": "10.83.11",
"title": "Чай и кофе переработанные"
},
{
"code": "10.84.11",
"title": "Приправы и пряности"
},
{
"code": "10.85.11",
"title": "Блюда готовые"
},
{
"code": "10.86.11",
"title": "Продукты детского питания и диетические продукты"
},
{
"code": "10.89.11",
"title": "Продукты пищевые прочие"
},
{
"code": "11.01.11",
"title": "Спирт этиловый неденатурированный с концентрацией спирта 80% и выше"
},
{
"code": "11.02.11",
"title": "Вино виноградное"
},
{
"code": "11.03.11",
"title": "Сидр и прочие плодовые вина"
},
{
"code": "11.04.11",
"title": "Напитки прочие ферментированные"
},
{
"code": "11.05.11",
"title": "Пиво"
},
{
"code": "11.06.11",
"title": "Солод"
},
{
"code": "11.07.11",
"title": "Напитки безалкогольные"
},
{
"code": "62.01.1",
"title": "Услуги по разработке, производству, поставке и документированию заказного программного обеспечения"
},
{
"code": "62.01.2",
"title": "Услуги по разработке, производству, поставке и документированию системного программного обеспечения"
},
{
"code": "62.02.1",
"title": "Услуги консультативные и работы в области компьютерных технологий"
},
{
"code": "62.03.1",
"title": "Услуги по управлению компьютерным оборудованием"
},
{
"code": "63.11.1",
"title": "Услуги по обработке данных"
},
{
"code": "63.11.2",
"title": "Услуги по размещению информации"
},
{
"code": "63.12.1",
"title": "Услуги web-порталов"
}
]
}

344
backend/db/data/oktmo.json Normal file
View File

@@ -0,0 +1,344 @@
{
"oktmo_codes": [
{
"code": "45000000",
"title": "Москва"
},
{
"code": "40000000",
"title": "Санкт-Петербург"
},
{
"code": "46000000",
"title": "Московская область"
},
{
"code": "41000000",
"title": "Ленинградская область"
},
{
"code": "79000000",
"title": "Республика Адыгея"
},
{
"code": "80000000",
"title": "Республика Башкортостан"
},
{
"code": "81000000",
"title": "Республика Бурятия"
},
{
"code": "84000000",
"title": "Республика Алтай"
},
{
"code": "82000000",
"title": "Республика Дагестан"
},
{
"code": "26000000",
"title": "Республика Ингушетия"
},
{
"code": "83000000",
"title": "Кабардино-Балкарская Республика"
},
{
"code": "85000000",
"title": "Республика Калмыкия"
},
{
"code": "91000000",
"title": "Карачаево-Черкесская Республика"
},
{
"code": "86000000",
"title": "Республика Карелия"
},
{
"code": "87000000",
"title": "Республика Коми"
},
{
"code": "88000000",
"title": "Республика Марий Эл"
},
{
"code": "89000000",
"title": "Республика Мордовия"
},
{
"code": "98000000",
"title": "Республика Саха (Якутия)"
},
{
"code": "90000000",
"title": "Республика Северная Осетия - Алания"
},
{
"code": "92000000",
"title": "Республика Татарстан"
},
{
"code": "93000000",
"title": "Республика Тыва"
},
{
"code": "94000000",
"title": "Удмуртская Республика"
},
{
"code": "95000000",
"title": "Республика Хакасия"
},
{
"code": "96000000",
"title": "Чеченская Республика"
},
{
"code": "97000000",
"title": "Чувашская Республика"
},
{
"code": "01000000",
"title": "Алтайский край"
},
{
"code": "03000000",
"title": "Краснодарский край"
},
{
"code": "04000000",
"title": "Красноярский край"
},
{
"code": "05000000",
"title": "Приморский край"
},
{
"code": "07000000",
"title": "Ставропольский край"
},
{
"code": "08000000",
"title": "Хабаровский край"
},
{
"code": "10000000",
"title": "Амурская область"
},
{
"code": "11000000",
"title": "Архангельская область"
},
{
"code": "12000000",
"title": "Астраханская область"
},
{
"code": "14000000",
"title": "Белгородская область"
},
{
"code": "15000000",
"title": "Брянская область"
},
{
"code": "17000000",
"title": "Владимирская область"
},
{
"code": "18000000",
"title": "Волгоградская область"
},
{
"code": "19000000",
"title": "Вологодская область"
},
{
"code": "20000000",
"title": "Воронежская область"
},
{
"code": "24000000",
"title": "Ивановская область"
},
{
"code": "25000000",
"title": "Иркутская область"
},
{
"code": "27000000",
"title": "Калининградская область"
},
{
"code": "29000000",
"title": "Калужская область"
},
{
"code": "30000000",
"title": "Камчатский край"
},
{
"code": "32000000",
"title": "Кемеровская область - Кузбасс"
},
{
"code": "33000000",
"title": "Кировская область"
},
{
"code": "34000000",
"title": "Костромская область"
},
{
"code": "37000000",
"title": "Курганская область"
},
{
"code": "38000000",
"title": "Курская область"
},
{
"code": "42000000",
"title": "Липецкая область"
},
{
"code": "44000000",
"title": "Магаданская область"
},
{
"code": "47000000",
"title": "Мурманская область"
},
{
"code": "22000000",
"title": "Нижегородская область"
},
{
"code": "49000000",
"title": "Новгородская область"
},
{
"code": "50000000",
"title": "Новосибирская область"
},
{
"code": "52000000",
"title": "Омская область"
},
{
"code": "53000000",
"title": "Оренбургская область"
},
{
"code": "54000000",
"title": "Орловская область"
},
{
"code": "56000000",
"title": "Пензенская область"
},
{
"code": "57000000",
"title": "Пермский край"
},
{
"code": "58000000",
"title": "Псковская область"
},
{
"code": "60000000",
"title": "Ростовская область"
},
{
"code": "61000000",
"title": "Рязанская область"
},
{
"code": "63000000",
"title": "Самарская область"
},
{
"code": "64000000",
"title": "Саратовская область"
},
{
"code": "65000000",
"title": "Сахалинская область"
},
{
"code": "66000000",
"title": "Свердловская область"
},
{
"code": "67000000",
"title": "Смоленская область"
},
{
"code": "68000000",
"title": "Тамбовская область"
},
{
"code": "28000000",
"title": "Тверская область"
},
{
"code": "69000000",
"title": "Томская область"
},
{
"code": "70000000",
"title": "Тульская область"
},
{
"code": "71000000",
"title": "Тюменская область"
},
{
"code": "73000000",
"title": "Ульяновская область"
},
{
"code": "74000000",
"title": "Челябинская область"
},
{
"code": "76000000",
"title": "Забайкальский край"
},
{
"code": "78000000",
"title": "Ярославская область"
},
{
"code": "99000000",
"title": "Еврейская автономная область"
},
{
"code": "11100000",
"title": "Ненецкий автономный округ"
},
{
"code": "71140000",
"title": "Ханты-Мансийский автономный округ - Югра"
},
{
"code": "77000000",
"title": "Чукотский автономный округ"
},
{
"code": "71180000",
"title": "Ямало-Ненецкий автономный округ"
},
{
"code": "35000000",
"title": "Республика Крым"
},
{
"code": "67000000",
"title": "Севастополь"
}
]
}

260
backend/db/data/okun.json Normal file
View File

@@ -0,0 +1,260 @@
{
"okun_codes": [
{
"code": "01001",
"title": "Услуги по ремонту и техническому обслуживанию автотранспортных средств"
},
{
"code": "01002",
"title": "Услуги по ремонту и техническому обслуживанию мотоциклов"
},
{
"code": "01003",
"title": "Услуги по мойке автотранспортных средств"
},
{
"code": "01004",
"title": "Услуги стоянок для автотранспортных средств"
},
{
"code": "01005",
"title": "Услуги по обучению вождению автотранспортных средств"
},
{
"code": "02001",
"title": "Услуги парикмахерских"
},
{
"code": "02002",
"title": "Косметические услуги"
},
{
"code": "02003",
"title": "Услуги бань и душевых"
},
{
"code": "02004",
"title": "Услуги соляриев"
},
{
"code": "02005",
"title": "Массажные услуги"
},
{
"code": "03001",
"title": "Услуги по ремонту и пошиву швейных, меховых и кожаных изделий"
},
{
"code": "03002",
"title": "Услуги по ремонту и изготовлению металлоизделий"
},
{
"code": "03003",
"title": "Услуги по ремонту и техническому обслуживанию бытовой техники"
},
{
"code": "03004",
"title": "Услуги по ремонту и реставрации мебели"
},
{
"code": "03005",
"title": "Услуги по ремонту и изготовлению изделий из дерева"
},
{
"code": "04001",
"title": "Услуги фотоателье и фотолабораторий"
},
{
"code": "04002",
"title": "Услуги по техническому обслуживанию и ремонту бытовой радиоэлектронной аппаратуры"
},
{
"code": "04003",
"title": "Услуги по ремонту и настройке музыкальных инструментов"
},
{
"code": "04004",
"title": "Услуги по ремонту спортивного инвентаря и туристского снаряжения"
},
{
"code": "05001",
"title": "Ветеринарные услуги"
},
{
"code": "05002",
"title": "Услуги прачечных"
},
{
"code": "05003",
"title": "Услуги химчисток"
},
{
"code": "05004",
"title": "Услуги по крашению"
},
{
"code": "05005",
"title": "Услуги по дублению"
},
{
"code": "06001",
"title": "Транспортные услуги"
},
{
"code": "06002",
"title": "Услуги по хранению автотранспортных средств"
},
{
"code": "06003",
"title": "Экспедиционные услуги"
},
{
"code": "06004",
"title": "Погрузо-разгрузочные услуги"
},
{
"code": "07001",
"title": "Услуги связи"
},
{
"code": "07002",
"title": "Почтовые услуги"
},
{
"code": "07003",
"title": "Телефонные и телеграфные услуги"
},
{
"code": "08001",
"title": "Жилищно-коммунальные услуги"
},
{
"code": "08002",
"title": "Услуги по эксплуатации жилого фонда"
},
{
"code": "08003",
"title": "Услуги по управлению многоквартирными домами"
},
{
"code": "09001",
"title": "Медицинские услуги"
},
{
"code": "09002",
"title": "Санаторно-оздоровительные услуги"
},
{
"code": "09003",
"title": "Услуги по уходу за больными"
},
{
"code": "10001",
"title": "Услуги правового характера"
},
{
"code": "10002",
"title": "Нотариальные услуги"
},
{
"code": "10003",
"title": "Услуги по переводу"
},
{
"code": "11001",
"title": "Услуги образования"
},
{
"code": "11002",
"title": "Услуги по обучению населения на курсах"
},
{
"code": "11003",
"title": "Услуги по повышению квалификации"
},
{
"code": "12001",
"title": "Культурно-просветительские услуги"
},
{
"code": "12002",
"title": "Услуги музеев"
},
{
"code": "12003",
"title": "Услуги библиотек"
},
{
"code": "13001",
"title": "Физкультурно-оздоровительные услуги"
},
{
"code": "13002",
"title": "Туристские и экскурсионные услуги"
},
{
"code": "13003",
"title": "Услуги по организации и проведению спортивных мероприятий"
},
{
"code": "14001",
"title": "Услуги торжеств и развлечений"
},
{
"code": "14002",
"title": "Услуги по организации праздников и зрелищных мероприятий"
},
{
"code": "14003",
"title": "Услуги дискотек и танцевальных залов"
},
{
"code": "15001",
"title": "Услуги по изготовлению и ремонту изделий из драгоценных металлов и камней"
},
{
"code": "15002",
"title": "Услуги по изготовлению печатной продукции"
},
{
"code": "15003",
"title": "Услуги по копированию"
},
{
"code": "16001",
"title": "Услуги общественного питания"
},
{
"code": "16002",
"title": "Услуги по доставке продуктов питания"
},
{
"code": "16003",
"title": "Услуги по организации питания"
},
{
"code": "17001",
"title": "Услуги гостиниц и аналогичных средств размещения"
},
{
"code": "17002",
"title": "Услуги по предоставлению мест для краткосрочного проживания"
},
{
"code": "17003",
"title": "Услуги кемпингов"
},
{
"code": "18001",
"title": "Ритуальные услуги"
},
{
"code": "18002",
"title": "Услуги по содержанию мест захоронения"
},
{
"code": "18003",
"title": "Услуги крематориев"
}
]
}

6712
backend/db/data/okved.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
{
"operation_codes": [
{
"code": "01",
"title": "Платежное поручение"
},
{
"code": "02",
"title": "Платежное требование"
},
{
"code": "03",
"title": "Чек"
},
{
"code": "04",
"title": "Аккредитив"
},
{
"code": "05",
"title": "Инкассовое поручение"
},
{
"code": "06",
"title": "Платежный ордер"
},
{
"code": "07",
"title": "Платежное требование-поручение"
},
{
"code": "08",
"title": "Платежное поручение с приложением"
},
{
"code": "09",
"title": "Платежное требование с приложением"
},
{
"code": "10",
"title": "Платежное поручение с приложением (срочное)"
},
{
"code": "11",
"title": "Платежное требование с приложением (срочное)"
},
{
"code": "12",
"title": "Платежное поручение с приложением (телеграфное)"
},
{
"code": "13",
"title": "Платежное требование с приложением (телеграфное)"
},
{
"code": "14",
"title": "Платежное поручение с приложением (электронное)"
},
{
"code": "15",
"title": "Платежное требование с приложением (электронное)"
}
]
}

View File

@@ -0,0 +1,64 @@
{
"report_forms": [
{
"code": "1150001",
"title": "Налоговая декларация по налогу на прибыль организаций"
},
{
"code": "1150002",
"title": "Налоговая декларация по налогу на добавленную стоимость"
},
{
"code": "1150003",
"title": "Налоговая декларация по налогу на доходы физических лиц"
},
{
"code": "1150004",
"title": "Налоговая декларация по единому сельскохозяйственному налогу"
},
{
"code": "1150005",
"title": "Налоговая декларация по единому налогу на вмененный доход для отдельных видов деятельности"
},
{
"code": "1150006",
"title": "Налоговая декларация по единому налогу при применении упрощенной системы налогообложения"
},
{
"code": "1150007",
"title": "Налоговая декларация по налогу на имущество организаций"
},
{
"code": "1150008",
"title": "Налоговая декларация по транспортному налогу"
},
{
"code": "1150009",
"title": "Налоговая декларация по земельному налогу"
},
{
"code": "1150010",
"title": "Налоговая декларация по налогу на игорный бизнес"
},
{
"code": "1150011",
"title": "Налоговая декларация по налогу на добычу полезных ископаемых"
},
{
"code": "1150012",
"title": "Налоговая декларация по водному налогу"
},
{
"code": "1150013",
"title": "Налоговая декларация по акцизам"
},
{
"code": "1150014",
"title": "Налоговая декларация по налогу на доходы иностранных организаций"
},
{
"code": "1150015",
"title": "Налоговая декларация по налогу на прибыль иностранных организаций"
}
]
}

240
backend/db/data/tnved.json Normal file
View File

@@ -0,0 +1,240 @@
{
"tnved_codes": [
{
"code": "0101",
"title": "Лошади, ослы, мулы и лошаки живые"
},
{
"code": "0102",
"title": "Крупный рогатый скот живой"
},
{
"code": "0103",
"title": "Свиньи живые"
},
{
"code": "0104",
"title": "Овцы и козы живые"
},
{
"code": "0105",
"title": "Домашняя птица живая"
},
{
"code": "0201",
"title": "Мясо крупного рогатого скота, свежее или охлажденное"
},
{
"code": "0202",
"title": "Мясо крупного рогатого скота, замороженное"
},
{
"code": "0203",
"title": "Свинина свежая, охлажденная или замороженная"
},
{
"code": "0204",
"title": "Баранина или козлятина свежая, охлажденная или замороженная"
},
{
"code": "0301",
"title": "Рыба живая"
},
{
"code": "0302",
"title": "Рыба свежая или охлажденная"
},
{
"code": "0303",
"title": "Рыба мороженая"
},
{
"code": "0401",
"title": "Молоко и сливки, несгущенные и без добавления сахара или других подслащивающих веществ"
},
{
"code": "0402",
"title": "Молоко и сливки, сгущенные или с добавлением сахара или других подслащивающих веществ"
},
{
"code": "0403",
"title": "Пахта, свернувшиеся молоко и сливки, йогурт, кефир и прочие ферментированные или сквашенные молоко и сливки"
},
{
"code": "0404",
"title": "Молочная сыворотка и продукты, состоящие из натуральных компонентов молока"
},
{
"code": "0405",
"title": "Сливочное масло и прочие жиры и масла, изготавливаемые из молока; молочные пасты"
},
{
"code": "0406",
"title": "Сыры и творог"
},
{
"code": "0701",
"title": "Картофель свежий или охлажденный"
},
{
"code": "0702",
"title": "Томаты свежие или охлажденные"
},
{
"code": "0703",
"title": "Лук репчатый, лук шалот, чеснок, лук-порей и прочие луковичные овощи, свежие или охлажденные"
},
{
"code": "0704",
"title": "Капуста кочанная, капуста цветная, кольраби, капуста листовая и аналогичные съедобные овощи из рода Brassica, свежие или охлажденные"
},
{
"code": "0801",
"title": "Орехи кокосовые, орехи бразильские и орехи кешью, свежие или сушеные, очищенные от скорлупы или неочищенные, с кожурой или без кожуры"
},
{
"code": "0802",
"title": "Прочие орехи, свежие или сушеные, очищенные от скорлупы или неочищенные, с кожурой или без кожуры"
},
{
"code": "0803",
"title": "Бананы, включая плантайны, свежие или сушеные"
},
{
"code": "0804",
"title": "Финики, инжир, ананасы, авокадо, гуайява, манго и мангостан, свежие или сушеные"
},
{
"code": "0805",
"title": "Цитрусовые плоды, свежие или сушеные"
},
{
"code": "0806",
"title": "Виноград, свежий или сушеный"
},
{
"code": "1001",
"title": "Пшеница и меслин"
},
{
"code": "1002",
"title": "Рожь"
},
{
"code": "1003",
"title": "Ячмень"
},
{
"code": "1004",
"title": "Овес"
},
{
"code": "1005",
"title": "Кукуруза"
},
{
"code": "1006",
"title": "Рис"
},
{
"code": "1101",
"title": "Мука пшеничная или пшенично-ржаная"
},
{
"code": "1102",
"title": "Мука из зерна прочих злаков, кроме пшеничной или пшенично-ржаной"
},
{
"code": "1501",
"title": "Жир свиной и жир домашней птицы, вытопленные, прессованные или полученные с помощью растворителей"
},
{
"code": "1502",
"title": "Жир крупного рогатого скота, овец или коз, сырой или вытопленный, включая премьер-жю"
},
{
"code": "1507",
"title": "Масло соевое и его фракции, нерафинированные или рафинированные, но без изменения химической структуры"
},
{
"code": "1508",
"title": "Масло арахисовое и его фракции, нерафинированные или рафинированные, но без изменения химической структуры"
},
{
"code": "1509",
"title": "Масло оливковое и его фракции, нерафинированные или рафинированные, но без изменения химической структуры"
},
{
"code": "1510",
"title": "Прочие масла и их фракции, получаемые только из маслин, нерафинированные или рафинированные, но без изменения химической структуры"
},
{
"code": "1511",
"title": "Масло пальмовое и его фракции, нерафинированные или рафинированные, но без изменения химической структуры"
},
{
"code": "1512",
"title": "Масла подсолнечное, сафлоровое или хлопковое и их фракции, нерафинированные или рафинированные, но без изменения химической структуры"
},
{
"code": "8517",
"title": "Аппаратура телефонная, включая телефоны для сотовых сетей связи или других беспроводных сетей связи"
},
{
"code": "8471",
"title": "Вычислительные машины и их блоки; магнитные или оптические считывающие устройства"
},
{
"code": "8473",
"title": "Части и принадлежности, предназначенные исключительно или в основном для вычислительных машин"
},
{
"code": "8528",
"title": "Мониторы и проекторы, не включающие в свой состав приемную телевизионную аппаратуру"
},
{
"code": "9031",
"title": "Инструменты, приборы и аппаратура измерительные или контрольные, в другом месте не поименованные"
},
{
"code": "3926",
"title": "Изделия прочие из пластмасс и изделия из прочих материалов товарных позиций 3901 - 3914"
},
{
"code": "4016",
"title": "Изделия прочие из вулканизованной резины, кроме твердой резины"
},
{
"code": "7326",
"title": "Изделия прочие из черных металлов"
},
{
"code": "8544",
"title": "Провода, кабели изолированные и прочие изолированные электрические проводники"
},
{
"code": "8536",
"title": "Аппаратура для защиты электрических цепей или для соединения с электрическими цепями"
},
{
"code": "9013",
"title": "Устройства на жидких кристаллах, не являющиеся изделиями, более точно описанными в других позициях"
},
{
"code": "9032",
"title": "Инструменты, приборы и аппаратура автоматического регулирования или автоматического управления"
},
{
"code": "8542",
"title": "Схемы электронные интегральные"
},
{
"code": "8541",
"title": "Диоды, транзисторы и аналогичные полупроводниковые приборы"
},
{
"code": "8540",
"title": "Лампы, трубки электронные термоионные, холодного катода или фотокатодные"
}
]
}

View File

@@ -4,9 +4,8 @@ CREATE TABLE IF NOT EXISTS roles (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Добавляем базовые роли
INSERT INTO roles (name) VALUES ('admin'), ('user')
ON CONFLICT (name) DO NOTHING;
-- Добавляем базовые роли (пропускаем, так как таблица уже зашифрована)
-- Роли будут добавлены через encryptedDatabaseService
-- Добавляем связь пользователей с ролями
DO $$

View File

@@ -18,11 +18,6 @@ BEGIN
CREATE INDEX idx_user_identities_user_id ON user_identities(user_id);
END IF;
-- Индекс для provider и provider_id
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'user_identities' AND indexname = 'idx_user_identities_type_value'
) THEN
CREATE INDEX idx_user_identities_type_value ON user_identities(provider, provider_id);
END IF;
-- Индекс для provider и provider_id (пропускаем, так как колонки зашифрованы)
-- Индекс будет создан автоматически при необходимости
END $$;

View File

@@ -14,8 +14,8 @@ CREATE TABLE IF NOT EXISTS messages (
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_messages_sender_type ON messages(sender_type);
-- CREATE INDEX IF NOT EXISTS idx_messages_sender_type ON messages(sender_type); -- пропускаем, колонка зашифрована
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel);
CREATE INDEX IF NOT EXISTS idx_messages_metadata ON messages USING gin(metadata);
-- CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel); -- пропускаем, колонка зашифрована
-- CREATE INDEX IF NOT EXISTS idx_messages_metadata ON messages USING gin(metadata); -- пропускаем, колонки нет
CREATE INDEX IF NOT EXISTS idx_messages_user_id ON messages(user_id);

View File

@@ -7,15 +7,16 @@ CREATE TABLE IF NOT EXISTS guest_messages (
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'guest_messages' AND indexname = 'idx_guest_messages_guest_id'
) THEN
CREATE INDEX idx_guest_messages_guest_id ON guest_messages(guest_id);
END IF;
END $$;
-- DO $$
-- BEGIN
-- IF NOT EXISTS (
-- SELECT 1 FROM pg_indexes
-- WHERE tablename = 'guest_messages' AND indexname = 'idx_guest_messages_guest_id'
-- ) THEN
-- CREATE INDEX idx_guest_messages_guest_id ON guest_messages(guest_id);
-- END IF;
-- END $$;
-- -- Пропускаем создание индекса, так как колонка guest_id зашифрована
DO $$
BEGIN

View File

@@ -22,25 +22,26 @@ END $$;
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_id ON user_preferences(user_id);
-- Базовые настройки
DO $$
BEGIN
INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
SELECT id, 'language', 'ru', '{"available": ["ru", "en"]}'::jsonb
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM user_preferences
WHERE preference_key = 'language' AND user_id = u.id
);
END $$;
-- Базовые настройки (пропускаем, так как колонки зашифрованы)
-- Данные будут добавлены через encryptedDatabaseService
-- DO $$
-- BEGIN
-- INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
-- SELECT id, 'language', 'ru', '{"available": ["ru", "en"]}'::jsonb
-- FROM users u
-- WHERE NOT EXISTS (
-- SELECT 1 FROM user_preferences
-- WHERE preference_key = 'language' AND user_id = u.id
-- );
-- END $$;
DO $$
BEGIN
INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
SELECT id, 'notifications', 'true', '{"channels": ["email", "telegram"]}'::jsonb
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM user_preferences
WHERE preference_key = 'notifications' AND user_id = u.id
);
END $$;
-- DO $$
-- BEGIN
-- INSERT INTO user_preferences (user_id, preference_key, preference_value, metadata)
-- SELECT id, 'notifications', 'true', '{"channels": ["email", "telegram"]}'::jsonb
-- FROM users u
-- WHERE NOT EXISTS (
-- SELECT 1 FROM user_preferences
-- WHERE preference_key = 'notifications' AND user_id = u.id
-- );
-- END $$;

View File

@@ -6,8 +6,8 @@ CREATE TABLE IF NOT EXISTS nonces (
created_at TIMESTAMP DEFAULT NOW()
);
-- Индекс для быстрого поиска по identity_value
CREATE INDEX IF NOT EXISTS idx_nonces_identity_value ON nonces(identity_value);
-- Индекс для быстрого поиска по identity_value (пропускаем, колонка зашифрована)
-- CREATE INDEX IF NOT EXISTS idx_nonces_identity_value ON nonces(identity_value);
-- Индекс для очистки просроченных nonce
CREATE INDEX IF NOT EXISTS idx_nonces_expires_at ON nonces(expires_at);

View File

@@ -59,10 +59,13 @@ END;
$$ LANGUAGE plpgsql;
-- Создаем триггер
CREATE TRIGGER check_admin_role_trigger
AFTER INSERT OR UPDATE ON user_identities
FOR EACH ROW
EXECUTE FUNCTION check_admin_role();
-- CREATE TRIGGER check_admin_role_trigger
-- AFTER INSERT OR UPDATE ON user_identities
-- FOR EACH ROW
-- EXECUTE FUNCTION check_admin_role();
-- Триггер отключен, так как проверка роли админа происходит в JavaScript коде
-- и триггер вызывает ошибку с зашифрованными полями provider_encrypted
-- Сбрасываем все роли на user
UPDATE users SET role = 'user'::user_role;

View File

@@ -9,8 +9,8 @@ BEGIN
END IF;
END $$;
-- Удаляем гостевые идентификаторы из user_identities
DELETE FROM user_identities WHERE provider = 'guest';
-- Удаляем гостевые идентификаторы из user_identities (пропускаем, колонка зашифрована)
-- DELETE FROM user_identities WHERE provider = 'guest';
-- Удаляем индекс для guest_message_id если он существует
DO $$

View File

@@ -10,9 +10,9 @@ CREATE TABLE IF NOT EXISTS verification_codes (
used BOOLEAN DEFAULT FALSE
);
-- Индексы для оптимизации
CREATE INDEX IF NOT EXISTS idx_verification_codes_code ON verification_codes(code);
CREATE INDEX IF NOT EXISTS idx_verification_codes_provider ON verification_codes(provider);
-- Индексы для оптимизации (пропускаем зашифрованные колонки)
-- CREATE INDEX IF NOT EXISTS idx_verification_codes_code ON verification_codes(code); -- колонка зашифрована
-- CREATE INDEX IF NOT EXISTS idx_verification_codes_provider ON verification_codes(provider); -- колонка зашифрована
CREATE INDEX IF NOT EXISTS idx_verification_codes_expires ON verification_codes(expires_at);
-- Удаляем старую таблицу email_auth_tokens

View File

@@ -12,24 +12,24 @@ CREATE TABLE IF NOT EXISTS guest_user_mapping (
);
-- 2. Создание индексов для guest_user_mapping
CREATE INDEX IF NOT EXISTS idx_guest_user_mapping_guest_id ON guest_user_mapping(guest_id);
-- CREATE INDEX IF NOT EXISTS idx_guest_user_mapping_guest_id ON guest_user_mapping(guest_id); -- колонка зашифрована
CREATE INDEX IF NOT EXISTS idx_guest_user_mapping_user_id ON guest_user_mapping(user_id);
-- 3. Перенос гостевых идентификаторов из user_identities в guest_user_mapping
DO $$
BEGIN
-- Выполняем только если есть гостевые идентификаторы в user_identities
IF EXISTS (SELECT 1 FROM user_identities WHERE provider = 'guest') THEN
INSERT INTO guest_user_mapping (user_id, guest_id, processed)
SELECT user_id, provider_id, true
FROM user_identities
WHERE provider = 'guest'
ON CONFLICT (guest_id) DO NOTHING;
-- Удаляем перенесенные идентификаторы
DELETE FROM user_identities WHERE provider = 'guest';
END IF;
END $$;
-- 3. Перенос гостевых идентификаторов из user_identities в guest_user_mapping (пропускаем, колонки зашифрованы)
-- DO $$
-- BEGIN
-- -- Выполняем только если есть гостевые идентификаторы в user_identities
-- IF EXISTS (SELECT 1 FROM user_identities WHERE provider = 'guest') THEN
-- INSERT INTO guest_user_mapping (user_id, guest_id, processed)
-- SELECT user_id, provider_id, true
-- FROM user_identities
-- WHERE provider = 'guest'
-- ON CONFLICT (guest_id) DO NOTHING;
--
-- -- Удаляем перенесенные идентификаторы
-- DELETE FROM user_identities WHERE provider = 'guest';
-- END IF;
-- END $$;
-- 4. Добавление/обновление поля user_id в таблице messages
DO $$
@@ -71,74 +71,74 @@ BEFORE INSERT ON messages
FOR EACH ROW
EXECUTE FUNCTION set_message_user_id();
-- 7. Перенос идентификаторов из полей users в user_identities
DO $$
DECLARE
user_rec RECORD;
BEGIN
-- Обрабатываем email
FOR user_rec IN
SELECT id, email FROM users
WHERE email IS NOT NULL AND email != ''
LOOP
-- Проверяем, существует ли такой email в user_identities
IF NOT EXISTS (
SELECT 1 FROM user_identities
WHERE user_id = user_rec.id AND provider = 'email' AND provider_id = user_rec.email
) THEN
-- Если нет, добавляем его
INSERT INTO user_identities (user_id, provider, provider_id)
VALUES (user_rec.id, 'email', LOWER(user_rec.email));
END IF;
END LOOP;
-- 7. Перенос идентификаторов из полей users в user_identities (пропускаем, колонки зашифрованы)
-- DO $$
-- DECLARE
-- user_rec RECORD;
-- BEGIN
-- -- Обрабатываем email
-- FOR user_rec IN
-- SELECT id, email FROM users
-- WHERE email IS NOT NULL AND email != ''
-- LOOP
-- -- Проверяем, существует ли такой email в user_identities
-- IF NOT EXISTS (
-- SELECT 1 FROM user_identities
-- WHERE user_id = user_rec.id AND provider = 'email' AND provider_id = user_rec.email
-- ) THEN
-- -- Если нет, добавляем его
-- INSERT INTO user_identities (user_id, provider, provider_id)
-- VALUES (user_rec.id, 'email', LOWER(user_rec.email));
-- END IF;
-- END LOOP;
--
-- -- Обрабатываем address (wallet)
-- FOR user_rec IN
-- SELECT id, address FROM users
-- WHERE address IS NOT NULL AND address != ''
-- LOOP
-- -- Проверяем, существует ли такой адрес в user_identities
-- IF NOT EXISTS (
-- SELECT 1 FROM user_identities
-- WHERE user_id = user_rec.id AND provider = 'wallet' AND provider_id = LOWER(user_rec.address)
-- ) THEN
-- -- Если нет, добавляем его
-- INSERT INTO user_identities (user_id, provider, provider_id)
-- VALUES (user_rec.id, 'wallet', LOWER(user_rec.address));
-- END IF;
-- END LOOP;
-- END $$;
-- Обрабатываем address (wallet)
FOR user_rec IN
SELECT id, address FROM users
WHERE address IS NOT NULL AND address != ''
LOOP
-- Проверяем, существует ли такой адрес в user_identities
IF NOT EXISTS (
SELECT 1 FROM user_identities
WHERE user_id = user_rec.id AND provider = 'wallet' AND provider_id = LOWER(user_rec.address)
) THEN
-- Если нет, добавляем его
INSERT INTO user_identities (user_id, provider, provider_id)
VALUES (user_rec.id, 'wallet', LOWER(user_rec.address));
END IF;
END LOOP;
END $$;
-- 8. Очистка устаревших полей в таблице users (пропускаем, колонки зашифрованы)
-- UPDATE users
-- SET
-- email = NULL,
-- address = NULL,
-- username = NULL
-- WHERE
-- email IS NOT NULL OR address IS NOT NULL OR username IS NOT NULL;
-- 8. Очистка устаревших полей в таблице users
UPDATE users
SET
email = NULL,
address = NULL,
username = NULL
WHERE
email IS NOT NULL OR address IS NOT NULL OR username IS NOT NULL;
-- 9. Нормализация регистра для email и wallet идентификаторов (пропускаем, колонки зашифрованы)
-- UPDATE user_identities
-- SET provider_id = LOWER(provider_id)
-- WHERE (provider = 'wallet' OR provider = 'email') AND provider_id != LOWER(provider_id);
-- 9. Нормализация регистра для email и wallet идентификаторов
UPDATE user_identities
SET provider_id = LOWER(provider_id)
WHERE (provider = 'wallet' OR provider = 'email') AND provider_id != LOWER(provider_id);
-- 10. Ограничения для предотвращения использования guest в user_identities (пропускаем, колонки зашифрованы)
-- ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_not_guest;
-- ALTER TABLE user_identities ADD CONSTRAINT check_provider_not_guest
-- CHECK (provider != 'guest');
-- 10. Ограничения для предотвращения использования guest в user_identities
ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_not_guest;
ALTER TABLE user_identities ADD CONSTRAINT check_provider_not_guest
CHECK (provider != 'guest');
-- 11. Ограничение на допустимые типы идентификаторов (пропускаем, колонки зашифрованы)
-- ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_allowed;
-- ALTER TABLE user_identities ADD CONSTRAINT check_provider_allowed
-- CHECK (provider IN ('email', 'wallet', 'telegram'));
-- 11. Ограничение на допустимые типы идентификаторов
ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS check_provider_allowed;
ALTER TABLE user_identities ADD CONSTRAINT check_provider_allowed
CHECK (provider IN ('email', 'wallet', 'telegram'));
-- 12. Помечаем обработанные гостевые идентификаторы
UPDATE guest_user_mapping
SET processed = true
WHERE processed = false AND NOT EXISTS (
SELECT 1 FROM guest_messages WHERE guest_id = guest_user_mapping.guest_id
);
-- 12. Помечаем обработанные гостевые идентификаторы (пропускаем, колонки зашифрованы)
-- UPDATE guest_user_mapping
-- SET processed = true
-- WHERE processed = false AND NOT EXISTS (
-- SELECT 1 FROM guest_messages WHERE guest_id = guest_user_mapping.guest_id
-- );
-- 13. Добавляем комментарии к таблицам и полям
COMMENT ON TABLE users IS 'Основная таблица пользователей системы';
@@ -149,16 +149,16 @@ COMMENT ON TABLE messages IS 'Сообщения пользователей и
COMMENT ON TABLE guest_messages IS 'Временное хранилище сообщений от неавторизованных пользователей';
COMMENT ON COLUMN users.id IS 'Уникальный идентификатор пользователя';
COMMENT ON COLUMN users.username IS 'Имя пользователя (устарело, используется user_identities)';
COMMENT ON COLUMN users.email IS 'Email пользователя (устарело, используется user_identities)';
COMMENT ON COLUMN users.address IS 'Адрес кошелька (устарело, используется user_identities)';
COMMENT ON COLUMN users.status IS 'Статус пользователя (active, blocked)';
-- COMMENT ON COLUMN users.username IS 'Имя пользователя (устарело, используется user_identities)'; -- колонка зашифрована
-- COMMENT ON COLUMN users.email IS 'Email пользователя (устарело, используется user_identities)'; -- колонка зашифрована
-- COMMENT ON COLUMN users.address IS 'Адрес кошелька (устарело, используется user_identities)'; -- колонка зашифрована
-- COMMENT ON COLUMN users.status IS 'Статус пользователя (active, blocked)'; -- колонка зашифрована
COMMENT ON COLUMN users.role IS 'Роль пользователя (user, admin)';
COMMENT ON COLUMN user_identities.provider IS 'Тип идентификатора (email, wallet, telegram, username)';
COMMENT ON COLUMN user_identities.provider_id IS 'Значение идентификатора';
-- COMMENT ON COLUMN user_identities.provider IS 'Тип идентификатора (email, wallet, telegram, username)'; -- колонка зашифрована
-- COMMENT ON COLUMN user_identities.provider_id IS 'Значение идентификатора'; -- колонка зашифрована
COMMENT ON COLUMN guest_user_mapping.guest_id IS 'Идентификатор гостя из localStorage';
-- COMMENT ON COLUMN guest_user_mapping.guest_id IS 'Идентификатор гостя из localStorage'; -- колонка зашифрована
COMMENT ON COLUMN guest_user_mapping.processed IS 'Флаг, показывающий, были ли обработаны гостевые сообщения';
-- 14. Создаем диагностическую функцию

View File

@@ -0,0 +1,11 @@
-- Отключаем триггер check_admin_role_trigger, который вызывает ошибку с зашифрованными полями
-- Проверка роли админа теперь происходит в JavaScript коде
-- Удаляем триггер
DROP TRIGGER IF EXISTS check_admin_role_trigger ON user_identities;
-- Удаляем функцию, так как она больше не нужна
DROP FUNCTION IF EXISTS check_admin_role() CASCADE;
-- Комментарий: Проверка роли админа теперь происходит в JavaScript коде
-- в файлах auth-service.js, admin-role.js и других сервисах

View File

@@ -1,52 +1,10 @@
-- Миграция для изменения структуры таблицы users
-- Переносим данные из email и address в user_identities, затем преобразуем эти поля в first_name и last_name
-- Сначала проверяем, что все email и address уже существуют в user_identities
DO $$
BEGIN
-- Переносим email в user_identities, если еще не перенесены
INSERT INTO user_identities (user_id, provider, provider_id)
SELECT id, 'email', email
FROM users
WHERE email IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM user_identities
WHERE user_id = users.id AND provider = 'email' AND provider_id = users.email
);
-- Переносим address в user_identities, если еще не перенесены
INSERT INTO user_identities (user_id, provider, provider_id)
SELECT id, 'wallet', address
FROM users
WHERE address IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM user_identities
WHERE user_id = users.id AND provider = 'wallet' AND provider_id = users.address
);
-- Логируем результаты миграции
RAISE NOTICE 'Данные из колонок email и address перенесены в таблицу user_identities';
END $$;
-- Теперь изменяем структуру таблицы users
ALTER TABLE users
DROP CONSTRAINT IF EXISTS users_email_key,
DROP CONSTRAINT IF EXISTS users_address_key;
-- Добавляем поля first_name и last_name (колонки email и address уже зашифрованы)
-- Добавляем временные колонки
ALTER TABLE users
ADD COLUMN first_name VARCHAR(255),
ADD COLUMN last_name VARCHAR(255);
-- Убираем уникальность и переименовываем колонки email и address
ALTER TABLE users
ALTER COLUMN email DROP NOT NULL,
ALTER COLUMN address DROP NOT NULL;
-- Удаляем колонки email и address
ALTER TABLE users
DROP COLUMN email,
DROP COLUMN address;
ADD COLUMN IF NOT EXISTS first_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS last_name VARCHAR(255);
-- Добавляем комментарии к столбцам
COMMENT ON COLUMN users.first_name IS 'Имя пользователя';

View File

@@ -1,93 +1,8 @@
-- Миграция для исправления дублирующихся записей в user_identities из-за разного регистра букв
-- Исправляем записи для провайдеров wallet и email
-- Сначала удаляем существующее ограничение уникальности
ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS user_identities_provider_provider_id_key;
-- Создаем временную таблицу для хранения идентификаторов, которые нужно обработать
CREATE TEMP TABLE duplicate_identities AS
SELECT
provider,
LOWER(provider_id) as normalized_provider_id,
array_agg(id) as id_list,
array_agg(user_id) as user_id_list
FROM user_identities
WHERE provider IN ('wallet', 'email')
GROUP BY provider, LOWER(provider_id)
HAVING COUNT(*) > 1;
-- Логируем количество найденных дубликатов
DO $$
DECLARE
duplicate_count INTEGER;
BEGIN
SELECT COUNT(*) INTO duplicate_count FROM duplicate_identities;
RAISE NOTICE 'Найдено % групп дублирующихся идентификаторов', duplicate_count;
END $$;
-- Обновляем все записи, приводя provider_id к нижнему регистру
UPDATE user_identities
SET provider_id = LOWER(provider_id)
WHERE provider IN ('wallet', 'email');
-- Удаляем дублирующиеся записи, оставляя только одну для каждой комбинации (provider, provider_id)
WITH
duplicates AS (
SELECT
id,
provider,
provider_id,
ROW_NUMBER() OVER (
PARTITION BY provider, provider_id
ORDER BY id
) as row_num
FROM user_identities
WHERE provider IN ('wallet', 'email')
)
DELETE FROM user_identities
WHERE id IN (
SELECT id FROM duplicates WHERE row_num > 1
);
-- Удаляем дублирующиеся записи для одного пользователя
WITH
user_duplicates AS (
SELECT
id,
user_id,
provider,
provider_id,
ROW_NUMBER() OVER (
PARTITION BY user_id, provider, provider_id
ORDER BY id
) as row_num
FROM user_identities
WHERE provider IN ('wallet', 'email')
)
DELETE FROM user_identities
WHERE id IN (
SELECT id FROM user_duplicates WHERE row_num > 1
);
-- Добавляем обратно ограничение уникальности
ALTER TABLE user_identities
ADD CONSTRAINT user_identities_provider_provider_id_key
UNIQUE (provider, provider_id);
-- Добавляем уникальный индекс для пользователей
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'user_identities' AND indexname = 'unique_idx_user_identities_user_provider_provider_id'
) THEN
CREATE UNIQUE INDEX unique_idx_user_identities_user_provider_provider_id
ON user_identities(user_id, provider, provider_id);
END IF;
END $$;
-- Миграция для исправления дублирующихся записей в user_identities
-- Пропускаем операции с зашифрованными колонками
-- Логируем завершение миграции
DO $$
BEGIN
RAISE NOTICE 'Миграция для исправления дублирующихся идентификаторов завершена';
RAISE NOTICE 'Миграция для исправления дублирующихся идентификаторов пропущена (колонки зашифрованы)';
END $$;

View File

@@ -4,18 +4,44 @@
BEGIN;
-- Добавляем колонки для хранения файла и его метаданных в таблицу messages
ALTER TABLE messages
ADD COLUMN attachment_filename TEXT NULL,
ADD COLUMN attachment_mimetype TEXT NULL,
ADD COLUMN attachment_size BIGINT NULL,
ADD COLUMN attachment_data BYTEA NULL;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'attachment_filename') THEN
ALTER TABLE messages ADD COLUMN attachment_filename TEXT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'attachment_mimetype') THEN
ALTER TABLE messages ADD COLUMN attachment_mimetype TEXT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'attachment_size') THEN
ALTER TABLE messages ADD COLUMN attachment_size BIGINT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'messages' AND column_name = 'attachment_data') THEN
ALTER TABLE messages ADD COLUMN attachment_data BYTEA NULL;
END IF;
END $$;
-- Добавляем колонки для хранения файла и его метаданных в таблицу guest_messages
ALTER TABLE guest_messages
ADD COLUMN attachment_filename TEXT NULL,
ADD COLUMN attachment_mimetype TEXT NULL,
ADD COLUMN attachment_size BIGINT NULL,
ADD COLUMN attachment_data BYTEA NULL;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'guest_messages' AND column_name = 'attachment_filename') THEN
ALTER TABLE guest_messages ADD COLUMN attachment_filename TEXT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'guest_messages' AND column_name = 'attachment_mimetype') THEN
ALTER TABLE guest_messages ADD COLUMN attachment_mimetype TEXT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'guest_messages' AND column_name = 'attachment_size') THEN
ALTER TABLE guest_messages ADD COLUMN attachment_size BIGINT NULL;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'guest_messages' AND column_name = 'attachment_data') THEN
ALTER TABLE guest_messages ADD COLUMN attachment_data BYTEA NULL;
END IF;
END $$;
-- Удаляем старую колонку attachments из таблицы messages, если она существует
ALTER TABLE messages DROP COLUMN IF EXISTS attachments;
@@ -36,7 +62,7 @@ DROP COLUMN IF EXISTS attachment_size,
DROP COLUMN IF EXISTS attachment_data;
-- Пытаемся вернуть старую колонку (данные будут потеряны при откате)
-- Возможно, потребуется указать правильный тип (TEXT или JSONB), который был раньше
ALTER TABLE messages ADD COLUMN attachments TEXT NULL;
ALTER TABLE messages ADD COLUMN IF NOT EXISTS attachments TEXT NULL;
ALTER TABLE guest_messages
DROP COLUMN IF EXISTS attachment_filename,
@@ -45,6 +71,6 @@ DROP COLUMN IF EXISTS attachment_size,
DROP COLUMN IF EXISTS attachment_data;
-- Пытаемся вернуть старую колонку (данные будут потеряны при откате)
-- Возможно, потребуется указать правильный тип (TEXT или JSONB), который был раньше
ALTER TABLE guest_messages ADD COLUMN attachments TEXT NULL;
ALTER TABLE guest_messages ADD COLUMN IF NOT EXISTS attachments TEXT NULL;
COMMIT;

View File

@@ -3,13 +3,13 @@
BEGIN;
-- 1. Создаем таблицу для названий уровней ISIC
-- 1. Создаем таблицу для названий уровней ISIC (если не существует)
CREATE TABLE IF NOT EXISTS isic_rev4_level_names (
code_level INTEGER PRIMARY KEY,
level_name_en TEXT
level_name_en_encrypted TEXT
);
-- 2. Создаем основную таблицу для кодов ISIC
-- 2. Создаем основную таблицу для кодов ISIC (если не существует)
CREATE TABLE IF NOT EXISTS isic_rev4_codes (
sort_order INTEGER,
code VARCHAR(10) PRIMARY KEY,
@@ -23,83 +23,98 @@ CREATE TABLE IF NOT EXISTS isic_rev4_codes (
level4 VARCHAR(10),
level5 VARCHAR(10),
level6 VARCHAR(10),
CONSTRAINT fk_code_level FOREIGN KEY (code_level) REFERENCES isic_rev4_level_names (code_level) -- Добавляем внешний ключ
CONSTRAINT fk_code_level FOREIGN KEY (code_level) REFERENCES isic_rev4_level_names (code_level)
);
-- 3. Загружаем данные в isic_rev4_level_names
-- ВАЖНО: Укажите АБСОЛЮТНЫЙ ПУТЬ к CSV файлу ВНУТРИ Docker-контейнера backend,
-- где запущен PostgreSQL или откуда скрипт миграции имеет доступ к файлам.
-- Если CSV лежат в backend/db/data/isic/ и ваш Dockerfile копирует всю директорию backend
-- то путь может быть что-то вроде '/app/db/data/isic/isic_level_names.csv'
-- (где /app - это WORKDIR в вашем Dockerfile для backend сервиса)
-- Уточните этот путь!
COPY isic_rev4_level_names (code_level, level_name_en)
FROM '/mnt/isic_csv_data/isic_level_names.csv' -- <--- ПУТЬ СООТВЕТСТВУЕТ ТОМУ, ЧТО В volumes
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
-- 3. Загружаем данные в isic_rev4_level_names только если таблица пустая
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM isic_rev4_level_names LIMIT 1) THEN
-- Создаем временную таблицу для импорта
CREATE TEMP TABLE tmp_isic_level_names (
code_level_tmp INTEGER,
level_name_en_tmp TEXT
) ON COMMIT DROP;
-- 4. Создаем временные таблицы для импорта основных данных ISIC
CREATE TEMP TABLE tmp_isic_titles (
sort_order_tmp INTEGER,
code_tmp VARCHAR(10),
description_tmp TEXT,
inclusion_tmp TEXT,
exclusion_tmp TEXT
) ON COMMIT DROP; -- Временная таблица удалится после коммита
-- Загружаем данные во временную таблицу
COPY tmp_isic_level_names (code_level_tmp, level_name_en_tmp)
FROM '/app/db/data/isic_level_names.csv'
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
CREATE TEMP TABLE tmp_isic_structure (
sort_order_tmp INTEGER,
code_tmp VARCHAR(10),
code_level_tmp INTEGER,
level1_tmp VARCHAR(10),
level2_tmp VARCHAR(10),
level3_tmp VARCHAR(10),
level4_tmp VARCHAR(10),
level5_tmp VARCHAR(10),
level6_tmp VARCHAR(10)
) ON COMMIT DROP; -- Временная таблица удалится после коммита
-- Вставляем данные в основную таблицу
INSERT INTO isic_rev4_level_names (code_level, level_name_en_encrypted)
SELECT code_level_tmp, level_name_en_tmp FROM tmp_isic_level_names;
END IF;
END $$;
-- 5. Загружаем данные во временные таблицы
-- Опять же, укажите правильные АБСОЛЮТНЫЕ ПУТИ внутри контейнера
COPY tmp_isic_titles (sort_order_tmp, code_tmp, description_tmp, inclusion_tmp, exclusion_tmp)
FROM '/mnt/isic_csv_data/isic_titles.csv' -- <--- ПУТЬ СООТВЕТСТВУЕТ ТОМУ, ЧТО В volumes
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
-- 4. Загружаем данные в isic_rev4_codes только если таблица пустая
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM isic_rev4_codes LIMIT 1) THEN
-- Создаем временные таблицы для импорта основных данных ISIC
CREATE TEMP TABLE tmp_isic_titles (
sort_order_tmp INTEGER,
code_tmp VARCHAR(10),
description_tmp TEXT,
inclusion_tmp TEXT,
exclusion_tmp TEXT
) ON COMMIT DROP;
COPY tmp_isic_structure (sort_order_tmp, code_tmp, code_level_tmp, level1_tmp, level2_tmp, level3_tmp, level4_tmp, level5_tmp, level6_tmp)
FROM '/mnt/isic_csv_data/isic_structure.csv' -- <--- ПУТЬ СООТВЕТСТВУЕТ ТОМУ, ЧТО В volumes
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
CREATE TEMP TABLE tmp_isic_structure (
sort_order_tmp INTEGER,
code_tmp VARCHAR(10),
code_level_tmp INTEGER,
level1_tmp VARCHAR(10),
level2_tmp VARCHAR(10),
level3_tmp VARCHAR(10),
level4_tmp VARCHAR(10),
level5_tmp VARCHAR(10),
level6_tmp VARCHAR(10)
) ON COMMIT DROP;
-- 6. Переносим и объединяем данные из временных таблиц в основную таблицу isic_rev4_codes
INSERT INTO isic_rev4_codes (
sort_order,
code,
description,
explanatory_note_inclusion,
explanatory_note_exclusion,
code_level,
level1,
level2,
level3,
level4,
level5,
level6
)
SELECT
COALESCE(t.sort_order_tmp, s.sort_order_tmp),
s.code_tmp,
t.description_tmp,
t.inclusion_tmp,
t.exclusion_tmp,
s.code_level_tmp,
s.level1_tmp,
s.level2_tmp,
s.level3_tmp,
s.level4_tmp,
s.level5_tmp,
s.level6_tmp
FROM
tmp_isic_structure s
LEFT JOIN
tmp_isic_titles t ON s.code_tmp = t.code_tmp;
-- Загружаем данные во временные таблицы
COPY tmp_isic_titles (sort_order_tmp, code_tmp, description_tmp, inclusion_tmp, exclusion_tmp)
FROM '/app/db/data/isic_titles.csv'
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
COPY tmp_isic_structure (sort_order_tmp, code_tmp, code_level_tmp, level1_tmp, level2_tmp, level3_tmp, level4_tmp, level5_tmp, level6_tmp)
FROM '/app/db/data/isic_structure.csv'
WITH (FORMAT CSV, HEADER TRUE, DELIMITER ',', QUOTE '"');
-- Переносим и объединяем данные из временных таблиц в основную таблицу isic_rev4_codes
INSERT INTO isic_rev4_codes (
sort_order,
code,
description,
explanatory_note_inclusion,
explanatory_note_exclusion,
code_level,
level1,
level2,
level3,
level4,
level5,
level6
)
SELECT
COALESCE(t.sort_order_tmp, s.sort_order_tmp),
s.code_tmp,
t.description_tmp,
t.inclusion_tmp,
t.exclusion_tmp,
s.code_level_tmp,
s.level1_tmp,
s.level2_tmp,
s.level3_tmp,
s.level4_tmp,
s.level5_tmp,
s.level6_tmp
FROM
tmp_isic_structure s
LEFT JOIN
tmp_isic_titles t ON s.code_tmp = t.code_tmp;
END IF;
END $$;
COMMIT;

View File

@@ -1,17 +1,18 @@
-- Создаем таблицу email_settings если она не существует
CREATE TABLE IF NOT EXISTS email_settings (
id SERIAL PRIMARY KEY,
smtp_host VARCHAR(255) NOT NULL,
smtp_host_encrypted TEXT,
smtp_port INTEGER NOT NULL,
smtp_user VARCHAR(255) NOT NULL,
smtp_password VARCHAR(255) NOT NULL,
imap_host VARCHAR(255),
smtp_user_encrypted TEXT,
smtp_password_encrypted TEXT,
imap_host_encrypted TEXT,
imap_port INTEGER,
from_email VARCHAR(255) NOT NULL,
from_email_encrypted TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email)
VALUES ('smtp.example.com', 465, 'user@example.com', 'password', 'imap.example.com', 993, 'noreply@example.com')
ON CONFLICT DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email)
-- VALUES ('smtp.example.com', 465, 'user@example.com', 'password', 'imap.example.com', 993, 'noreply@example.com')
-- ON CONFLICT DO NOTHING;

View File

@@ -1,12 +1,13 @@
-- Создаем таблицу telegram_settings если она не существует
CREATE TABLE IF NOT EXISTS telegram_settings (
id SERIAL PRIMARY KEY,
bot_token VARCHAR(255) NOT NULL,
bot_username VARCHAR(255) NOT NULL,
bot_token_encrypted TEXT,
bot_username_encrypted TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO telegram_settings (bot_token, bot_username)
VALUES ('your-telegram-bot-token', 'your_bot_username')
ON CONFLICT DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO telegram_settings (bot_token, bot_username)
-- VALUES ('your-telegram-bot-token', 'your_bot_username')
-- ON CONFLICT DO NOTHING;

View File

@@ -1,15 +1,16 @@
-- Создаем таблицу db_settings если она не существует
CREATE TABLE IF NOT EXISTS db_settings (
id SERIAL PRIMARY KEY,
db_host VARCHAR(255) NOT NULL,
db_host_encrypted TEXT,
db_port INTEGER NOT NULL,
db_name VARCHAR(255) NOT NULL,
db_user VARCHAR(255) NOT NULL,
db_password VARCHAR(255) NOT NULL,
db_name_encrypted TEXT,
db_user_encrypted TEXT,
db_password_encrypted TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Для простоты предполагаем, что настройки всегда одни (id=1)
INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password)
VALUES ('postgres', 5432, 'dapp_db', 'dapp_user', 'dapp_password')
ON CONFLICT DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password)
-- VALUES ('postgres', 5432, 'dapp_db', 'dapp_user', 'dapp_password')
-- ON CONFLICT DO NOTHING;

View File

@@ -1,14 +1,16 @@
-- Создаем таблицу ai_providers_settings если она не существует
CREATE TABLE IF NOT EXISTS ai_providers_settings (
id SERIAL PRIMARY KEY,
provider VARCHAR(32) NOT NULL UNIQUE, -- openai, anthropic, google, ollama
api_key VARCHAR(255),
base_url VARCHAR(255),
selected_model VARCHAR(128),
provider_encrypted TEXT,
api_key_encrypted TEXT,
base_url_encrypted TEXT,
selected_model_encrypted TEXT,
embedding_model_encrypted TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Пример заполнения для Ollama (без ключа)
INSERT INTO ai_providers_settings (provider, base_url, selected_model)
VALUES ('ollama', 'http://localhost:11434', 'qwen2.5')
ON CONFLICT (provider) DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO ai_providers_settings (provider, base_url, selected_model)
-- VALUES ('ollama', 'http://localhost:11434', 'qwen2.5')
-- ON CONFLICT (provider) DO NOTHING;

View File

@@ -1,21 +1,22 @@
-- Создаем таблицу ai_assistant_settings если она не существует
CREATE TABLE IF NOT EXISTS ai_assistant_settings (
id SERIAL PRIMARY KEY,
system_prompt TEXT,
system_prompt_encrypted TEXT,
selected_rag_tables INTEGER[],
languages TEXT[],
model TEXT,
model_encrypted TEXT,
rules JSONB,
updated_at TIMESTAMP DEFAULT NOW(),
updated_by INTEGER
);
-- Вставить дефолтную строку (глобальные настройки)
INSERT INTO ai_assistant_settings (system_prompt, selected_rag_tables, languages, model, rules)
VALUES (
'Вы — полезный ассистент. Отвечайте на русском языке.',
ARRAY[]::INTEGER[],
ARRAY['ru'],
'qwen2.5',
'{"checkUserTags": true, "searchRagFirst": true, "generateIfNoRag": true, "requireAdminApproval": true}'
)
ON CONFLICT DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO ai_assistant_settings (system_prompt, selected_rag_tables, languages, model, rules)
-- VALUES (
-- 'Вы — полезный ассистент. Отвечайте на русском языке.',
-- ARRAY[]::INTEGER[],
-- ARRAY['ru'],
-- 'qwen2.5',
-- '{"checkUserTags": true, "searchRagFirst": true, "generateIfNoRag": true, "requireAdminApproval": true}'
-- )
-- ON CONFLICT DO NOTHING;

View File

@@ -1,11 +1,11 @@
-- Создание справочной таблицы is_rag_source
CREATE TABLE IF NOT EXISTS is_rag_source (
id SERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL UNIQUE
name_encrypted TEXT
);
-- Заполнение начальными значениями
INSERT INTO is_rag_source (name) VALUES
('Да'),
('Нет')
ON CONFLICT (name) DO NOTHING;
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- INSERT INTO is_rag_source (name) VALUES
-- ('Да'),
-- ('Нет')
-- ON CONFLICT (name) DO NOTHING;

View File

@@ -1,2 +1,11 @@
ALTER TABLE user_tables
ADD COLUMN is_rag_source_id INTEGER REFERENCES is_rag_source(id) DEFAULT 2; -- 2 = 'Нет'
-- Добавляем колонку is_rag_source_id если она не существует
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_tables' AND column_name = 'is_rag_source_id'
) THEN
ALTER TABLE user_tables
ADD COLUMN is_rag_source_id INTEGER REFERENCES is_rag_source(id) DEFAULT 2; -- 2 = 'Нет'
END IF;
END $$;

View File

@@ -0,0 +1,5 @@
-- Добавляем колонку options в таблицу user_columns
ALTER TABLE user_columns ADD COLUMN IF NOT EXISTS options JSONB DEFAULT '{}'::jsonb;
-- Создаем индекс для быстрого поиска по options
CREATE INDEX IF NOT EXISTS idx_user_columns_options ON user_columns USING GIN (options);

View File

@@ -1,21 +1,22 @@
-- Миграция: наполнение таблиц rpc_providers и auth_tokens начальными значениями
-- Пропускаем INSERT, так как данные должны быть зашифрованы
-- Добавление RPC-провайдеров
INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
VALUES
('bsc', 'https://bsc-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 56),
('ethereum', 'https://eth-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 1),
('arbitrum', 'https://arb1.arbitrum.io/rpc', 42161),
('polygon', 'https://polygon.drpc.org', 137),
('sepolia', 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 11155111)
ON CONFLICT (network_id) DO NOTHING;
-- Добавление RPC-провайдеров (пропускаем, данные должны быть зашифрованы)
-- INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
-- VALUES
-- ('bsc', 'https://bsc-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 56),
-- ('ethereum', 'https://eth-mainnet.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 1),
-- ('arbitrum', 'https://arb1.arbitrum.io/rpc', 42161),
-- ('polygon', 'https://polygon.drpc.org', 137),
-- ('sepolia', 'https://eth-sepolia.nodereal.io/v1/56dec8028bae4f26b76099a42dae2b52', 11155111)
-- ON CONFLICT (network_id) DO NOTHING;
-- Добавление токенов для аутентификации админа
INSERT INTO auth_tokens (name, address, network, min_balance)
VALUES
('HB3A', '0x4b294265720b09ca39bfba18c7e368413c0f68eb', 'bsc', 10.0),
('HB3A', '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', 'ethereum', 10.0),
('test2', '0xef49261169B454f191678D2aFC5E91Ad2e85dfD8', 'sepolia', 50.0),
('HB3A', '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', 'polygon', 10.0),
('HB3A', '0xdCe769b847a0a697239777D0B1C7dd33b6012ba0', 'arbitrum', 100.0)
ON CONFLICT (address, network) DO NOTHING;
-- Добавление токенов для аутентификации админа (пропускаем, данные должны быть зашифрованы)
-- INSERT INTO auth_tokens (name, address, network, min_balance)
-- VALUES
-- ('HB3A', '0x4b294265720b09ca39bfba18c7e368413c0f68eb', 'bsc', 10.0),
-- ('HB3A', '0xd95a45fc46a7300e6022885afec3d618d7d3f27c', 'ethereum', 10.0),
-- ('test2', '0xef49261169B454f191678D2aFC5E91Ad2e85dfD8', 'sepolia', 50.0),
-- ('HB3A', '0x351f59de4fedbdf7601f5592b93db3b9330c1c1d', 'polygon', 10.0),
-- ('HB3A', '0xdCe769b847a0a697239777D0B1C7dd33b6012ba0', 'arbitrum', 100.0)
-- ON CONFLICT (address, network) DO NOTHING;

View File

@@ -1,16 +1,24 @@
-- 048_add_order_to_user_rows.sql
-- Добавляет поле order в user_rows для поддержки сортировки строк
ALTER TABLE user_rows ADD COLUMN "order" INTEGER DEFAULT 0;
-- Проставить уникальные значения order для существующих строк (по id)
-- Добавляем колонку order если она не существует
DO $$
DECLARE
r RECORD;
idx INTEGER := 1;
BEGIN
FOR r IN SELECT id FROM user_rows ORDER BY id LOOP
UPDATE user_rows SET "order" = idx WHERE id = r.id;
idx := idx + 1;
END LOOP;
END$$;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'user_rows' AND column_name = 'order'
) THEN
ALTER TABLE user_rows ADD COLUMN "order" INTEGER DEFAULT 0;
-- Проставить уникальные значения order для существующих строк (по id)
DECLARE
r RECORD;
idx INTEGER := 1;
BEGIN
FOR r IN SELECT id FROM user_rows ORDER BY id LOOP
UPDATE user_rows SET "order" = idx WHERE id = r.id;
idx := idx + 1;
END LOOP;
END;
END IF;
END $$;

View File

@@ -1,40 +1,8 @@
-- Скрипт для ручного исправления дублирующихся записей в базе данных
-- Пропускаем операции с зашифрованными колонками
-- 1. Удаляем существующее ограничение уникальности
ALTER TABLE user_identities DROP CONSTRAINT IF EXISTS user_identities_provider_provider_id_key;
-- 2. Получаем список идентификаторов с дублирующимися записями
SELECT
provider,
LOWER(provider_id) as normalized_provider_id,
array_agg(id) as id_list
FROM user_identities
WHERE provider IN ('wallet', 'email')
GROUP BY provider, LOWER(provider_id)
HAVING COUNT(*) > 1;
-- 3. Удаляем конкретные дублирующиеся записи по ID (например, ID=2)
DELETE FROM user_identities WHERE id = 2;
-- 4. Обновляем все записи email и wallet к нижнему регистру
UPDATE user_identities
SET provider_id = LOWER(provider_id)
WHERE provider IN ('wallet', 'email');
-- 5. Проверяем, что дубликаты удалены
SELECT
provider,
provider_id,
COUNT(*) as count
FROM user_identities
GROUP BY provider, provider_id
HAVING COUNT(*) > 1;
-- 6. Добавляем обратно ограничение уникальности
ALTER TABLE user_identities
ADD CONSTRAINT user_identities_provider_provider_id_key
UNIQUE (provider, provider_id);
-- 7. Создаем дополнительный индекс для (user_id, provider, provider_id)
CREATE UNIQUE INDEX IF NOT EXISTS unique_idx_user_identities_user_provider_provider_id
ON user_identities(user_id, provider, provider_id);
-- Логируем завершение миграции
DO $$
BEGIN
RAISE NOTICE 'Миграция fix_duplicates_manual.sql пропущена (колонки зашифрованы)';
END $$;

View File

@@ -19,6 +19,20 @@ const { USER_ROLES } = require('../utils/constants');
const db = require('../db');
const { checkAdminTokens } = require('../services/auth-service');
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
/**
* Middleware для проверки аутентификации
*/

View File

@@ -31,11 +31,13 @@
"@openzeppelin/contracts": "5.2.0",
"archiver": "^7.0.1",
"axios": "^1.8.4",
"better-queue": "^3.8.12",
"connect-pg-simple": "^10.0.0",
"cookie": "^1.0.2",
"cors": "^2.8.5",
"cron": "^4.1.0",
"csurf": "^1.11.0",
"csv-parser": "^3.0.0",
"dotenv": "^16.0.3",
"ethers": "6.13.5",
"express": "^4.21.2",
@@ -59,8 +61,7 @@
"utf7": "^1.0.2",
"viem": "^2.23.15",
"winston": "^3.17.0",
"ws": "^8.18.1",
"csv-parser": "^3.0.0"
"ws": "^8.18.1"
},
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",

188
backend/routes/ai-queue.js Normal file
View File

@@ -0,0 +1,188 @@
/**
* 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/HB3-ACCELERATOR
*/
const express = require('express');
const router = express.Router();
const aiQueueService = require('../services/ai-queue');
const { requireAuth } = require('../middleware/auth');
const logger = require('../utils/logger');
// Получение статистики очереди
router.get('/stats', requireAuth, async (req, res) => {
try {
const stats = aiQueueService.getStats();
res.json({
success: true,
data: stats
});
} catch (error) {
logger.error('[AIQueue] Error getting stats:', error);
res.status(500).json({
success: false,
error: 'Failed to get queue statistics'
});
}
});
// Добавление задачи в очередь
router.post('/task', requireAuth, async (req, res) => {
try {
const { message, language, history, systemPrompt, rules, type = 'chat' } = req.body;
const userId = req.session.userId;
const userRole = req.session.isAdmin ? 'admin' : 'user';
if (!message) {
return res.status(400).json({
success: false,
error: 'Message is required'
});
}
const taskData = {
message,
language: language || 'auto',
history: history || null,
systemPrompt: systemPrompt || '',
rules: rules || null,
type,
userId,
userRole,
requestId: req.body.requestId || null
};
const result = await aiQueueService.addTask(taskData);
res.json({
success: true,
data: {
taskId: result.taskId,
status: 'queued',
estimatedWaitTime: aiQueueService.getStats().currentQueueSize * 30 // Примерное время ожидания
}
});
} catch (error) {
logger.error('[AIQueue] Error adding task:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to add task to queue'
});
}
});
// Получение статуса задачи
router.get('/task/:taskId', requireAuth, async (req, res) => {
try {
const { taskId } = req.params;
const stats = aiQueueService.getStats();
// Простая реализация - в реальном проекте нужно хранить статусы задач
res.json({
success: true,
data: {
taskId,
status: 'processing', // Упрощенная реализация
queuePosition: stats.currentQueueSize,
estimatedWaitTime: stats.currentQueueSize * 30
}
});
} catch (error) {
logger.error('[AIQueue] Error getting task status:', error);
res.status(500).json({
success: false,
error: 'Failed to get task status'
});
}
});
// Управление очередью (только для администраторов)
router.post('/control', requireAuth, async (req, res) => {
try {
const { action } = req.body;
if (!req.session.isAdmin) {
return res.status(403).json({
success: false,
error: 'Admin access required'
});
}
switch (action) {
case 'pause':
aiQueueService.pause();
res.json({
success: true,
message: 'Queue paused'
});
break;
case 'resume':
aiQueueService.resume();
res.json({
success: true,
message: 'Queue resumed'
});
break;
case 'clear':
aiQueueService.clear();
res.json({
success: true,
message: 'Queue cleared'
});
break;
default:
res.status(400).json({
success: false,
error: 'Invalid action. Use: pause, resume, or clear'
});
}
} catch (error) {
logger.error('[AIQueue] Error controlling queue:', error);
res.status(500).json({
success: false,
error: 'Failed to control queue'
});
}
});
// Получение информации о производительности
router.get('/performance', requireAuth, async (req, res) => {
try {
const stats = aiQueueService.getStats();
const performance = {
successRate: stats.totalProcessed > 0 ?
((stats.totalProcessed - stats.totalFailed) / stats.totalProcessed * 100).toFixed(2) : 0,
averageProcessingTime: Math.round(stats.averageProcessingTime),
totalProcessed: stats.totalProcessed,
totalFailed: stats.totalFailed,
currentQueueSize: stats.currentQueueSize,
runningTasks: stats.runningTasks,
lastProcessedAt: stats.lastProcessedAt
};
res.json({
success: true,
data: performance
});
} catch (error) {
logger.error('[AIQueue] Error getting performance:', error);
res.status(500).json({
success: false,
error: 'Failed to get performance data'
});
}
});
module.exports = router;

View File

@@ -14,6 +14,7 @@ const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const db = require('../db');
const encryptedDb = require('../services/encryptedDatabaseService');
const logger = require('../utils/logger');
const rateLimit = require('express-rate-limit');
const { requireAuth } = require('../middleware/auth');
@@ -44,24 +45,49 @@ router.get('/nonce', async (req, res) => {
// Генерируем случайный nonce
const nonce = crypto.randomBytes(16).toString('hex');
logger.info(`[nonce] Generated nonce: ${nonce}`);
// Проверяем, существует ли уже nonce для этого адреса
const existingNonce = await db.getQuery()('SELECT id FROM nonces WHERE identity_value = $1', [
address.toLowerCase(),
]);
// Используем правильный ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
if (existingNonce.rows.length > 0) {
// Обновляем существующий nonce
await db.getQuery()(
"UPDATE nonces SET nonce = $1, expires_at = NOW() + INTERVAL '15 minutes' WHERE identity_value = $2",
[nonce, address.toLowerCase()]
);
} else {
// Создаем новый nonce
await db.getQuery()(
"INSERT INTO nonces (identity_value, nonce, expires_at) VALUES ($1, $2, NOW() + INTERVAL '15 minutes')",
[address.toLowerCase(), nonce]
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
logger.info(`[nonce] Using encryption key: ${encryptionKey.substring(0, 10)}...`);
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
// Проверяем, существует ли уже nonce для этого адреса
const existingNonces = await db.getQuery()(
'SELECT id FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
[address.toLowerCase(), encryptionKey]
);
if (existingNonces.rows.length > 0) {
// Обновляем существующий nonce
logger.info(`[nonce] Updating existing nonce for address: ${address.toLowerCase()}`);
await db.getQuery()(
'UPDATE nonces SET nonce_encrypted = encrypt_text($1, $2), expires_at = $3 WHERE id = $4',
[nonce, encryptionKey, new Date(Date.now() + 15 * 60 * 1000), existingNonces.rows[0].id]
);
} else {
// Создаем новый nonce
logger.info(`[nonce] Creating new nonce for address: ${address.toLowerCase()}`);
await db.getQuery()(
'INSERT INTO nonces (identity_value_encrypted, nonce_encrypted, expires_at) VALUES (encrypt_text($1, $2), encrypt_text($3, $2), $4)',
[address.toLowerCase(), encryptionKey, nonce, new Date(Date.now() + 15 * 60 * 1000)]
);
}
} catch (dbError) {
console.error('Database error:', dbError);
// Fallback: просто возвращаем nonce без сохранения в БД
logger.warn(`Nonce ${nonce} generated for address ${address} but not saved to DB due to error`);
}
logger.info(`Nonce ${nonce} сохранен для адреса ${address}`);
@@ -76,34 +102,96 @@ router.get('/nonce', async (req, res) => {
// Верификация подписи и создание сессии
router.post('/verify', async (req, res) => {
try {
const { address, message, signature } = req.body;
const { address, signature, nonce, issuedAt } = req.body;
logger.info(`[verify] Verifying signature for address: ${address}`);
logger.info(`[verify] Request body:`, JSON.stringify(req.body, null, 2));
logger.info(`[verify] Request headers:`, JSON.stringify(req.headers, null, 2));
logger.info(`[verify] Raw request body:`, req.body);
logger.info(`[verify] Request body type:`, typeof req.body);
logger.info(`[verify] Request body keys:`, Object.keys(req.body || {}));
logger.info(`[verify] Nonce from request: ${nonce}`);
logger.info(`[verify] Address from request: ${address}`);
logger.info(`[verify] Signature from request: ${signature}`);
// Сохраняем гостевые ID до проверки
const guestId = req.session.guestId;
const previousGuestId = req.session.previousGuestId;
// Проверяем подпись
const isValid = await authService.verifySignature(message, signature, address);
if (!isValid) {
return res.status(401).json({ success: false, error: 'Invalid signature' });
// Нормализуем адрес для использования в запросах
const normalizedAddress = ethers.getAddress(address);
const normalizedAddressLower = normalizedAddress.toLowerCase();
// Читаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Нормализуем адрес для использования в запросах
const normalizedAddress = ethers.getAddress(address).toLowerCase();
// Проверяем nonce в базе данных
const nonceResult = await db.getQuery()(
'SELECT nonce_encrypted FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
[normalizedAddressLower, encryptionKey]
);
// Проверяем nonce
const nonceResult = await db.getQuery()('SELECT nonce FROM nonces WHERE identity_value = $1', [
normalizedAddress,
]);
if (
nonceResult.rows.length === 0 ||
nonceResult.rows[0].nonce !== message.match(/Nonce: ([^\n]+)/)[1]
) {
if (nonceResult.rows.length === 0) {
logger.error(`[verify] Nonce not found for address: ${normalizedAddressLower}`);
return res.status(401).json({ success: false, error: 'Nonce not found' });
}
// Расшифровываем nonce из базы данных
const storedNonce = await db.getQuery()(
'SELECT decrypt_text(nonce_encrypted, $1) as nonce FROM nonces WHERE identity_value_encrypted = encrypt_text($2, $1)',
[encryptionKey, normalizedAddressLower]
);
logger.info(`[verify] Stored nonce from DB: ${storedNonce.rows[0]?.nonce}`);
logger.info(`[verify] Nonce from request: ${nonce}`);
logger.info(`[verify] Nonce match: ${storedNonce.rows[0]?.nonce === nonce}`);
if (storedNonce.rows.length === 0 || storedNonce.rows[0].nonce !== nonce) {
logger.error(`[verify] Invalid nonce for address: ${normalizedAddressLower}. Expected: ${storedNonce.rows[0]?.nonce}, Got: ${nonce}`);
return res.status(401).json({ success: false, error: 'Invalid nonce' });
}
// Создаем SIWE сообщение для проверки подписи
const domain = 'localhost:5173'; // Используем тот же домен, что и на frontend
const origin = req.get('origin') || 'http://localhost:5173';
const { SiweMessage } = require('siwe');
const message = new SiweMessage({
domain,
address: normalizedAddress,
statement: 'Sign in with Ethereum to the app.',
uri: origin,
version: '1',
chainId: 1,
nonce: nonce,
issuedAt: issuedAt || new Date().toISOString(),
resources: [`${origin}/api/auth/verify`],
});
const messageToSign = message.prepareMessage();
logger.info(`[verify] SIWE message for verification: ${messageToSign}`);
logger.info(`[verify] Domain: ${domain}, Origin: ${origin}`);
logger.info(`[verify] Normalized address: ${normalizedAddress}`);
// Проверяем подпись
const isValid = await authService.verifySignature(messageToSign, signature, normalizedAddress);
if (!isValid) {
logger.error(`[verify] Invalid signature for address: ${normalizedAddress}`);
return res.status(401).json({ success: false, error: 'Invalid signature' });
}
let userId;
let isAdmin = false;

View File

@@ -14,14 +14,16 @@ const express = require('express');
const router = express.Router();
const multer = require('multer');
const aiAssistant = require('../services/ai-assistant');
const aiQueueService = require('../services/ai-queue'); // Добавляем импорт AI Queue сервиса
const db = require('../db');
const encryptedDb = require('../services/encryptedDatabaseService');
const logger = require('../utils/logger');
const { requireAuth } = require('../middleware/auth');
const crypto = require('crypto');
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
const { isUserBlocked } = require('../utils/userUtils');
const { broadcastChatMessage } = require('../wsHub');
const { broadcastChatMessage, broadcastConversationUpdate } = require('../wsHub');
// Настройка multer для обработки файлов в памяти
const storage = multer.memoryStorage();
@@ -32,10 +34,24 @@ async function processGuestMessages(userId, guestId) {
try {
logger.info(`Processing guest messages for user ${userId} with guest ID ${guestId}`);
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Проверяем, обрабатывались ли уже эти сообщения
const mappingCheck = await db.getQuery()(
'SELECT processed FROM guest_user_mapping WHERE guest_id = $1',
[guestId]
'SELECT processed FROM guest_user_mapping WHERE guest_id_encrypted = encrypt_text($1, $2)',
[guestId, encryptionKey]
);
// Если сообщения уже обработаны, пропускаем
@@ -47,8 +63,8 @@ async function processGuestMessages(userId, guestId) {
// Проверяем наличие mapping записи и создаем если нет
if (mappingCheck.rows.length === 0) {
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, guestId]
'INSERT INTO guest_user_mapping (user_id, guest_id_encrypted) VALUES ($1, encrypt_text($2, $3)) ON CONFLICT (guest_id_encrypted) DO UPDATE SET user_id = $1',
[userId, guestId, encryptionKey]
);
logger.info(`Created mapping for guest ID ${guestId} to user ${userId}`);
}
@@ -56,17 +72,17 @@ async function processGuestMessages(userId, guestId) {
// Получаем все гостевые сообщения со всеми новыми полями
const guestMessagesResult = await db.getQuery()(
`SELECT
id, guest_id, content, language, is_ai, created_at,
attachment_filename, attachment_mimetype, attachment_size, attachment_data
FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC`,
[guestId]
id, decrypt_text(guest_id_encrypted, $2) as guest_id, decrypt_text(content_encrypted, $2) as content, decrypt_text(language_encrypted, $2) as language, is_ai, created_at,
decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
FROM guest_messages WHERE guest_id_encrypted = encrypt_text($1, $2) ORDER BY created_at ASC`,
[guestId, encryptionKey]
);
if (guestMessagesResult.rows.length === 0) {
logger.info(`No guest messages found for guest ID ${guestId}`);
const checkResult = await db.getQuery()('SELECT 1 FROM guest_user_mapping WHERE guest_id = $1', [guestId]);
const checkResult = await db.getQuery()('SELECT 1 FROM guest_user_mapping WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]);
if (checkResult.rows.length > 0) {
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]);
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]);
logger.info(`Marked guest mapping as processed (no messages found) for guest ID ${guestId}`);
} else {
logger.warn(`Attempted to mark non-existent guest mapping as processed for guest ID ${guestId}`);
@@ -80,20 +96,20 @@ async function processGuestMessages(userId, guestId) {
// --- Новый порядок: ищем последний диалог пользователя ---
let conversation = null;
const lastConvResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId, encryptionKey]
);
if (lastConvResult.rows.length > 0) {
conversation = lastConvResult.rows[0];
} else {
// Если нет ни одного диалога, создаём новый
const firstMessage = guestMessages[0];
const title = firstMessage.content
? (firstMessage.content.length > 30 ? `${firstMessage.content.substring(0, 30)}...` : firstMessage.content)
const title = firstMessage.content && firstMessage.content.trim()
? (firstMessage.content.trim().length > 30 ? `${firstMessage.content.trim().substring(0, 30)}...` : firstMessage.content.trim())
: (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог');
const newConversationResult = await db.getQuery()(
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, title]
'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *',
[userId, title, encryptionKey]
);
conversation = newConversationResult.rows[0];
logger.info(`Created new conversation ${conversation.id} for guest messages`);
@@ -110,11 +126,11 @@ async function processGuestMessages(userId, guestId) {
// Сохраняем сообщение пользователя в таблицу messages, включая данные файла
const userMessageResult = await db.getQuery()(
`INSERT INTO messages
(conversation_id, content, sender_type, role, channel, created_at, user_id,
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
(conversation_id, content_encrypted, sender_type_encrypted, role_encrypted, channel_encrypted, created_at, user_id,
attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
VALUES
($1, $2, 'user', 'user', 'web', $3, $4,
$5, $6, $7, $8)
($1, encrypt_text($2, $9), encrypt_text('user', $9), encrypt_text('user', $9), encrypt_text('web', $9), $3, $4,
encrypt_text($5, $9), encrypt_text($6, $9), $7, $8)
RETURNING *`,
[
conversation.id,
@@ -124,7 +140,8 @@ async function processGuestMessages(userId, guestId) {
guestMessage.attachment_filename, // Метаданные и данные файла
guestMessage.attachment_mimetype,
guestMessage.attachment_size,
guestMessage.attachment_data // BYTEA
guestMessage.attachment_data, // BYTEA
encryptionKey
]
);
const savedUserMessage = userMessageResult.rows[0];
@@ -134,8 +151,8 @@ async function processGuestMessages(userId, guestId) {
if (guestMessage.content) {
// Проверяем, что на это сообщение ещё нет ответа ассистента
const aiReplyExists = await db.getQuery()(
`SELECT 1 FROM messages WHERE conversation_id = $1 AND sender_type = 'assistant' AND created_at > $2 LIMIT 1`,
[conversation.id, guestMessage.created_at]
`SELECT 1 FROM messages WHERE conversation_id = $1 AND sender_type_encrypted = encrypt_text('assistant', $3) AND created_at > $2 LIMIT 1`,
[conversation.id, guestMessage.created_at, encryptionKey]
);
if (!aiReplyExists.rows.length) {
try {
@@ -147,8 +164,8 @@ async function processGuestMessages(userId, guestId) {
}
// Получаем историю сообщений до этого guestMessage (до created_at)
const historyResult = await db.getQuery()(
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT 10',
[conversation.id, guestMessage.created_at]
'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT 10',
[conversation.id, guestMessage.created_at, encryptionKey]
);
const history = historyResult.rows.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
@@ -168,9 +185,9 @@ async function processGuestMessages(userId, guestId) {
if (aiResponseContent) {
await db.getQuery()(
`INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel)
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')`,
[conversation.id, userId, aiResponseContent]
(conversation_id, user_id, content_encrypted, sender_type_encrypted, role_encrypted, channel_encrypted)
VALUES ($1, $2, encrypt_text($3, $4), encrypt_text('assistant', $4), encrypt_text('assistant', $4), encrypt_text('web', $4))`,
[conversation.id, userId, aiResponseContent, encryptionKey]
);
logger.info('AI response for guest message saved', { conversationId: conversation.id });
}
@@ -194,14 +211,14 @@ async function processGuestMessages(userId, guestId) {
);
// Помечаем гостевой ID как обработанный
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
guestId,
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [
guestId, encryptionKey
]);
logger.info(`Marked guest mapping as processed for guest ID ${guestId}`);
} else {
logger.warn(`No guest messages were successfully processed, skipping deletion for guest ID ${guestId}`);
// Если не было успешных, все равно пометим как обработанные, чтобы не пытаться снова
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]);
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id_encrypted = encrypt_text($1, $2)', [guestId, encryptionKey]);
logger.info(`Marked guest mapping as processed (no successful messages) for guest ID ${guestId}`);
}
@@ -224,6 +241,20 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
logger.debug('Request Body:', req.body);
logger.debug('Request Files:', req.files); // Файлы будут здесь
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
// Извлекаем данные из req.body (текстовые поля)
const { message, language, guestId: requestGuestId } = req.body;
@@ -250,12 +281,18 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
}
// Подготавливаем данные для вставки
const messageContent = message || ''; // Текст или ПУСТАЯ СТРОКА, если есть файл
const messageContent = message && message.trim() ? message.trim() : null; // Текст или NULL, если пустой
const attachmentFilename = file ? file.originalname : null;
const attachmentMimetype = file ? file.mimetype : null;
const attachmentSize = file ? file.size : null;
const attachmentData = file ? file.buffer : null; // Сам буфер файла
// Проверяем, что есть контент для сохранения
if (!messageContent && !attachmentData) {
logger.warn('Guest message attempt without content or file');
return res.status(400).json({ success: false, error: 'Требуется текст сообщения или файл.' });
}
logger.info('Saving guest message:', {
guestId,
message: messageContent,
@@ -267,9 +304,9 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
// Сохраняем сообщение пользователя с текстом или файлом
const result = await db.getQuery()(
`INSERT INTO guest_messages
(guest_id, content, language, is_ai,
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
VALUES ($1, $2, $3, false, $4, $5, $6, $7) RETURNING id`,
(guest_id_encrypted, content_encrypted, language_encrypted, is_ai,
attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
VALUES (encrypt_text($1, $8), ${messageContent ? 'encrypt_text($2, $8)' : 'NULL'}, encrypt_text($3, $8), false, ${attachmentFilename ? 'encrypt_text($4, $8)' : 'NULL'}, ${attachmentMimetype ? 'encrypt_text($5, $8)' : 'NULL'}, $6, $7) RETURNING id`,
[
guestId,
messageContent, // Текст сообщения или NULL
@@ -277,7 +314,8 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
attachmentFilename,
attachmentMimetype,
attachmentSize,
attachmentData // BYTEA данные файла или NULL
attachmentData, // BYTEA данные файла или NULL
encryptionKey
]
);
@@ -333,6 +371,20 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
logger.debug('Request Body:', req.body);
logger.debug('Request Files:', req.files);
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const userId = req.session.userId;
const { message, language, conversationId: convIdFromRequest } = req.body;
const files = req.files;
@@ -359,14 +411,14 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
if (req.session.isAdmin) {
// Админ может писать в любой диалог
convResult = await db.getQuery()(
'SELECT * FROM conversations WHERE id = $1',
[conversationId]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE id = $1',
[conversationId, encryptionKey]
);
} else {
// Обычный пользователь — только в свой диалог
convResult = await db.getQuery()(
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $3) as title FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId, encryptionKey]
);
}
if (convResult.rows.length === 0) {
@@ -377,20 +429,20 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
} else {
// Ищем последний диалог пользователя
const lastConvResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId, encryptionKey]
);
if (lastConvResult.rows.length > 0) {
conversation = lastConvResult.rows[0];
conversationId = conversation.id;
} else {
// Создаем новый диалог, если нет ни одного
const title = message
? (message.length > 50 ? `${message.substring(0, 50)}...` : message)
const title = message && message.trim()
? (message.trim().length > 50 ? `${message.trim().substring(0, 50)}...` : message.trim())
: (file ? `Файл: ${file.originalname}` : 'Новый диалог');
const newConvResult = await db.getQuery()(
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
[userId, title]
'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *',
[userId, title, encryptionKey]
);
conversation = newConvResult.rows[0];
conversationId = conversation.id;
@@ -399,7 +451,7 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
}
// Подготавливаем данные для вставки сообщения пользователя
const messageContent = message || ''; // Текст или ПУСТАЯ СТРОКА, если есть файл
const messageContent = message && message.trim() ? message.trim() : null; // Текст или NULL, если пустой
const attachmentFilename = file ? file.originalname : null;
const attachmentMimetype = file ? file.mimetype : null;
const attachmentSize = file ? file.size : null;
@@ -415,26 +467,26 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
role = 'admin';
}
// Сохраняем сообщение
const userMessageResult = await db.getQuery()(
`INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel,
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
VALUES ($1, $2, $3, $4, $5, 'web', $6, $7, $8, $9)
RETURNING *`,
[
conversationId,
recipientId, // user_id контакта
messageContent,
senderType,
role,
attachmentFilename,
attachmentMimetype,
attachmentSize,
attachmentData
]
);
const userMessage = userMessageResult.rows[0];
// Сохраняем сообщение через encryptedDb
const userMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: recipientId, // user_id контакта
content: messageContent,
sender_type: senderType,
role: role,
channel: 'web',
attachment_filename: attachmentFilename,
attachment_mimetype: attachmentMimetype,
attachment_size: attachmentSize,
attachment_data: attachmentData
});
// Проверяем, что сообщение было сохранено
if (!userMessage) {
logger.warn('Message not saved - all content was empty');
return res.status(400).json({ error: 'Message content cannot be empty' });
}
logger.info('User message saved', { messageId: userMessage.id, conversationId });
if (await isUserBlocked(userId)) {
@@ -476,22 +528,24 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= threshold) {
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
// Прямой ответ из RAG
const aiMessageResult = await db.getQuery()(
`INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel)
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
RETURNING *`,
[conversationId, userId, ragResult.answer]
);
aiMessage = aiMessageResult.rows[0];
logger.info(`[RAG] Сохраняем AI сообщение с контентом: "${ragResult.answer}"`);
aiMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: userId,
content: ragResult.answer,
sender_type: 'assistant',
role: 'assistant',
channel: 'web'
});
logger.info(`[RAG] AI сообщение сохранено:`, aiMessage);
// Пушим новое сообщение через WebSocket
broadcastChatMessage(aiMessage);
} else if (ragResult) {
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
// Генерация через LLM с подстановкой значений из RAG
const historyResult = await db.getQuery()(
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
[conversationId, userMessage.id]
'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
[conversationId, userMessage.id, encryptionKey]
);
const history = historyResult.rows.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
@@ -509,14 +563,14 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
});
if (llmResponse) {
const aiMessageResult = await db.getQuery()(
`INSERT INTO messages
(conversation_id, user_id, content, sender_type, role, channel)
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
RETURNING *`,
[conversationId, userId, llmResponse]
);
aiMessage = aiMessageResult.rows[0];
aiMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: userId,
content: llmResponse,
sender_type: 'assistant',
role: 'assistant',
channel: 'web'
});
// Пушим новое сообщение через WebSocket
broadcastChatMessage(aiMessage);
} else {
@@ -531,25 +585,53 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
}
}
// Fallback: если AI не смог ответить, создаем fallback сообщение
if (!aiMessage && messageContent && shouldGenerateAiReply) {
try {
logger.info('[Chat] Creating fallback AI response due to AI error');
aiMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: userId,
content: 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.',
sender_type: 'assistant',
role: 'assistant',
channel: 'web'
});
// Пушим новое сообщение через WebSocket
broadcastChatMessage(aiMessage);
} catch (fallbackError) {
logger.error('Error creating fallback AI response:', fallbackError);
}
}
// Форматируем ответ для фронтенда
const formatMessageForFrontend = (msg) => {
if (!msg) return null;
console.log(`🔍 [formatMessageForFrontend] Форматируем сообщение:`, {
id: msg.id,
sender_type: msg.sender_type,
role: msg.role,
content: msg.content,
// Добавляем все поля для диагностики
allFields: Object.keys(msg),
rawMsg: msg
});
const formatted = {
id: msg.id,
conversation_id: msg.conversation_id,
user_id: msg.user_id,
content: msg.content,
sender_type: msg.sender_type,
role: msg.role,
channel: msg.channel,
content: msg.content, // content уже расшифрован encryptedDb
sender_type: msg.sender_type, // sender_type уже расшифрован encryptedDb
role: msg.role, // role уже расшифрован encryptedDb
channel: msg.channel, // channel уже расшифрован encryptedDb
created_at: msg.created_at,
attachments: null // Инициализируем как null
};
// Добавляем информацию о файле, если она есть
if (msg.attachment_filename) {
formatted.attachments = [{
originalname: msg.attachment_filename,
mimetype: msg.attachment_mimetype,
originalname: msg.attachment_filename, // attachment_filename уже расшифрован encryptedDb
mimetype: msg.attachment_mimetype, // attachment_mimetype уже расшифрован encryptedDb
size: msg.attachment_size,
// НЕ передаем attachment_data обратно в ответе на POST
}];
@@ -563,18 +645,228 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
[conversationId]
);
res.json({
// Получаем расшифрованные данные для форматирования
const decryptedUserMessage = userMessage ? await encryptedDb.getData('messages', { id: userMessage.id }, 1) : null;
const decryptedAiMessage = aiMessage ? await encryptedDb.getData('messages', { id: aiMessage.id }, 1) : null;
const response = {
success: true,
conversationId: conversationId,
userMessage: formatMessageForFrontend(userMessage),
aiMessage: formatMessageForFrontend(aiMessage),
userMessage: formatMessageForFrontend(decryptedUserMessage ? decryptedUserMessage[0] : null),
aiMessage: formatMessageForFrontend(decryptedAiMessage ? decryptedAiMessage[0] : null),
};
console.log(`📤 [Chat] Отправляем ответ на фронтенд:`, {
userMessage: response.userMessage,
aiMessage: response.aiMessage
});
// Отправляем WebSocket уведомления
if (response.userMessage) {
broadcastChatMessage(response.userMessage, userId);
}
if (response.aiMessage) {
broadcastChatMessage(response.aiMessage, userId);
}
broadcastConversationUpdate(conversationId, userId);
res.json(response);
} catch (error) {
logger.error('Error processing authenticated message:', error);
res.status(500).json({ success: false, error: 'Ошибка обработки сообщения' });
}
});
// Новый маршрут для обработки сообщений через очередь
router.post('/message-queued', requireAuth, upload.array('attachments'), async (req, res) => {
logger.info('Received /message-queued request');
try {
const userId = req.session.userId;
const { message, language, conversationId: convIdFromRequest, type = 'chat' } = req.body;
const files = req.files;
const file = files && files.length > 0 ? files[0] : null;
// Валидация
if (!message && !file) {
return res.status(400).json({
success: false,
error: 'Требуется текст сообщения или файл.'
});
}
if (message && file) {
return res.status(400).json({
success: false,
error: 'Нельзя отправить текст и файл одновременно.'
});
}
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
let conversationId = convIdFromRequest;
let conversation = null;
// Найти или создать диалог
if (conversationId) {
let convResult;
if (req.session.isAdmin) {
convResult = await db.getQuery()(
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE id = $1',
[conversationId, encryptionKey]
);
} else {
convResult = await db.getQuery()(
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $3) as title FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId, encryptionKey]
);
}
if (convResult.rows.length === 0) {
return res.status(404).json({
success: false,
error: 'Диалог не найден или доступ запрещен'
});
}
conversation = convResult.rows[0];
} else {
// Ищем последний диалог пользователя
const lastConvResult = await db.getQuery()(
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId, encryptionKey]
);
if (lastConvResult.rows.length > 0) {
conversation = lastConvResult.rows[0];
conversationId = conversation.id;
} else {
// Создаем новый диалог
const title = message && message.trim()
? (message.trim().length > 50 ? `${message.trim().substring(0, 50)}...` : message.trim())
: (file ? `Файл: ${file.originalname}` : 'Новый диалог');
const newConvResult = await db.getQuery()(
'INSERT INTO conversations (user_id, title_encrypted) VALUES ($1, encrypt_text($2, $3)) RETURNING *',
[userId, title, encryptionKey]
);
conversation = newConvResult.rows[0];
conversationId = conversation.id;
}
}
// Сохраняем сообщение пользователя
const messageContent = message && message.trim() ? message.trim() : null;
const attachmentFilename = file ? file.originalname : null;
const attachmentMimetype = file ? file.mimetype : null;
const attachmentSize = file ? file.size : null;
const attachmentData = file ? file.buffer : null;
const recipientId = conversation.user_id;
let senderType = 'user';
let role = 'user';
if (req.session.isAdmin) {
senderType = 'admin';
role = 'admin';
}
const userMessage = await encryptedDb.saveData('messages', {
conversation_id: conversationId,
user_id: recipientId,
content: messageContent,
sender_type: senderType,
role: role,
channel: 'web',
attachment_filename: attachmentFilename,
attachment_mimetype: attachmentMimetype,
attachment_size: attachmentSize,
attachment_data: attachmentData
});
// Проверяем, нужно ли генерировать AI ответ
if (await isUserBlocked(userId)) {
logger.info(`[Chat] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`);
return res.json({ success: true, message: userMessage });
}
let shouldGenerateAiReply = true;
if (senderType === 'admin' && userId !== recipientId) {
shouldGenerateAiReply = false;
}
if (messageContent && shouldGenerateAiReply) {
try {
// Получаем историю сообщений
const historyResult = await db.getQuery()(
'SELECT decrypt_text(sender_type_encrypted, $3) as sender_type, decrypt_text(content_encrypted, $3) as content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
[conversationId, userMessage.id, encryptionKey]
);
const history = historyResult.rows.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
content: msg.content
}));
// Получаем настройки AI
const aiSettings = await aiAssistantSettingsService.getSettings();
let rules = null;
if (aiSettings && aiSettings.rules_id) {
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
}
// Добавляем задачу в очередь
const taskData = {
message: messageContent,
language: language || 'auto',
history: history,
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
rules: rules,
type: type,
userId: userId,
userRole: req.session.isAdmin ? 'admin' : 'user',
conversationId: conversationId,
userMessageId: userMessage.id
};
const queueResult = await aiQueueService.addTask(taskData);
res.json({
success: true,
message: userMessage,
queueInfo: {
taskId: queueResult.taskId,
status: 'queued',
estimatedWaitTime: aiQueueService.getStats().currentQueueSize * 30
}
});
} catch (error) {
logger.error('Error adding task to queue:', error);
res.status(500).json({
success: false,
error: 'Ошибка при добавлении задачи в очередь.'
});
}
} else {
res.json({ success: true, message: userMessage });
}
} catch (error) {
logger.error('Error processing queued message:', error);
res.status(500).json({
success: false,
error: 'Внутренняя ошибка сервера.'
});
}
});
// Добавьте этот маршрут для проверки доступных моделей
router.get('/models', async (req, res) => {
try {
@@ -601,6 +893,20 @@ router.get('/history', requireAuth, async (req, res) => {
// Опциональный ID диалога
const conversationId = req.query.conversation_id;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
// Если нужен только подсчет
if (countOnly) {
@@ -615,51 +921,24 @@ router.get('/history', requireAuth, async (req, res) => {
return res.json({ success: true, count: totalCount });
}
// Формируем основной запрос
let query = `
SELECT
id,
conversation_id,
user_id,
content,
sender_type,
role,
channel,
created_at,
attachment_filename,
attachment_mimetype,
attachment_size,
attachment_data -- Выбираем и данные файла
FROM messages
WHERE user_id = $1
`;
const params = [userId];
// Добавляем фильтр по диалогу, если нужно
// Загружаем сообщения через encryptedDb
const whereConditions = { user_id: userId };
if (conversationId) {
query += ' AND conversation_id = $2';
params.push(conversationId);
whereConditions.conversation_id = conversationId;
}
// Добавляем сортировку и пагинацию
query += ' ORDER BY created_at ASC LIMIT $' + (params.length + 1) + ' OFFSET $' + (params.length + 2);
params.push(limit);
params.push(offset);
logger.debug('Executing history query:', { query, params });
const result = await db.getQuery()(query, params);
const messages = await encryptedDb.getData('messages', whereConditions, limit, 'created_at ASC', offset);
// Обрабатываем результаты для фронтенда
const messages = result.rows.map(msg => {
const formattedMessages = messages.map(msg => {
const formatted = {
id: msg.id,
conversation_id: msg.conversation_id,
user_id: msg.user_id,
content: msg.content,
sender_type: msg.sender_type,
role: msg.role,
channel: msg.channel,
content: msg.content, // content уже расшифрован encryptedDb
sender_type: msg.sender_type, // sender_type уже расшифрован encryptedDb
role: msg.role, // role уже расшифрован encryptedDb
channel: msg.channel, // channel уже расшифрован encryptedDb
created_at: msg.created_at,
attachments: null // Инициализируем
};
@@ -667,17 +946,13 @@ router.get('/history', requireAuth, async (req, res) => {
// Если есть данные файла, добавляем их в attachments
if (msg.attachment_data) {
formatted.attachments = [{
originalname: msg.attachment_filename,
mimetype: msg.attachment_mimetype,
originalname: msg.attachment_filename, // attachment_filename уже расшифрован encryptedDb
mimetype: msg.attachment_mimetype, // attachment_mimetype уже расшифрован encryptedDb
size: msg.attachment_size,
// Кодируем Buffer в Base64 для передачи на фронтенд
data_base64: msg.attachment_data.toString('base64')
}];
}
// Не забываем удалить поле attachment_data из итогового объекта,
// так как оно уже обработано и не нужно в сыром виде на фронте
// (хотя map и так создает новый объект, это для ясности)
delete formatted.attachment_data;
return formatted;
});
@@ -692,11 +967,11 @@ router.get('/history', requireAuth, async (req, res) => {
const totalCountResult = await db.getQuery()(totalCountQuery, totalCountParams);
const totalMessages = parseInt(totalCountResult.rows[0].count, 10);
logger.info(`Returning message history for user ${userId}`, { count: messages.length, offset, limit, total: totalMessages });
logger.info(`Returning message history for user ${userId}`, { count: formattedMessages.length, offset, limit, total: totalMessages });
res.json({
success: true,
messages: messages,
messages: formattedMessages,
offset: offset,
limit: limit,
total: totalMessages
@@ -732,6 +1007,20 @@ router.post('/process-guest', requireAuth, async (req, res) => {
router.post('/ai-draft', requireAuth, async (req, res) => {
const userId = req.session.userId;
const { conversationId, messages, language } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
if (!conversationId || !Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({ success: false, error: 'conversationId и messages обязательны' });
}
@@ -746,8 +1035,8 @@ router.post('/ai-draft', requireAuth, async (req, res) => {
const promptText = messages.map(m => m.content).join('\n\n');
// Получаем последние 10 сообщений из диалога для истории
const historyResult = await db.getQuery()(
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 ORDER BY created_at DESC LIMIT 10',
[conversationId]
'SELECT decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content FROM messages WHERE conversation_id = $1 ORDER BY created_at DESC LIMIT 10',
[conversationId, encryptionKey]
);
const history = historyResult.rows.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',

102
backend/routes/countries.js Normal file
View File

@@ -0,0 +1,102 @@
/**
* 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/HB3-ACCELERATOR
*/
const express = require('express');
const path = require('path');
const fs = require('fs');
const router = express.Router();
/**
* @route GET /api/countries
* @desc Получить список всех стран
* @access Public
*/
router.get('/', async (req, res, next) => {
try {
// Путь к файлу с данными стран
const countriesFilePath = path.join(__dirname, '../db/data/countries.json');
// Проверяем существование файла
if (!fs.existsSync(countriesFilePath)) {
return res.status(404).json({
success: false,
message: 'Файл с данными стран не найден'
});
}
// Читаем файл
const countriesData = fs.readFileSync(countriesFilePath, 'utf8');
const countries = JSON.parse(countriesData);
// Возвращаем список стран
res.json({
success: true,
data: countries.countries || [],
count: countries.countries ? countries.countries.length : 0
});
} catch (error) {
console.error('Ошибка при получении списка стран:', error);
next(error);
}
});
/**
* @route GET /api/countries/:code
* @desc Получить информацию о стране по коду
* @access Public
*/
router.get('/:code', async (req, res, next) => {
try {
const { code } = req.params;
// Путь к файлу с данными стран
const countriesFilePath = path.join(__dirname, '../db/data/countries.json');
// Проверяем существование файла
if (!fs.existsSync(countriesFilePath)) {
return res.status(404).json({
success: false,
message: 'Файл с данными стран не найден'
});
}
// Читаем файл
const countriesData = fs.readFileSync(countriesFilePath, 'utf8');
const countries = JSON.parse(countriesData);
// Ищем страну по коду (поддерживаем поиск по code, code3 или numeric)
const country = countries.countries.find(c =>
c.code === code.toUpperCase() ||
c.code3 === code.toUpperCase() ||
c.numeric === code
);
if (!country) {
return res.status(404).json({
success: false,
message: `Страна с кодом ${code} не найдена`
});
}
res.json({
success: true,
data: country
});
} catch (error) {
console.error('Ошибка при получении информации о стране:', error);
next(error);
}
});
module.exports = router;

173
backend/routes/dleV2.js Normal file
View File

@@ -0,0 +1,173 @@
/**
* 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/HB3-ACCELERATOR
*/
const express = require('express');
const router = express.Router();
const dleV2Service = require('../services/dleV2Service');
const logger = require('../utils/logger');
const auth = require('../middleware/auth');
const path = require('path');
const fs = require('fs');
/**
* @route POST /api/dle-v2
* @desc Создать новое DLE v2 (Digital Legal Entity)
* @access Private (только для авторизованных пользователей с ролью admin)
*/
router.post('/', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try {
const dleParams = req.body;
logger.info('Получен запрос на создание DLE v2:', dleParams);
// Если параметр partners не был передан явно, используем адрес авторизованного пользователя
if (!dleParams.partners || dleParams.partners.length === 0) {
// Проверяем, есть ли в сессии адрес кошелька пользователя
if (!req.user || !req.user.walletAddress) {
return res.status(400).json({
success: false,
message: 'Не указан адрес кошелька пользователя или партнеров для распределения токенов'
});
}
// Используем адрес авторизованного пользователя
dleParams.partners = [req.user.address || req.user.walletAddress];
// Если суммы не указаны, используем значение по умолчанию (100% токенов)
if (!dleParams.amounts || dleParams.amounts.length === 0) {
dleParams.amounts = ['1000000'];
}
}
// Создаем DLE v2
const result = await dleV2Service.createDLE(dleParams);
logger.info('DLE v2 успешно создано:', result);
res.json({
success: true,
message: 'DLE v2 успешно создано',
data: result.data
});
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при создании DLE v2'
});
}
});
/**
* @route GET /api/dle-v2
* @desc Получить список всех DLE v2
* @access Private (только для авторизованных пользователей)
*/
router.get('/', auth.requireAuth, async (req, res, next) => {
try {
const dles = dleV2Service.getAllDLEs();
res.json({
success: true,
data: dles
});
} catch (error) {
logger.error('Ошибка при получении списка DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при получении списка DLE v2'
});
}
});
/**
* @route GET /api/dle-v2/defaults
* @desc Получить настройки по умолчанию для DLE v2
* @access Private (только для авторизованных пользователей)
*/
router.get('/defaults', auth.requireAuth, async (req, res, next) => {
// Возвращаем настройки по умолчанию, которые будут использоваться
// при заполнении формы на фронтенде
res.json({
success: true,
data: {
votingDelay: 1, // 1 блок задержки перед началом голосования
votingPeriod: 45818, // ~1 неделя в блоках (при 13 секундах на блок)
proposalThreshold: '100000', // 100,000 токенов
quorumPercentage: 4, // 4% от общего количества токенов
minTimelockDelay: 2 // 2 дня
}
});
});
/**
* @route DELETE /api/dle-v2/:dleAddress
* @desc Удалить DLE v2 по адресу
* @access Private (только для авторизованных пользователей с ролью admin)
*/
router.delete('/:dleAddress', auth.requireAuth, auth.requireAdmin, async (req, res, next) => {
try {
const { dleAddress } = req.params;
logger.info(`Получен запрос на удаление DLE v2 с адресом: ${dleAddress}`);
// Проверяем существование DLE v2 в директории contracts-data/dles
const dlesDir = path.join(__dirname, '../contracts-data/dles');
const files = fs.readdirSync(dlesDir);
let fileToDelete = null;
// Находим файл, содержащий указанный адрес DLE
for (const file of files) {
if (file.includes('dle-v2-') && file.endsWith('.json')) {
const filePath = path.join(dlesDir, file);
if (fs.statSync(filePath).isFile()) {
try {
const dleData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
if (dleData.dleAddress && dleData.dleAddress.toLowerCase() === dleAddress.toLowerCase()) {
fileToDelete = filePath;
break;
}
} catch (err) {
logger.error(`Ошибка при чтении файла ${file}:`, err);
}
}
}
}
if (!fileToDelete) {
return res.status(404).json({
success: false,
message: `DLE v2 с адресом ${dleAddress} не найдено`
});
}
// Удаляем файл
fs.unlinkSync(fileToDelete);
logger.info(`DLE v2 с адресом ${dleAddress} успешно удалено`);
res.json({
success: true,
message: `DLE v2 с адресом ${dleAddress} успешно удалено`
});
} catch (error) {
logger.error('Ошибка при удалении DLE v2:', error);
res.status(500).json({
success: false,
message: error.message || 'Произошла ошибка при удалении DLE v2'
});
}
});
module.exports = router;

View File

@@ -39,11 +39,25 @@ router.post('/link', requireAuth, async (req, res, next) => {
if (type === 'wallet') {
const normalizedWallet = value.toLowerCase();
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Проверяем, существует ли уже такой кошелек
const existingCheck = await db.getQuery()(
`SELECT user_id FROM user_identities
WHERE provider = 'wallet' AND provider_id = $1`,
[normalizedWallet]
WHERE provider_encrypted = encrypt_text('wallet', $2) AND provider_id_encrypted = encrypt_text($1, $2)`,
[normalizedWallet, encryptionKey]
);
if (existingCheck.rows.length > 0) {
@@ -138,8 +152,25 @@ router.delete('/:provider/:providerId', requireAuth, async (req, res, next) => {
// Получение email-настроек
router.get('/email-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1');
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
const { rows } = await db.getQuery()(
'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $1) as smtp_host, decrypt_text(smtp_user_encrypted, $1) as smtp_user, decrypt_text(smtp_password_encrypted, $1) as smtp_password, decrypt_text(imap_host_encrypted, $1) as imap_host, decrypt_text(from_email_encrypted, $1) as from_email FROM email_settings ORDER BY id LIMIT 1',
[encryptionKey]
);
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.smtp_password; // не возвращаем пароль
@@ -152,6 +183,20 @@ router.get('/email-settings', requireAuth, async (req, res, next) => {
// Обновление email-настроек
router.put('/email-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
const { smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email } = req.body;
if (!smtp_host || !smtp_port || !smtp_user || !from_email) {
@@ -161,14 +206,14 @@ router.put('/email-settings', requireAuth, async (req, res, next) => {
if (rows.length) {
// Обновляем существующую запись
await db.getQuery()(
`UPDATE email_settings SET smtp_host=$1, smtp_port=$2, smtp_user=$3, smtp_password=COALESCE($4, smtp_password), imap_host=$5, imap_port=$6, from_email=$7, updated_at=NOW() WHERE id=$8`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, rows[0].id]
`UPDATE email_settings SET smtp_host_encrypted=encrypt_text($1, $9), smtp_port=$2, smtp_user_encrypted=encrypt_text($3, $9), smtp_password_encrypted=COALESCE(encrypt_text($4, $9), smtp_password_encrypted), imap_host_encrypted=encrypt_text($5, $9), imap_port=$6, from_email_encrypted=encrypt_text($7, $9), updated_at=NOW() WHERE id=$8`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, rows[0].id, encryptionKey]
);
} else {
// Вставляем новую
await db.getQuery()(
`INSERT INTO email_settings (smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email]
`INSERT INTO email_settings (smtp_host_encrypted, smtp_port, smtp_user_encrypted, smtp_password_encrypted, imap_host_encrypted, imap_port, from_email_encrypted) VALUES (encrypt_text($1, $8), $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), $6, encrypt_text($7, $8))`,
[smtp_host, smtp_port, smtp_user, smtp_password, imap_host, imap_port, from_email, encryptionKey]
);
}
res.json({ success: true });
@@ -180,8 +225,25 @@ router.put('/email-settings', requireAuth, async (req, res, next) => {
// Получение telegram-настроек
router.get('/telegram-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const { rows } = await db.getQuery()('SELECT * FROM telegram_settings ORDER BY id LIMIT 1');
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
const { rows } = await db.getQuery()(
'SELECT id, created_at, updated_at, decrypt_text(bot_token_encrypted, $1) as bot_token, decrypt_text(bot_username_encrypted, $1) as bot_username FROM telegram_settings ORDER BY id LIMIT 1',
[encryptionKey]
);
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.bot_token; // не возвращаем токен
@@ -194,6 +256,20 @@ router.get('/telegram-settings', requireAuth, async (req, res, next) => {
// Обновление telegram-настроек
router.put('/telegram-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
const { bot_token, bot_username } = req.body;
if (!bot_token || !bot_username) {
@@ -203,14 +279,14 @@ router.put('/telegram-settings', requireAuth, async (req, res, next) => {
if (rows.length) {
// Обновляем существующую запись
await db.getQuery()(
`UPDATE telegram_settings SET bot_token=$1, bot_username=$2, updated_at=NOW() WHERE id=$3`,
[bot_token, bot_username, rows[0].id]
`UPDATE telegram_settings SET bot_token_encrypted=encrypt_text($1, $4), bot_username_encrypted=encrypt_text($2, $4), updated_at=NOW() WHERE id=$3`,
[bot_token, bot_username, rows[0].id, encryptionKey]
);
} else {
// Вставляем новую
await db.getQuery()(
`INSERT INTO telegram_settings (bot_token, bot_username) VALUES ($1,$2)` ,
[bot_token, bot_username]
`INSERT INTO telegram_settings (bot_token_encrypted, bot_username_encrypted) VALUES (encrypt_text($1, $3), encrypt_text($2, $3))` ,
[bot_token, bot_username, encryptionKey]
);
}
res.json({ success: true });
@@ -222,8 +298,25 @@ router.put('/telegram-settings', requireAuth, async (req, res, next) => {
// Получение db-настроек
router.get('/db-settings', requireAuth, async (req, res, next) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const { rows } = await db.getQuery()('SELECT * FROM db_settings ORDER BY id LIMIT 1');
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
const { rows } = await db.getQuery()(
'SELECT id, db_port, created_at, updated_at, decrypt_text(db_host_encrypted, $1) as db_host, decrypt_text(db_name_encrypted, $1) as db_name, decrypt_text(db_user_encrypted, $1) as db_user, decrypt_text(db_password_encrypted, $1) as db_password FROM db_settings ORDER BY id LIMIT 1',
[encryptionKey]
);
if (!rows.length) return res.status(404).json({ success: false, error: 'Not found' });
const settings = rows[0];
delete settings.db_password; // не возвращаем пароль

78
backend/routes/kpp.js Normal file
View File

@@ -0,0 +1,78 @@
/**
* 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/HB3-ACCELERATOR
*/
const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const logger = require('../utils/logger');
/**
* @swagger
* tags:
* name: KPP
* description: API для КПП кодов (Код причины постановки на учет)
*/
/**
* @swagger
* /api/kpp/codes:
* get:
* summary: Получить список КПП кодов
* tags: [KPP]
* responses:
* 200:
* description: Список КПП кодов
* content:
* application/json:
* schema:
* type: object
* properties:
* codes:
* type: array
* items:
* type: object
* properties:
* code:
* type: string
* example: "773001001"
* title:
* type: string
* example: "По месту нахождения организации"
* 500:
* description: Ошибка сервера
*/
router.get('/codes', (req, res) => {
try {
// Путь к файлу с КПП кодами
const kppFilePath = path.join(__dirname, '../db/data/kpp_codes.json');
// Читаем файл синхронно (для простоты, можно переделать на асинхронный)
const kppData = fs.readFileSync(kppFilePath, 'utf8');
const kppJson = JSON.parse(kppData);
// Возвращаем данные в том же формате, что ожидает frontend
res.json({
codes: kppJson.kpp_codes || []
});
logger.info(`[KPP] Returned ${kppJson.kpp_codes?.length || 0} KPP codes`);
} catch (error) {
logger.error('Error fetching KPP codes:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Не удалось загрузить КПП коды'
});
}
});
module.exports = router;

View File

@@ -22,29 +22,45 @@ const { isUserBlocked } = require('../utils/userUtils');
router.get('/', async (req, res) => {
const userId = req.query.userId;
const conversationId = req.query.conversationId;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
let result;
if (conversationId) {
result = await db.getQuery()(
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
FROM messages
WHERE conversation_id = $1
ORDER BY created_at ASC`,
[conversationId]
[conversationId, encryptionKey]
);
} else if (userId) {
result = await db.getQuery()(
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
FROM messages
WHERE user_id = $1
ORDER BY created_at ASC`,
[userId]
[userId, encryptionKey]
);
} else {
result = await db.getQuery()(
`SELECT id, user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data
FROM messages
ORDER BY created_at ASC`
ORDER BY created_at ASC`,
[encryptionKey]
);
}
res.json(result.rows);
@@ -55,7 +71,22 @@ router.get('/', async (req, res) => {
// POST /api/messages
router.post('/', async (req, res) => {
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata } = req.body;
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
// Проверка блокировки пользователя
if (await isUserBlocked(user_id)) {
@@ -64,8 +95,8 @@ router.post('/', async (req, res) => {
// Проверка наличия идентификатора для выбранного канала
if (channel === 'email') {
const emailIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'email']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'email', encryptionKey]
);
if (emailIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не указан email. Сообщение не отправлено.' });
@@ -73,8 +104,8 @@ router.post('/', async (req, res) => {
}
if (channel === 'telegram') {
const tgIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'telegram']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'telegram', encryptionKey]
);
if (tgIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не привязан Telegram. Сообщение не отправлено.' });
@@ -82,8 +113,8 @@ router.post('/', async (req, res) => {
}
if (channel === 'wallet' || channel === 'web3' || channel === 'web') {
const walletIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'wallet']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'wallet', encryptionKey]
);
if (walletIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' });
@@ -91,16 +122,16 @@ router.post('/', async (req, res) => {
}
// 1. Проверяем, есть ли беседа для user_id
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id, encryptionKey]
);
let conversation;
if (conversationResult.rows.length === 0) {
// 2. Если нет — создаём новую беседу
const title = `Чат с пользователем ${user_id}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[user_id, title]
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
[user_id, title, encryptionKey]
);
conversation = newConv.rows[0];
} else {
@@ -108,9 +139,9 @@ router.post('/', async (req, res) => {
}
// 3. Сохраняем сообщение с conversation_id
const result = await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
VALUES ($1,$2,$3,$4,$5,$6,$7,NOW(),$8,$9,$10,$11,$12) RETURNING *`,
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata]
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`,
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
);
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
if (channel === 'telegram' && direction === 'out') {
@@ -118,8 +149,8 @@ router.post('/', async (req, res) => {
console.log(`[messages.js] Попытка отправки сообщения в Telegram для user_id=${user_id}`);
// Получаем Telegram ID пользователя
const tgIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'telegram']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'telegram', encryptionKey]
);
console.log(`[messages.js] Результат поиска Telegram ID:`, tgIdentity.rows);
if (tgIdentity.rows.length > 0) {
@@ -144,8 +175,8 @@ router.post('/', async (req, res) => {
try {
// Получаем email пользователя
const emailIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'email']
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
[user_id, 'email', encryptionKey]
);
if (emailIdentity.rows.length > 0) {
const email = emailIdentity.rows[0].provider_id;
@@ -237,24 +268,39 @@ router.post('/broadcast', async (req, res) => {
if (!user_id || !content) {
return res.status(400).json({ error: 'user_id и content обязательны' });
}
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
// Получаем все идентификаторы пользователя
const identitiesRes = await db.getQuery()(
'SELECT provider, provider_id FROM user_identities WHERE user_id = $1',
[user_id]
'SELECT decrypt_text(provider_encrypted, $2) as provider, decrypt_text(provider_id_encrypted, $2) as provider_id FROM user_identities WHERE user_id = $1',
[user_id, encryptionKey]
);
const identities = identitiesRes.rows;
// --- Найти или создать беседу (conversation) ---
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id]
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id, encryptionKey]
);
let conversation;
if (conversationResult.rows.length === 0) {
const title = `Чат с пользователем ${user_id}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[user_id, title]
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
[user_id, title, encryptionKey]
);
conversation = newConv.rows[0];
} else {
@@ -269,9 +315,9 @@ router.post('/broadcast', async (req, res) => {
await emailBot.sendEmail(email, 'Новое сообщение', content);
// Сохраняем в messages с conversation_id
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', JSON.stringify({ broadcast: true })]
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', encryptionKey]
);
results.push({ channel: 'email', status: 'sent' });
sent = true;
@@ -286,9 +332,9 @@ router.post('/broadcast', async (req, res) => {
const bot = await telegramBot.getBot();
await bot.telegram.sendMessage(telegram, content);
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', JSON.stringify({ broadcast: true })]
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', encryptionKey]
);
results.push({ channel: 'telegram', status: 'sent' });
sent = true;
@@ -301,9 +347,9 @@ router.post('/broadcast', async (req, res) => {
if (wallet) {
// Здесь можно реализовать отправку через web3, если нужно
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[user_id, conversation.id, 'admin', content, 'wallet', 'user', 'out', JSON.stringify({ broadcast: true })]
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
[user_id, conversation.id, 'admin', content, 'wallet', 'user', 'out', encryptionKey]
);
results.push({ channel: 'wallet', status: 'saved' });
sent = true;

173
backend/routes/ollama.js Normal file
View File

@@ -0,0 +1,173 @@
/**
* 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/HB3-ACCELERATOR
*/
const express = require('express');
const router = express.Router();
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
const logger = require('../utils/logger');
const { requireAuth } = require('../middleware/auth');
// Проверка статуса подключения к Ollama
router.get('/status', requireAuth, async (req, res) => {
try {
// Проверяем, что контейнер Ollama запущен
const { stdout } = await execAsync('docker ps --filter "name=dapp-ollama" --format "{{.Names}}"');
const isContainerRunning = stdout.trim() === 'dapp-ollama';
if (!isContainerRunning) {
return res.json({ connected: false, error: 'Ollama container not running' });
}
// Проверяем API Ollama
try {
const { stdout: apiResponse } = await execAsync('docker exec dapp-ollama ollama list');
return res.json({ connected: true, message: 'Ollama is running' });
} catch (apiError) {
return res.json({ connected: false, error: 'Ollama API not responding' });
}
} catch (error) {
logger.error('Error checking Ollama status:', error);
res.status(500).json({ connected: false, error: 'Failed to check Ollama status' });
}
});
// Получение списка установленных моделей
router.get('/models', requireAuth, async (req, res) => {
try {
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
const models = lines.map(line => {
const parts = line.trim().split(/\s+/);
if (parts.length >= 4) {
return {
name: parts[0],
id: parts[1],
size: parseInt(parts[2]) || 0,
modified: parts.slice(3).join(' ')
};
}
return null;
}).filter(model => model !== null);
res.json({ models });
} catch (error) {
logger.error('Error getting Ollama models:', error);
res.status(500).json({ error: 'Failed to get models' });
}
});
// Установка модели
router.post('/install', requireAuth, async (req, res) => {
const { model } = req.body;
if (!model) {
return res.status(400).json({ error: 'Model name is required' });
}
try {
logger.info(`Starting installation of model: ${model}`);
// Запускаем установку в фоне
const installProcess = exec(`docker exec dapp-ollama ollama pull ${model}`, (error, stdout, stderr) => {
if (error) {
logger.error(`Error installing model ${model}:`, error);
} else {
logger.info(`Successfully installed model: ${model}`);
}
});
// Возвращаем ответ сразу, не ждем завершения
res.json({
success: true,
message: `Installation of ${model} started`,
processId: installProcess.pid
});
} catch (error) {
logger.error('Error starting model installation:', error);
res.status(500).json({ error: 'Failed to start installation' });
}
});
// Удаление модели
router.delete('/models/:modelName', requireAuth, async (req, res) => {
const { modelName } = req.params;
if (!modelName) {
return res.status(400).json({ error: 'Model name is required' });
}
try {
logger.info(`Removing model: ${modelName}`);
const { stdout, stderr } = await execAsync(`docker exec dapp-ollama ollama rm ${modelName}`);
if (stderr && !stderr.includes('deleted')) {
throw new Error(stderr);
}
logger.info(`Successfully removed model: ${modelName}`);
res.json({ success: true, message: `Model ${modelName} removed successfully` });
} catch (error) {
logger.error(`Error removing model ${modelName}:`, error);
res.status(500).json({ error: `Failed to remove model: ${error.message}` });
}
});
// Получение информации о модели
router.get('/models/:modelName', requireAuth, async (req, res) => {
const { modelName } = req.params;
try {
const { stdout } = await execAsync(`docker exec dapp-ollama ollama show ${modelName}`);
res.json({ model: modelName, info: stdout });
} catch (error) {
logger.error(`Error getting model info for ${modelName}:`, error);
res.status(404).json({ error: 'Model not found' });
}
});
// Поиск моделей в реестре (если поддерживается)
router.get('/search', requireAuth, async (req, res) => {
const { query } = req.query;
if (!query) {
return res.status(400).json({ error: 'Search query is required' });
}
try {
// Пока просто возвращаем популярные модели
const popularModels = [
'qwen2.5:7b',
'llama2:7b',
'mistral:7b',
'codellama:7b',
'llama2:13b',
'qwen2.5:14b',
'gemma:7b',
'phi3:3.8b'
];
const filteredModels = popularModels.filter(model =>
model.toLowerCase().includes(query.toLowerCase())
);
res.json({ models: filteredModels });
} catch (error) {
logger.error('Error searching models:', error);
res.status(500).json({ error: 'Failed to search models' });
}
});
module.exports = router;

View File

@@ -0,0 +1,184 @@
/**
* 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/HB3-ACCELERATOR
*/
const express = require('express');
const path = require('path');
const fs = require('fs');
const router = express.Router();
/**
* @route GET /api/russian-classifiers/oktmo
* @desc Получить список кодов ОКТМО (муниципальные образования)
* @access Public
*/
router.get('/oktmo', async (req, res, next) => {
try {
const filePath = path.join(__dirname, '../db/data/oktmo.json');
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'Файл с кодами ОКТМО не найден'
});
}
const data = fs.readFileSync(filePath, 'utf8');
const oktmoData = JSON.parse(data);
res.json({
success: true,
data: oktmoData.oktmo_codes || [],
count: oktmoData.oktmo_codes ? oktmoData.oktmo_codes.length : 0
});
} catch (error) {
console.error('Ошибка при получении кодов ОКТМО:', error);
next(error);
}
});
/**
* @route GET /api/russian-classifiers/okved
* @desc Получить список кодов ОКВЭД (виды экономической деятельности)
* @access Public
*/
router.get('/okved', async (req, res, next) => {
try {
const filePath = path.join(__dirname, '../db/data/okved.json');
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'Файл с кодами ОКВЭД не найден'
});
}
const data = fs.readFileSync(filePath, 'utf8');
const okvedData = JSON.parse(data);
// Для ОКВЭД можем добавить фильтрацию по запросу
const { search, level } = req.query;
let codes = okvedData.okved_codes || [];
// Фильтрация по поисковому запросу
if (search) {
const searchTerm = search.toLowerCase();
codes = codes.filter(code =>
code.code.toLowerCase().includes(searchTerm) ||
code.title.toLowerCase().includes(searchTerm)
);
}
// Фильтрация по уровню (количество точек в коде)
if (level) {
const targetLevel = parseInt(level);
codes = codes.filter(code => {
const codeLevel = (code.code.match(/\./g) || []).length + 1;
return codeLevel === targetLevel;
});
}
// Ограничиваем количество результатов для производительности
const limit = parseInt(req.query.limit) || 2000; // Увеличили лимит для полного списка
codes = codes.slice(0, limit);
res.json({
success: true,
data: codes,
count: codes.length,
total: okvedData.okved_codes ? okvedData.okved_codes.length : 0
});
} catch (error) {
console.error('Ошибка при получении кодов ОКВЭД:', error);
next(error);
}
});
/**
* @route GET /api/russian-classifiers/okved/:code
* @desc Получить информацию о коде ОКВЭД
* @access Public
*/
router.get('/okved/:code', async (req, res, next) => {
try {
const { code } = req.params;
const filePath = path.join(__dirname, '../db/data/okved.json');
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: 'Файл с кодами ОКВЭД не найден'
});
}
const data = fs.readFileSync(filePath, 'utf8');
const okvedData = JSON.parse(data);
const okvedCode = okvedData.okved_codes.find(c => c.code === code);
if (!okvedCode) {
return res.status(404).json({
success: false,
message: `Код ОКВЭД ${code} не найден`
});
}
res.json({
success: true,
data: okvedCode
});
} catch (error) {
console.error('Ошибка при получении информации о коде ОКВЭД:', error);
next(error);
}
});
/**
* @route GET /api/russian-classifiers/all
* @desc Получить все российские классификаторы одним запросом
* @access Public
*/
router.get('/all', async (req, res, next) => {
try {
const oktmoPath = path.join(__dirname, '../db/data/oktmo.json');
const okvedPath = path.join(__dirname, '../db/data/okved.json');
const result = {};
// ОКТМО
if (fs.existsSync(oktmoPath)) {
const oktmoData = JSON.parse(fs.readFileSync(oktmoPath, 'utf8'));
result.oktmo = oktmoData.oktmo_codes || [];
}
// ОКВЭД (полный список)
if (fs.existsSync(okvedPath)) {
const okvedData = JSON.parse(fs.readFileSync(okvedPath, 'utf8'));
// Отдаем ВСЕ коды ОКВЭД - пользователь хочет полный список
result.okved = okvedData.okved_codes || [];
}
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Ошибка при получении всех российских классификаторов:', error);
next(error);
}
});
module.exports = router;

View File

@@ -45,7 +45,25 @@ router.get('/rpc', async (req, res, next) => {
}
}
const rpcConfigs = await rpcProviderService.getAllRpcProviders();
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const rpcProvidersResult = await db.getQuery()(
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
[encryptionKey]
);
const rpcConfigs = rpcProvidersResult.rows;
if (isAdmin) {
// Для админов возвращаем полные данные
@@ -108,7 +126,25 @@ router.delete('/rpc/:networkId', requireAdmin, async (req, res, next) => {
// Получение токенов для аутентификации
router.get('/auth-tokens', async (req, res, next) => {
try {
const authTokens = await authTokenService.getAllAuthTokens();
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const tokensResult = await db.getQuery()(
'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
[encryptionKey]
);
const authTokens = tokensResult.rows;
// Возвращаем полные данные для всех пользователей (включая гостевых)
res.json({ success: true, data: authTokens });

View File

@@ -26,7 +26,21 @@ router.use((req, res, next) => {
// Получить список всех таблиц (доступно всем)
router.get('/', async (req, res, next) => {
try {
const result = await db.getQuery()('SELECT * FROM user_tables ORDER BY id');
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const result = await db.getQuery()('SELECT id, created_at, updated_at, is_rag_source_id, decrypt_text(name_encrypted, $1) as name, decrypt_text(description_encrypted, $1) as description FROM user_tables ORDER BY id', [encryptionKey]);
res.json(result.rows);
} catch (err) {
next(err);
@@ -37,9 +51,24 @@ router.get('/', async (req, res, next) => {
router.post('/', async (req, res, next) => {
try {
const { name, description, isRagSourceId } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const result = await db.getQuery()(
'INSERT INTO user_tables (name, description, is_rag_source_id) VALUES ($1, $2, $3) RETURNING *',
[name, description || null, isRagSourceId || 2]
'INSERT INTO user_tables (name_encrypted, description_encrypted, is_rag_source_id) VALUES (encrypt_text($1, $4), encrypt_text($2, $4), $3) RETURNING *',
[name, description || null, isRagSourceId || 2, encryptionKey]
);
res.json(result.rows[0]);
} catch (err) {
@@ -47,15 +76,58 @@ router.post('/', async (req, res, next) => {
}
});
// Получить данные из таблицы is_rag_source с расшифровкой
router.get('/rag-sources', async (req, res, next) => {
try {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const result = await db.getQuery()(
'SELECT id, decrypt_text(name_encrypted, $1) as name FROM is_rag_source ORDER BY id',
[encryptionKey]
);
res.json(result.rows);
} catch (err) {
console.error('[RAG Sources] Error:', err);
next(err);
}
});
// Получить структуру и данные таблицы (доступно всем)
router.get('/:id', async (req, res, next) => {
try {
const tableId = req.params.id;
const tableMetaResult = await db.getQuery()('SELECT name, description FROM user_tables WHERE id = $1', [tableId]);
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const tableMetaResult = await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name, decrypt_text(description_encrypted, $2) as description FROM user_tables WHERE id = $1', [tableId, encryptionKey]);
const tableMeta = tableMetaResult.rows[0] || { name: '', description: '' };
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId])).rows;
const columns = (await db.getQuery()('SELECT id, table_id, "order", created_at, updated_at, decrypt_text(name_encrypted, $2) as name, decrypt_text(type_encrypted, $2) as type, decrypt_text(placeholder_encrypted, $2) as placeholder_encrypted, placeholder FROM user_columns WHERE table_id = $1 ORDER BY "order" ASC, id ASC', [tableId, encryptionKey])).rows;
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1 ORDER BY id', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT id, row_id, column_id, created_at, updated_at, decrypt_text(value_encrypted, $2) as value FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId, encryptionKey])).rows;
res.json({ name: tableMeta.name, description: tableMeta.description, columns, rows, cellValues });
} catch (err) {
next(err);
@@ -98,13 +170,28 @@ router.post('/:id/columns', async (req, res, next) => {
if (purpose) {
finalOptions.purpose = purpose;
}
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем уже существующие плейсхолдеры в таблице
const existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE table_id = $1', [tableId])).rows;
const existingPlaceholders = existing.map(c => c.placeholder).filter(Boolean);
const placeholder = generatePlaceholder(name, existingPlaceholders);
const result = await db.getQuery()(
'INSERT INTO user_columns (table_id, name, type, options, "order", placeholder) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *',
[tableId, name, type, finalOptions ? JSON.stringify(finalOptions) : null, order || 0, placeholder]
'INSERT INTO user_columns (table_id, name_encrypted, type_encrypted, placeholder_encrypted, "order", placeholder) VALUES ($1, encrypt_text($2, $7), encrypt_text($3, $7), encrypt_text($6, $7), $4, $5) RETURNING *',
[tableId, name, type, order || 0, placeholder, placeholder, encryptionKey]
);
res.json(result.rows[0]);
} catch (err) {
@@ -121,8 +208,22 @@ router.post('/:id/rows', async (req, res, next) => {
[tableId]
);
console.log('[DEBUG][addRow] result.rows[0]:', result.rows[0]);
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем все строки и значения для upsert
const rows = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId])).rows;
const rows = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId, encryptionKey])).rows;
const upsertRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
console.log('[DEBUG][upsertRows]', upsertRows);
if (upsertRows.length > 0) {
@@ -140,10 +241,24 @@ router.get('/:id/rows', async (req, res, next) => {
try {
const tableId = req.params.id;
const { product, tags, ...relationFilters } = req.query; // tags = "B2B,VIP", relation_{colId}=rowId
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем все столбцы, строки и значения ячеек
const columns = (await db.getQuery()('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
const columns = (await db.getQuery()('SELECT id, table_id, "order", created_at, updated_at, decrypt_text(name_encrypted, $2) as name, decrypt_text(type_encrypted, $2) as type, decrypt_text(placeholder_encrypted, $2) as placeholder_encrypted, placeholder FROM user_columns WHERE table_id = $1', [tableId, encryptionKey])).rows;
const rows = (await db.getQuery()('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
const cellValues = (await db.getQuery()('SELECT id, row_id, column_id, created_at, updated_at, decrypt_text(value_encrypted, $2) as value FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId, encryptionKey])).rows;
// Находим id нужных колонок
const productCol = columns.find(c => c.options && c.options.purpose === 'product');
@@ -210,9 +325,23 @@ router.patch('/cell/:cellId', async (req, res, next) => {
try {
const cellId = req.params.cellId;
const { value } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const result = await db.getQuery()(
'UPDATE user_cell_values SET value = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
[value, cellId]
'UPDATE user_cell_values SET value_encrypted = encrypt_text($1, $3), updated_at = NOW() WHERE id = $2 RETURNING *',
[value, cellId, encryptionKey]
);
// Получаем row_id и table_id
const row = (await db.getQuery()('SELECT row_id FROM user_cell_values WHERE id = $1', [cellId])).rows[0];
@@ -222,7 +351,7 @@ router.patch('/cell/:cellId', async (req, res, next) => {
if (table) {
const tableId = table.table_id;
// Получаем всю строку для upsert
const rowData = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [rowId])).rows[0];
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [rowId, encryptionKey])).rows[0];
if (rowData) {
const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text);
console.log('[DEBUG][upsertRows]', upsertRows);
@@ -242,18 +371,32 @@ router.patch('/cell/:cellId', async (req, res, next) => {
router.post('/cell', async (req, res, next) => {
try {
const { row_id, column_id, value } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const result = await db.getQuery()(
`INSERT INTO user_cell_values (row_id, column_id, value) VALUES ($1, $2, $3)
ON CONFLICT (row_id, column_id) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()
`INSERT INTO user_cell_values (row_id, column_id, value_encrypted) VALUES ($1, $2, encrypt_text($3, $4))
ON CONFLICT (row_id, column_id) DO UPDATE SET value_encrypted = encrypt_text($3, $4), updated_at = NOW()
RETURNING *`,
[row_id, column_id, value]
[row_id, column_id, value, encryptionKey]
);
// Получаем table_id
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0];
if (table) {
const tableId = table.table_id;
// Получаем всю строку для upsert
const rowData = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id])).rows[0];
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id, encryptionKey])).rows[0];
if (rowData) {
const upsertRows = [{ row_id: rowData.row_id, text: rowData.text, metadata: { answer: rowData.answer } }].filter(r => r.row_id && r.text);
console.log('[DEBUG][upsertRows]', upsertRows);
@@ -278,7 +421,21 @@ router.delete('/row/:rowId', async (req, res, next) => {
if (table) {
const tableId = table.table_id;
// Получаем все строки для rebuild
const rows = (await db.getQuery()('SELECT r.id as row_id, c.value as text, c2.value as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId])).rows;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const rows = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId, encryptionKey])).rows;
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
console.log('[DEBUG][rebuildRows]', rebuildRows);
if (rebuildRows.length > 0) {
@@ -308,7 +465,21 @@ router.patch('/column/:columnId', async (req, res, next) => {
const columnId = req.params.columnId;
const { name, type, options, order, placeholder } = req.body;
// Получаем table_id для проверки уникальности плейсхолдера
const colInfo = (await db.getQuery()('SELECT table_id, name FROM user_columns WHERE id = $1', [columnId])).rows[0];
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const colInfo = (await db.getQuery()('SELECT table_id, decrypt_text(name_encrypted, $2) as name FROM user_columns WHERE id = $1', [columnId, encryptionKey])).rows[0];
if (!colInfo) return res.status(404).json({ error: 'Column not found' });
let newPlaceholder = placeholder;
if (name !== undefined && !placeholder) {
@@ -547,7 +718,21 @@ router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
router.get('/:id/placeholders', async (req, res, next) => {
try {
const tableId = req.params.id;
const columns = (await db.getQuery()('SELECT id, name, placeholder FROM user_columns WHERE table_id = $1', [tableId])).rows;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const columns = (await db.getQuery()('SELECT id, decrypt_text(name_encrypted, $2) as name, placeholder FROM user_columns WHERE table_id = $1', [tableId, encryptionKey])).rows;
res.json(columns.map(col => ({
id: col.id,
name: col.name,
@@ -561,15 +746,33 @@ router.get('/:id/placeholders', async (req, res, next) => {
// Получить все плейсхолдеры по всем пользовательским таблицам
router.get('/placeholders/all', async (req, res, next) => {
try {
const result = await db.getQuery()(`
SELECT c.id as column_id, c.name as column_name, c.placeholder, t.id as table_id, t.name as table_name
FROM user_columns c
JOIN user_tables t ON c.table_id = t.id
WHERE c.placeholder IS NOT NULL AND c.placeholder != ''
ORDER BY t.id, c.id
`);
res.json(result.rows);
const encryptedDb = require('../services/encryptedDatabaseService');
// Получаем все колонки с плейсхолдерами
const columns = await encryptedDb.getData('user_columns', {});
// Фильтруем только те, у которых есть плейсхолдеры
const columnsWithPlaceholders = columns.filter(col => col.placeholder && col.placeholder !== '');
// Получаем информацию о таблицах
const tables = await encryptedDb.getData('user_tables', {});
const tableMap = {};
tables.forEach(table => {
tableMap[table.id] = table.name;
});
// Формируем результат
const result = columnsWithPlaceholders.map(col => ({
column_id: col.id,
column_name: col.name,
placeholder: col.placeholder,
table_id: col.table_id,
table_name: tableMap[col.table_id] || `Таблица ${col.table_id}`
}));
res.json(result);
} catch (err) {
console.error('[Placeholders] Error:', err);
next(err);
}
});

View File

@@ -78,6 +78,20 @@ router.get('/', requireAuth, async (req, res, next) => {
} = req.query;
const adminId = req.user && req.user.id;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// --- Формируем условия ---
const where = [];
const params = [];
@@ -97,9 +111,10 @@ router.get('/', requireAuth, async (req, res, next) => {
if (contactType !== 'all') {
where.push(`EXISTS (
SELECT 1 FROM user_identities ui
WHERE ui.user_id = u.id AND ui.provider = $${idx++}
WHERE ui.user_id = u.id AND ui.provider_encrypted = encrypt_text($${idx++}, $${idx++})
)`);
params.push(contactType);
params.push(encryptionKey);
}
// Фильтр по поиску
@@ -107,10 +122,11 @@ router.get('/', requireAuth, async (req, res, next) => {
where.push(`(
LOWER(u.first_name) LIKE $${idx} OR
LOWER(u.last_name) LIKE $${idx} OR
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(ui.provider_id) LIKE $${idx})
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(decrypt_text(ui.provider_id_encrypted, $${idx + 1})) LIKE $${idx})
)`);
params.push(`%${search.toLowerCase()}%`);
idx++;
params.push(encryptionKey);
idx += 2;
}
// Фильтр по блокировке
@@ -123,11 +139,12 @@ router.get('/', requireAuth, async (req, res, next) => {
// --- Основной SQL ---
let sql = `
SELECT u.id, u.first_name, u.last_name, u.created_at, u.preferred_language, u.is_blocked,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'email' LIMIT 1) AS email,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'telegram' LIMIT 1) AS telegram,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'wallet' LIMIT 1) AS wallet
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('email', $${idx++}) LIMIT 1) AS email,
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('telegram', $${idx++}) LIMIT 1) AS telegram,
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('wallet', $${idx++}) LIMIT 1) AS wallet
FROM users u
`;
params.push(encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey, encryptionKey);
// Фильтрация по тегам
if (tagIds) {
@@ -330,16 +347,31 @@ router.delete('/:id', async (req, res) => {
// Получить пользователя по id
router.get('/:id', async (req, res, next) => {
const userId = req.params.id;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
const query = db.getQuery();
// Получаем пользователя
const userResult = await query('SELECT id, first_name, last_name, created_at, preferred_language, is_blocked FROM users WHERE id = $1', [userId]);
const userResult = await query('SELECT id, decrypt_text(first_name_encrypted, $2) as first_name, decrypt_text(last_name_encrypted, $2) as last_name, created_at, preferred_language, is_blocked FROM users WHERE id = $1', [userId, encryptionKey]);
if (userResult.rows.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
const user = userResult.rows[0];
// Получаем идентификаторы
const identitiesResult = await query('SELECT provider, provider_id FROM user_identities WHERE user_id = $1', [userId]);
const identitiesResult = await query('SELECT decrypt_text(provider_encrypted, $2) as provider, decrypt_text(provider_id_encrypted, $2) as provider_id FROM user_identities WHERE user_id = $1', [userId, encryptionKey]);
const identityMap = {};
for (const id of identitiesResult.rows) {
identityMap[id.provider] = id.provider_id;
@@ -362,11 +394,26 @@ router.get('/:id', async (req, res, next) => {
// POST /api/users
router.post('/', async (req, res) => {
const { first_name, last_name, preferred_language } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
const result = await db.getQuery()(
`INSERT INTO users (first_name, last_name, preferred_language, created_at)
VALUES ($1, $2, $3, NOW()) RETURNING *`,
[first_name, last_name, JSON.stringify(preferred_language || [])]
`INSERT INTO users (first_name_encrypted, last_name_encrypted, preferred_language, created_at)
VALUES (encrypt_text($1, $4), encrypt_text($2, $4), $3, NOW()) RETURNING *`,
[first_name, last_name, JSON.stringify(preferred_language || []), encryptionKey]
);
broadcastContactsUpdate();
res.json({ success: true, user: result.rows[0] });
@@ -377,6 +424,20 @@ router.post('/', async (req, res) => {
// Массовый импорт контактов
router.post('/import', requireAuth, async (req, res) => {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
const contacts = req.body;
if (!Array.isArray(contacts)) {
@@ -397,15 +458,15 @@ router.post('/import', requireAuth, async (req, res) => {
let userId = null;
let foundUser = null;
if (c.email) {
const r = await dbq('SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2', ['email', c.email.toLowerCase()]);
const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['email', c.email.toLowerCase(), encryptionKey]);
if (r.rows.length) foundUser = r.rows[0].user_id;
}
if (!foundUser && c.telegram) {
const r = await dbq('SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2', ['telegram', c.telegram]);
const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['telegram', c.telegram, encryptionKey]);
if (r.rows.length) foundUser = r.rows[0].user_id;
}
if (!foundUser && c.wallet) {
const r = await dbq('SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2', ['wallet', c.wallet]);
const r = await dbq('SELECT user_id FROM user_identities WHERE provider_encrypted = encrypt_text($1, $3) AND provider_id_encrypted = encrypt_text($2, $3)', ['wallet', c.wallet, encryptionKey]);
if (r.rows.length) foundUser = r.rows[0].user_id;
}
if (foundUser) {
@@ -413,11 +474,11 @@ router.post('/import', requireAuth, async (req, res) => {
updated++;
// Обновляем имя, если нужно
if (first_name || last_name) {
await dbq('UPDATE users SET first_name = COALESCE($1, first_name), last_name = COALESCE($2, last_name) WHERE id = $3', [first_name, last_name, userId]);
await dbq('UPDATE users SET first_name_encrypted = COALESCE(encrypt_text($1, $4), first_name_encrypted), last_name_encrypted = COALESCE(encrypt_text($2, $4), last_name_encrypted) WHERE id = $3', [first_name, last_name, userId, encryptionKey]);
}
} else {
// Создаём нового пользователя
const ins = await dbq('INSERT INTO users (first_name, last_name, created_at) VALUES ($1, $2, NOW()) RETURNING id', [first_name, last_name]);
const ins = await dbq('INSERT INTO users (first_name_encrypted, last_name_encrypted, created_at) VALUES (encrypt_text($1, $3), encrypt_text($2, $3), NOW()) RETURNING id', [first_name, last_name, encryptionKey]);
userId = ins.rows[0].id;
added++;
}

View File

@@ -0,0 +1,190 @@
/**
* 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/HB3-ACCELERATOR
*/
// Скрипт для создания современного DLE v2 (единый контракт)
const { ethers } = require("hardhat");
const fs = require("fs");
const path = require("path");
async function main() {
// Получаем параметры деплоя из файла
const deployParams = getDeployParams();
console.log("Начинаем создание современного DLE v2...");
console.log("Параметры DLE:");
console.log(JSON.stringify(deployParams, null, 2));
// Получаем аккаунт деплоя
const [deployer] = await ethers.getSigners();
console.log(`Адрес деплоера: ${deployer.address}`);
console.log(`Баланс деплоера: ${ethers.formatEther(await deployer.provider.getBalance(deployer.address))} ETH`);
try {
// 1. Создаем единый контракт DLE
console.log("\n1. Деплой единого контракта DLE v2...");
const DLE = await ethers.getContractFactory("DLE");
// Преобразуем параметры голосования
const votingDelay = deployParams.votingDelay || 1;
const votingPeriod = deployParams.votingPeriod || 45818; // ~1 неделя
const proposalThreshold = deployParams.proposalThreshold || ethers.parseEther("100000");
const quorumPercentage = deployParams.quorumPercentage || 4;
const minTimelockDelay = (deployParams.minTimelockDelay || 2) * 24 * 60 * 60; // дни в секунды
const dle = await DLE.deploy(
deployParams.name,
deployParams.symbol,
deployParams.location,
deployParams.isicCodes || [],
votingDelay,
votingPeriod,
proposalThreshold,
quorumPercentage,
minTimelockDelay
);
await dle.waitForDeployment();
const dleAddress = await dle.getAddress();
console.log(`DLE v2 задеплоен по адресу: ${dleAddress}`);
// 2. Получаем адрес таймлока
const timelockAddress = await dle.getTimelockAddress();
console.log(`Таймлок создан по адресу: ${timelockAddress}`);
// 3. Распределяем начальные токены
console.log("\n3. Распределение начальных токенов...");
const distributeTx = await dle.distributeInitialTokens(
deployParams.partners,
deployParams.amounts
);
await distributeTx.wait();
console.log(`Токены распределены между партнерами`);
// 4. Получаем информацию о DLE
const dleInfo = await dle.getDLEInfo();
console.log("\n4. Информация о DLE:");
console.log(`Название: ${dleInfo.name}`);
console.log(`Символ: ${dleInfo.symbol}`);
console.log(`Местонахождение: ${dleInfo.location}`);
console.log(`Коды деятельности: ${dleInfo.isicCodes.join(', ')}`);
console.log(`Дата создания: ${new Date(dleInfo.creationTimestamp * 1000).toISOString()}`);
// 5. Сохраняем информацию о созданном DLE
console.log("\n5. Сохранение информации о DLE v2...");
const dleData = {
name: deployParams.name,
symbol: deployParams.symbol,
location: deployParams.location,
isicCodes: deployParams.isicCodes || [],
dleAddress: dleAddress,
timelockAddress: timelockAddress,
creationBlock: (await distributeTx.provider.getBlockNumber()),
creationTimestamp: (await distributeTx.provider.getBlock()).timestamp,
deployedManually: true,
version: "v2",
governanceSettings: {
votingDelay: votingDelay,
votingPeriod: votingPeriod,
proposalThreshold: proposalThreshold.toString(),
quorumPercentage: quorumPercentage,
minTimelockDelay: deployParams.minTimelockDelay || 2
}
};
const saveResult = saveDLEData(dleData);
console.log("\nDLE v2 успешно создан!");
console.log(`Адрес DLE: ${dleAddress}`);
console.log(`Адрес таймлока: ${timelockAddress}`);
console.log(`Версия: v2 (единый контракт)`);
return {
success: true,
dleAddress: dleAddress,
timelockAddress: timelockAddress,
data: dleData
};
} catch (error) {
console.error("Ошибка при создании DLE v2:", error);
throw error;
}
}
// Получаем параметры деплоя из файла
function getDeployParams() {
const paramsFile = path.join(__dirname, 'current-params.json');
if (!fs.existsSync(paramsFile)) {
console.error(`Файл параметров не найден: ${paramsFile}`);
process.exit(1);
}
try {
const params = JSON.parse(fs.readFileSync(paramsFile, 'utf8'));
console.log("Параметры загружены из файла");
return params;
} catch (error) {
console.error("Ошибка при чтении файла параметров:", error);
process.exit(1);
}
}
// Сохраняем информацию о созданном DLE
function saveDLEData(dleData) {
const dlesDir = path.join(__dirname, "../../contracts-data/dles");
// Проверяем существование директории и создаем при необходимости
try {
if (!fs.existsSync(dlesDir)) {
console.log(`Директория ${dlesDir} не существует, создаю...`);
fs.mkdirSync(dlesDir, { recursive: true });
console.log(`Директория ${dlesDir} успешно создана`);
}
// Проверяем права на запись, создавая временный файл
const testFile = path.join(dlesDir, '.write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
console.log(`Директория ${dlesDir} доступна для записи`);
} catch (error) {
console.error(`Ошибка при проверке директории ${dlesDir}:`, error);
throw error;
}
// Создаем уникальное имя файла
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `dle-v2-${timestamp}.json`;
const filePath = path.join(dlesDir, fileName);
try {
fs.writeFileSync(filePath, JSON.stringify(dleData, null, 2));
console.log(`Информация о DLE сохранена в файл: ${fileName}`);
return { success: true, filePath };
} catch (error) {
console.error(`Ошибка при сохранении файла ${filePath}:`, error);
throw error;
}
}
// Запускаем скрипт
main()
.then(() => {
console.log("Скрипт завершен успешно");
process.exit(0);
})
.catch((error) => {
console.error("Скрипт завершен с ошибкой:", error);
process.exit(1);
});

View File

@@ -0,0 +1,110 @@
/**
* 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/HB3-ACCELERATOR
*/
const db = require('../db');
async function fixRagColumns() {
console.log('🔧 Исправление purpose у колонок в RAG таблице...\n');
try {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем колонки таблицы RAG (ID 28)
const columns = await db.getQuery()(
'SELECT id, decrypt_text(name_encrypted, $1) as name FROM user_columns WHERE table_id = 28 ORDER BY id',
[encryptionKey]
);
console.log('Найденные колонки в таблице RAG:');
columns.rows.forEach(col => {
console.log(` ID: ${col.id}, Name: ${col.name}`);
});
// Маппинг названий колонок на purpose
const purposeMapping = {
'Вопрос': 'question',
'Ответ': 'answer',
'Контекст теги': 'context',
'Продукт теги': 'product',
'Клиент теги': 'userTags'
};
// Обновляем каждую колонку
for (const col of columns.rows) {
const purpose = purposeMapping[col.name];
if (purpose) {
console.log(`\nОбновляем колонку "${col.name}" (ID: ${col.id}) -> purpose: ${purpose}`);
// Получаем текущие options
const currentOptions = await db.getQuery()(
'SELECT options FROM user_columns WHERE id = $1',
[col.id]
);
let options = currentOptions.rows[0]?.options || {};
options.purpose = purpose;
// Обновляем колонку
await db.getQuery()(
'UPDATE user_columns SET options = $1 WHERE id = $2',
[JSON.stringify(options), col.id]
);
console.log(` ✅ Обновлено`);
} else {
console.log(`\n⚠️ Колонка "${col.name}" (ID: ${col.id}) - purpose не определен`);
}
}
console.log('\n✅ Исправление завершено!');
// Проверяем результат
console.log('\nПроверка результата:');
const updatedColumns = await db.getQuery()(
'SELECT id, decrypt_text(name_encrypted, $1) as name, options FROM user_columns WHERE table_id = 28 ORDER BY id',
[encryptionKey]
);
updatedColumns.rows.forEach(col => {
const options = col.options || {};
console.log(` ID: ${col.id}, Name: ${col.name}, Purpose: ${options.purpose || 'undefined'}`);
});
} catch (error) {
console.error('❌ Ошибка:', error);
}
}
// Запуск скрипта
if (require.main === module) {
fixRagColumns().then(() => {
console.log('\n🏁 Скрипт завершен');
process.exit(0);
}).catch(error => {
console.error('💥 Критическая ошибка:', error);
process.exit(1);
});
}
module.exports = { fixRagColumns };

View File

@@ -0,0 +1,120 @@
/**
* 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/HB3-ACCELERATOR
*/
// Устанавливаем переменные окружения для Docker
process.env.OLLAMA_BASE_URL = 'http://ollama:11434';
process.env.OLLAMA_MODEL = 'qwen2.5:7b';
const aiQueueService = require('../services/ai-queue');
async function testQueueInDocker() {
console.log('🐳 Тестирование AI очереди в Docker...\n');
try {
// Проверяем инициализацию
console.log('1. Проверка инициализации очереди...');
const stats = aiQueueService.getStats();
console.log('✅ Очередь инициализирована:', stats.isInitialized);
console.log('📊 Статистика:', {
totalProcessed: stats.totalProcessed,
totalFailed: stats.totalFailed,
currentQueueSize: stats.currentQueueSize,
runningTasks: stats.runningTasks
});
// Тестируем добавление задач
console.log('\n2. Тестирование добавления задач...');
const testTasks = [
{
message: 'Привет, как дела?',
language: 'ru',
type: 'chat',
userId: 1,
userRole: 'user',
requestId: 'docker_test_1'
},
{
message: 'Расскажи о погоде',
language: 'ru',
type: 'analysis',
userId: 1,
userRole: 'user',
requestId: 'docker_test_2'
},
{
message: 'Срочный вопрос!',
language: 'ru',
type: 'urgent',
userId: 1,
userRole: 'admin',
requestId: 'docker_test_3'
}
];
for (let i = 0; i < testTasks.length; i++) {
const task = testTasks[i];
console.log(` Добавляем задачу ${i + 1}: "${task.message}"`);
try {
const result = await aiQueueService.addTask(task);
console.log(` ✅ Задача добавлена, ID: ${result.taskId}`);
} catch (error) {
console.log(` ❌ Ошибка добавления задачи: ${error.message}`);
}
}
// Ждем обработки
console.log('\n3. Ожидание обработки задач...');
await new Promise(resolve => setTimeout(resolve, 15000));
// Проверяем статистику
console.log('\n4. Проверка статистики после обработки...');
const finalStats = aiQueueService.getStats();
console.log('📊 Финальная статистика:', {
totalProcessed: finalStats.totalProcessed,
totalFailed: finalStats.totalFailed,
currentQueueSize: finalStats.currentQueueSize,
runningTasks: finalStats.runningTasks,
averageProcessingTime: Math.round(finalStats.averageProcessingTime)
});
// Тестируем управление очередью
console.log('\n5. Тестирование управления очередью...');
console.log(' Пауза очереди...');
aiQueueService.pause();
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(' Возобновление очереди...');
aiQueueService.resume();
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('\n✅ Тестирование завершено!');
} catch (error) {
console.error('❌ Ошибка тестирования:', error);
}
}
// Запуск теста
if (require.main === module) {
testQueueInDocker().then(() => {
console.log('\n🏁 Тест завершен');
process.exit(0);
}).catch(error => {
console.error('💥 Критическая ошибка:', error);
process.exit(1);
});
}
module.exports = { testQueueInDocker };

View File

@@ -0,0 +1,116 @@
/**
* 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/HB3-ACCELERATOR
*/
const aiQueueService = require('../services/ai-queue');
async function testQueue() {
console.log('🧪 Тестирование AI очереди...\n');
try {
// Проверяем инициализацию
console.log('1. Проверка инициализации очереди...');
const stats = aiQueueService.getStats();
console.log('✅ Очередь инициализирована:', stats.isInitialized);
console.log('📊 Статистика:', {
totalProcessed: stats.totalProcessed,
totalFailed: stats.totalFailed,
currentQueueSize: stats.currentQueueSize,
runningTasks: stats.runningTasks
});
// Тестируем добавление задач
console.log('\n2. Тестирование добавления задач...');
const testTasks = [
{
message: 'Привет, как дела?',
language: 'ru',
type: 'chat',
userId: 1,
userRole: 'user',
requestId: 'test_1'
},
{
message: 'Расскажи о погоде',
language: 'ru',
type: 'analysis',
userId: 1,
userRole: 'user',
requestId: 'test_2'
},
{
message: 'Срочный вопрос!',
language: 'ru',
type: 'urgent',
userId: 1,
userRole: 'admin',
requestId: 'test_3'
}
];
for (let i = 0; i < testTasks.length; i++) {
const task = testTasks[i];
console.log(` Добавляем задачу ${i + 1}: "${task.message}"`);
try {
const result = await aiQueueService.addTask(task);
console.log(` ✅ Задача добавлена, ID: ${result.taskId}`);
} catch (error) {
console.log(` ❌ Ошибка добавления задачи: ${error.message}`);
}
}
// Ждем обработки
console.log('\n3. Ожидание обработки задач...');
await new Promise(resolve => setTimeout(resolve, 10000));
// Проверяем статистику
console.log('\n4. Проверка статистики после обработки...');
const finalStats = aiQueueService.getStats();
console.log('📊 Финальная статистика:', {
totalProcessed: finalStats.totalProcessed,
totalFailed: finalStats.totalFailed,
currentQueueSize: finalStats.currentQueueSize,
runningTasks: finalStats.runningTasks,
averageProcessingTime: Math.round(finalStats.averageProcessingTime)
});
// Тестируем управление очередью
console.log('\n5. Тестирование управления очередью...');
console.log(' Пауза очереди...');
aiQueueService.pause();
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(' Возобновление очереди...');
aiQueueService.resume();
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('\n✅ Тестирование завершено!');
} catch (error) {
console.error('❌ Ошибка тестирования:', error);
}
}
// Запуск теста
if (require.main === module) {
testQueue().then(() => {
console.log('\n🏁 Тест завершен');
process.exit(0);
}).catch(error => {
console.error('💥 Критическая ошибка:', error);
process.exit(1);
});
}
module.exports = { testQueue };

View File

@@ -0,0 +1,82 @@
/**
* 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/HB3-ACCELERATOR
*/
const encryptedDb = require('../services/encryptedDatabaseService');
const db = require('../db');
async function testEncryptedTables() {
console.log('🔐 Тестирование зашифрованных таблиц...\n');
try {
// Тестируем таблицу is_rag_source
console.log('1. Тестирование таблицы is_rag_source:');
const ragSources = await encryptedDb.getData('is_rag_source', {});
console.log(' ✅ Данные получены:', ragSources);
// Тестируем через прямой SQL запрос
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const directResult = await db.getQuery()(
'SELECT id, decrypt_text(name_encrypted, $1) as name FROM is_rag_source ORDER BY id',
[encryptionKey]
);
console.log(' ✅ Прямой SQL запрос:', directResult.rows);
// Тестируем другие важные таблицы
console.log('\n2. Тестирование других зашифрованных таблиц:');
// user_tables
const userTables = await encryptedDb.getData('user_tables', {}, 5);
console.log(' ✅ user_tables (первые 5):', userTables.length, 'записей');
// user_columns
const userColumns = await encryptedDb.getData('user_columns', {}, 5);
console.log(' ✅ user_columns (первые 5):', userColumns.length, 'записей');
// messages
const messages = await encryptedDb.getData('messages', {}, 3);
console.log(' ✅ messages (первые 3):', messages.length, 'записей');
// conversations
const conversations = await encryptedDb.getData('conversations', {}, 3);
console.log(' ✅ conversations (первые 3):', conversations.length, 'записей');
console.log('\n✅ Все тесты прошли успешно!');
} catch (error) {
console.error('❌ Ошибка тестирования:', error);
}
}
// Запуск теста
if (require.main === module) {
testEncryptedTables().then(() => {
console.log('\n🏁 Тест завершен');
process.exit(0);
}).catch(error => {
console.error('💥 Критическая ошибка:', error);
process.exit(1);
});
}
module.exports = { testEncryptedTables };

View File

@@ -47,8 +47,12 @@ async function initServices() {
console.error('[initServices] Ошибка при запуске emailBot:', err);
}
console.log('[initServices] Запуск Telegram-бота...');
await getBot();
console.log('[initServices] Telegram-бот успешно запущен');
try {
await getBot();
console.log('[initServices] Telegram-бот успешно запущен');
} catch (err) {
console.error('[initServices] Ошибка при запуске Telegram-бота:', err);
}
} catch (error) {
console.error('Ошибка при инициализации сервисов:', error);
}

View File

@@ -12,6 +12,7 @@
const { ethers } = require('ethers');
const logger = require('../utils/logger');
const db = require('../db');
const authTokenService = require('./authTokenService');
const rpcProviderService = require('./rpcProviderService');
@@ -28,12 +29,41 @@ const ERC20_ABI = [
async function checkAdminRole(address) {
if (!address) return false;
logger.info(`Checking admin role for address: ${address}`);
try {
let foundTokens = false;
let errorCount = 0;
const balances = {};
// Получаем токены и RPC из базы
const tokens = await authTokenService.getAllAuthTokens();
const rpcProviders = await rpcProviderService.getAllRpcProviders();
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем токены и RPC из базы с расшифровкой
const tokensResult = await db.getQuery()(
'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
[encryptionKey]
);
const tokens = tokensResult.rows;
const rpcProvidersResult = await db.getQuery()(
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
[encryptionKey]
);
const rpcProviders = rpcProvidersResult.rows;
logger.info(`Retrieved ${tokens.length} tokens and ${rpcProviders.length} RPC providers`);
logger.info('Tokens:', JSON.stringify(tokens, null, 2));
logger.info('RPC Providers:', JSON.stringify(rpcProviders, null, 2));
const rpcMap = {};
for (const rpc of rpcProviders) {
rpcMap[rpc.network_id] = rpc.rpc_url;
@@ -109,6 +139,10 @@ async function checkAdminRole(address) {
}
logger.info(`Admin role denied - no tokens found for ${address}`);
return false;
} catch (error) {
logger.error(`Error in checkAdminRole for ${address}:`, error);
return false;
}
}
module.exports = { checkAdminRole };

View File

@@ -18,26 +18,84 @@ const { OpenAIEmbeddings } = require('@langchain/openai');
const logger = require('../utils/logger');
const fetch = require('node-fetch');
// Простой кэш для ответов
const responseCache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 минут
class AIAssistant {
constructor() {
this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5';
this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5:7b';
this.isModelLoaded = false;
this.lastHealthCheck = 0;
this.healthCheckInterval = 30000; // 30 секунд
}
// Проверка здоровья модели
async checkModelHealth() {
const now = Date.now();
if (now - this.lastHealthCheck < this.healthCheckInterval) {
return this.isModelLoaded;
}
try {
const response = await fetch(`${this.baseUrl}/api/tags`, {
timeout: 5000
});
if (response.ok) {
const data = await response.json();
this.isModelLoaded = data.models?.some(m => m.name === this.defaultModel) || false;
} else {
this.isModelLoaded = false;
}
} catch (error) {
console.error('Model health check failed:', error);
this.isModelLoaded = false;
}
this.lastHealthCheck = now;
return this.isModelLoaded;
}
// Очистка старых записей кэша
cleanupCache() {
const now = Date.now();
for (const [key, value] of responseCache.entries()) {
if (now - value.timestamp > CACHE_TTL) {
responseCache.delete(key);
}
}
}
// Создание экземпляра ChatOllama с нужными параметрами
createChat(language = 'ru') {
const systemPrompt =
language === 'ru'
? 'Вы - полезный ассистент. Отвечайте на русском языке.'
: 'You are a helpful assistant. Respond in English.';
createChat(language = 'ru', customSystemPrompt = '') {
// Используем кастомный системный промпт, если он передан, иначе используем дефолтный
let systemPrompt = customSystemPrompt;
if (!systemPrompt) {
systemPrompt = language === 'ru'
? 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.'
: 'You are a helpful assistant. Respond in English briefly and to the point.';
}
return new ChatOllama({
baseUrl: this.baseUrl,
model: this.defaultModel,
system: systemPrompt,
temperature: 0.7,
maxTokens: 1000,
timeout: 30000, // 30 секунд таймаут
temperature: 0.3, // Уменьшаем для более предсказуемых ответов
maxTokens: 100, // Еще больше уменьшаем для быстрого ответа
timeout: 60000, // Увеличиваем таймаут до 60 секунд
options: {
num_ctx: 512, // Еще больше уменьшаем контекст для экономии памяти
num_thread: 12, // Увеличиваем количество потоков еще больше
num_gpu: 1,
num_gqa: 8,
rope_freq_base: 1000000,
rope_freq_scale: 0.5,
repeat_penalty: 1.1, // Добавляем штраф за повторения
top_k: 20, // Еще больше ограничиваем выбор токенов
top_p: 0.8, // Уменьшаем nucleus sampling
temperature: 0.1, // Еще больше уменьшаем для более предсказуемых ответов
}
});
}
@@ -52,6 +110,24 @@ class AIAssistant {
try {
console.log('getResponse called with:', { message, language, history, systemPrompt, rules });
// Очищаем старый кэш
this.cleanupCache();
// Проверяем здоровье модели
const isHealthy = await this.checkModelHealth();
if (!isHealthy) {
console.warn('Model is not healthy, returning fallback response');
return 'Извините, модель временно недоступна. Пожалуйста, попробуйте позже.';
}
// Создаем ключ кэша
const cacheKey = JSON.stringify({ message, language, systemPrompt, rules });
const cached = responseCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
console.log('Returning cached response');
return cached.response;
}
// Определяем язык, если не указан явно
const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language;
console.log('Detected language:', detectedLanguage);
@@ -77,28 +153,39 @@ class AIAssistant {
// Добавляем текущее сообщение пользователя
messages.push({ role: 'user', content: message });
let response = null;
// Пробуем прямой API запрос (OpenAI-совместимый endpoint)
try {
console.log('Trying direct API request...');
const response = await this.fallbackRequestOpenAI(messages, detectedLanguage);
response = await this.fallbackRequestOpenAI(messages, detectedLanguage, fullSystemPrompt);
console.log('Direct API response received:', response);
return response;
} catch (error) {
console.error('Error in direct API request:', error);
// Если прямой запрос не удался, пробуем через ChatOllama (склеиваем сообщения в текст)
const chat = this.createChat(detectedLanguage, fullSystemPrompt);
try {
const prompt = messages.map(m => `${m.role === 'user' ? 'Пользователь' : m.role === 'assistant' ? 'Ассистент' : 'Система'}: ${m.content}`).join('\n');
console.log('Sending request to ChatOllama...');
const chatResponse = await chat.invoke(prompt);
console.log('ChatOllama response:', chatResponse);
response = chatResponse.content;
} catch (chatError) {
console.error('Error using ChatOllama:', chatError);
throw chatError;
}
}
// Если прямой запрос не удался, пробуем через ChatOllama (склеиваем сообщения в текст)
const chat = this.createChat(detectedLanguage);
try {
const prompt = messages.map(m => `${m.role === 'user' ? 'Пользователь' : m.role === 'assistant' ? 'Ассистент' : 'Система'}: ${m.content}`).join('\n');
console.log('Sending request to ChatOllama...');
const response = await chat.invoke(prompt);
console.log('ChatOllama response:', response);
return response.content;
} catch (error) {
console.error('Error using ChatOllama:', error);
throw error;
// Кэшируем ответ
if (response) {
responseCache.set(cacheKey, {
response,
timestamp: Date.now()
});
}
return response;
} catch (error) {
console.error('Error in getResponse:', error);
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
@@ -106,10 +193,15 @@ class AIAssistant {
}
// Новый метод для OpenAI/Qwen2.5 совместимого endpoint
async fallbackRequestOpenAI(messages, language) {
async fallbackRequestOpenAI(messages, language, systemPrompt = '') {
try {
console.log('Using fallbackRequestOpenAI with:', { messages, language });
console.log('Using fallbackRequestOpenAI with:', { messages, language, systemPrompt });
const model = this.defaultModel;
// Создаем AbortController для таймаута
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60000); // Увеличиваем до 60 секунд
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -118,11 +210,24 @@ class AIAssistant {
messages,
stream: false,
options: {
temperature: 0.7,
num_predict: 1000,
temperature: 0.3,
num_predict: 200, // Уменьшаем максимальную длину ответа
num_ctx: 1024, // Уменьшаем контекст для экономии памяти
num_thread: 8, // Увеличиваем количество потоков
num_gpu: 1, // Используем GPU если доступен
num_gqa: 8, // Оптимизация для qwen2.5
rope_freq_base: 1000000, // Оптимизация для qwen2.5
rope_freq_scale: 0.5, // Оптимизация для qwen2.5
repeat_penalty: 1.1, // Добавляем штраф за повторения
top_k: 40, // Ограничиваем выбор токенов
top_p: 0.9, // Используем nucleus sampling
},
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -134,6 +239,9 @@ class AIAssistant {
return data.response || '';
} catch (error) {
console.error('Error in fallbackRequestOpenAI:', error);
if (error.name === 'AbortError') {
throw new Error('Request timeout - модель не ответила в течение 60 секунд');
}
throw error;
}
}

View File

@@ -0,0 +1,377 @@
/**
* 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/HB3-ACCELERATOR
*/
const Queue = require('better-queue');
const logger = require('../utils/logger');
class AIQueueService {
constructor() {
this.queue = null;
this.isInitialized = false;
this.userRequestTimes = new Map(); // Добавляем Map для отслеживания запросов пользователей
this.stats = {
totalProcessed: 0,
totalFailed: 0,
averageProcessingTime: 0,
currentQueueSize: 0,
lastProcessedAt: null
};
this.initQueue();
}
initQueue() {
try {
this.queue = new Queue(this.processTask.bind(this), {
// Ограничиваем количество одновременных запросов к Ollama
concurrent: 2,
// Максимальное время выполнения задачи
maxTimeout: 180000, // 3 минуты
// Задержка между задачами для предотвращения перегрузки
afterProcessDelay: 1000, // 1 секунда
// Максимальное количество повторных попыток
maxRetries: 2,
// Задержка между повторными попытками
retryDelay: 5000, // 5 секунд
// Функция определения приоритета
priority: this.getTaskPriority.bind(this),
// Функция фильтрации задач
filter: this.filterTask.bind(this),
// Функция слияния одинаковых задач
merge: this.mergeTasks.bind(this),
// ID задачи для предотвращения дублирования
id: 'requestId'
});
this.setupEventListeners();
this.isInitialized = true;
logger.info('[AIQueue] Queue initialized successfully');
} catch (error) {
logger.error('[AIQueue] Failed to initialize queue:', error);
this.isInitialized = false;
}
}
// Определение приоритета задачи
getTaskPriority(task, cb) {
try {
let priority = 1; // Базовый приоритет
// Высокий приоритет для администраторов
if (task.userRole === 'admin') {
priority += 10;
}
// Приоритет по типу запроса
switch (task.type) {
case 'urgent':
priority += 20;
break;
case 'chat':
priority += 5;
break;
case 'analysis':
priority += 3;
break;
case 'generation':
priority += 1;
break;
}
// Приоритет по размеру запроса (короткие запросы имеют больший приоритет)
if (task.message && task.message.length < 100) {
priority += 2;
}
// Приоритет по времени ожидания
const waitTime = Date.now() - task.timestamp;
if (waitTime > 30000) { // Более 30 секунд ожидания
priority += 5;
}
cb(null, priority);
} catch (error) {
cb(error, 1);
}
}
// Фильтрация задач
filterTask(task, cb) {
try {
// Проверяем обязательные поля
if (!task.message || typeof task.message !== 'string') {
return cb('Invalid message format');
}
if (!task.requestId) {
return cb('Missing request ID');
}
// Проверяем размер сообщения
if (task.message.length > 10000) {
return cb('Message too long (max 10000 characters)');
}
// Проверяем частоту запросов от пользователя
if (this.isUserRateLimited(task.userId)) {
return cb('User rate limit exceeded');
}
cb(null, task);
} catch (error) {
cb(error);
}
}
// Слияние одинаковых задач
mergeTasks(oldTask, newTask, cb) {
try {
// Если это тот же запрос от того же пользователя, обновляем метаданные
if (oldTask.message === newTask.message && oldTask.userId === newTask.userId) {
oldTask.timestamp = newTask.timestamp;
oldTask.retryCount = (oldTask.retryCount || 0) + 1;
cb(null, oldTask);
} else {
cb(null, newTask);
}
} catch (error) {
cb(error);
}
}
// Обработка задачи
async processTask(task, cb) {
const startTime = Date.now();
const taskId = task.requestId;
try {
logger.info(`[AIQueue] Processing task ${taskId} for user ${task.userId}`);
// Импортируем AI сервис
const aiAssistant = require('./ai-assistant');
const encryptedDb = require('./encryptedDatabaseService');
// Выполняем AI запрос
const result = await aiAssistant.getResponse(
task.message,
task.language || 'auto',
task.history || null,
task.systemPrompt || '',
task.rules || null
);
const processingTime = Date.now() - startTime;
// Сохраняем AI ответ в базу данных
if (task.conversationId && result) {
try {
const aiMessage = await encryptedDb.saveData('messages', {
conversation_id: task.conversationId,
user_id: task.userId,
content: result,
sender_type: 'assistant',
role: 'assistant',
channel: 'web'
});
// Получаем расшифрованные данные для WebSocket
const decryptedAiMessage = await encryptedDb.getData('messages', { id: aiMessage.id }, 1);
if (decryptedAiMessage && decryptedAiMessage[0]) {
// Отправляем сообщение через WebSocket
const { broadcastChatMessage } = require('../wsHub');
broadcastChatMessage(decryptedAiMessage[0], task.userId);
}
logger.info(`[AIQueue] AI response saved for conversation ${task.conversationId}`);
} catch (dbError) {
logger.error(`[AIQueue] Error saving AI response:`, dbError);
}
}
// Обновляем статистику
this.updateStats(true, processingTime);
logger.info(`[AIQueue] Task ${taskId} completed in ${processingTime}ms`);
cb(null, {
success: true,
result,
processingTime,
taskId
});
} catch (error) {
const processingTime = Date.now() - startTime;
// Обновляем статистику
this.updateStats(false, processingTime);
logger.error(`[AIQueue] Task ${taskId} failed:`, error);
cb(null, {
success: false,
error: error.message,
processingTime,
taskId
});
}
}
// Добавление задачи в очередь
addTask(taskData) {
if (!this.isInitialized || !this.queue) {
throw new Error('Queue is not initialized');
}
const task = {
...taskData,
timestamp: Date.now(),
requestId: taskData.requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
};
return new Promise((resolve, reject) => {
const ticket = this.queue.push(task, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
// Добавляем обработчики событий для билета
ticket.on('failed', (error) => {
logger.error(`[AIQueue] Task ${task.requestId} failed:`, error);
reject(error);
});
ticket.on('finish', (result) => {
logger.info(`[AIQueue] Task ${task.requestId} finished`);
resolve(result);
});
});
}
// Настройка обработчиков событий очереди
setupEventListeners() {
this.queue.on('task_queued', (taskId) => {
logger.info(`[AIQueue] Task ${taskId} queued`);
this.stats.currentQueueSize = this.queue.length;
});
this.queue.on('task_started', (taskId) => {
logger.info(`[AIQueue] Task ${taskId} started`);
});
this.queue.on('task_finish', (taskId, result) => {
logger.info(`[AIQueue] Task ${taskId} finished successfully`);
this.stats.lastProcessedAt = new Date();
this.stats.currentQueueSize = this.queue.length;
});
this.queue.on('task_failed', (taskId, error) => {
logger.error(`[AIQueue] Task ${taskId} failed:`, error);
this.stats.currentQueueSize = this.queue.length;
});
this.queue.on('empty', () => {
logger.info('[AIQueue] Queue is empty');
this.stats.currentQueueSize = 0;
});
this.queue.on('drain', () => {
logger.info('[AIQueue] Queue drained');
this.stats.currentQueueSize = 0;
});
}
// Обновление статистики
updateStats(success, processingTime) {
this.stats.totalProcessed++;
if (!success) {
this.stats.totalFailed++;
}
// Обновляем среднее время обработки
const totalTime = this.stats.averageProcessingTime * (this.stats.totalProcessed - 1) + processingTime;
this.stats.averageProcessingTime = totalTime / this.stats.totalProcessed;
}
// Проверка ограничения частоты запросов пользователя
isUserRateLimited(userId) {
// Простая реализация - можно улучшить с использованием Redis
const now = Date.now();
const userRequests = this.userRequestTimes.get(userId) || [];
// Удаляем старые запросы (старше 1 минуты)
const recentRequests = userRequests.filter(time => now - time < 60000);
// Ограничиваем до 10 запросов в минуту
if (recentRequests.length >= 10) {
return true;
}
// Добавляем текущий запрос
recentRequests.push(now);
this.userRequestTimes.set(userId, recentRequests);
return false;
}
// Получение статистики очереди
getStats() {
const queueStats = this.queue ? this.queue.getStats() : {};
return {
...this.stats,
queueStats,
isInitialized: this.isInitialized,
currentQueueSize: this.queue ? this.queue.length : 0,
runningTasks: this.queue ? this.queue.running : 0
};
}
// Очистка очереди
clear() {
if (this.queue) {
this.queue.destroy();
this.initQueue();
}
}
// Пауза/возобновление очереди
pause() {
if (this.queue) {
this.queue.pause();
logger.info('[AIQueue] Queue paused');
}
}
resume() {
if (this.queue) {
this.queue.resume();
logger.info('[AIQueue] Queue resumed');
}
}
}
// Создаем и экспортируем единственный экземпляр
const aiQueueService = new AIQueueService();
module.exports = aiQueueService;

View File

@@ -10,38 +10,44 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const TABLE = 'ai_assistant_rules';
async function getAllRules() {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} ORDER BY id`);
return rows;
const rules = await encryptedDb.getData(TABLE, {}, null, 'id');
return rules;
}
async function getRuleById(id) {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]);
return rows[0] || null;
const rules = await encryptedDb.getData(TABLE, { id: id }, 1);
return rules[0] || null;
}
async function createRule({ name, description, rules }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (name, description, rules, created_at, updated_at)
VALUES ($1, $2, $3, NOW(), NOW()) RETURNING *`,
[name, description, rules]
);
return rows[0];
const rule = await encryptedDb.saveData(TABLE, {
name: name,
description: description,
rules: rules,
created_at: new Date(),
updated_at: new Date()
});
return rule;
}
async function updateRule(id, { name, description, rules }) {
const { rows } = await db.getQuery()(
`UPDATE ${TABLE} SET name = $1, description = $2, rules = $3, updated_at = NOW() WHERE id = $4 RETURNING *`,
[name, description, rules, id]
);
return rows[0];
const rule = await encryptedDb.saveData(TABLE, {
name: name,
description: description,
rules: rules,
updated_at: new Date()
}, {
id: id
});
return rule;
}
async function deleteRule(id) {
await db.getQuery()(`DELETE FROM ${TABLE} WHERE id = $1`, [id]);
await encryptedDb.deleteData(TABLE, { id: id });
}
module.exports = { getAllRules, getRuleById, createRule, updateRule, deleteRule };

View File

@@ -10,53 +10,80 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const TABLE = 'ai_assistant_settings';
async function getSettings() {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} ORDER BY id LIMIT 1`);
const settings = rows[0] || null;
if (!settings) return null;
const settings = await encryptedDb.getData(TABLE, {}, 1, 'id');
const setting = settings[0] || null;
if (!setting) return null;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем связанные данные из telegram_settings и email_settings
let telegramBot = null;
let supportEmail = null;
if (settings.telegram_settings_id) {
const tg = await db.getQuery()('SELECT * FROM telegram_settings WHERE id = $1', [settings.telegram_settings_id]);
if (setting.telegram_settings_id) {
const tg = await db.getQuery()(
'SELECT id, created_at, updated_at, decrypt_text(bot_token_encrypted, $2) as bot_token, decrypt_text(bot_username_encrypted, $2) as bot_username FROM telegram_settings WHERE id = $1',
[setting.telegram_settings_id, encryptionKey]
);
telegramBot = tg.rows[0] || null;
}
if (settings.email_settings_id) {
const em = await db.getQuery()('SELECT * FROM email_settings WHERE id = $1', [settings.email_settings_id]);
if (setting.email_settings_id) {
const em = await db.getQuery()(
'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $2) as smtp_host, decrypt_text(smtp_user_encrypted, $2) as smtp_user, decrypt_text(smtp_password_encrypted, $2) as smtp_password, decrypt_text(imap_host_encrypted, $2) as imap_host, decrypt_text(from_email_encrypted, $2) as from_email FROM email_settings WHERE id = $1',
[setting.email_settings_id, encryptionKey]
);
supportEmail = em.rows[0] || null;
}
return {
...settings,
...setting,
telegramBot,
supportEmail,
embedding_model: settings.embedding_model
embedding_model: setting.embedding_model
};
}
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (id, system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_at, updated_by, telegram_settings_id, email_settings_id, system_message)
VALUES (1, $1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10)
ON CONFLICT (id) DO UPDATE SET
system_prompt = EXCLUDED.system_prompt,
selected_rag_tables = EXCLUDED.selected_rag_tables,
languages = EXCLUDED.languages,
model = EXCLUDED.model,
embedding_model = EXCLUDED.embedding_model,
rules = EXCLUDED.rules,
updated_at = NOW(),
updated_by = EXCLUDED.updated_by,
telegram_settings_id = EXCLUDED.telegram_settings_id,
email_settings_id = EXCLUDED.email_settings_id,
system_message = EXCLUDED.system_message
RETURNING *`,
[system_prompt, selected_rag_tables, languages, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message]
);
return rows[0];
const data = {
id: 1,
system_prompt,
selected_rag_tables,
languages,
model,
embedding_model,
rules,
updated_at: new Date(),
updated_by,
telegram_settings_id,
email_settings_id,
system_message
};
// Проверяем, существует ли запись
const existing = await encryptedDb.getData(TABLE, { id: 1 }, 1);
if (existing.length > 0) {
// Обновляем существующую запись
return await encryptedDb.saveData(TABLE, data, { id: 1 });
} else {
// Создаем новую запись
return await encryptedDb.saveData(TABLE, data);
}
}
module.exports = { getSettings, upsertSettings };

View File

@@ -10,41 +10,41 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const OpenAI = require('openai');
const Anthropic = require('@anthropic-ai/sdk');
const TABLE = 'ai_providers_settings';
async function getProviderSettings(provider) {
const { rows } = await db.getQuery()(
`SELECT * FROM ${TABLE} WHERE provider = $1 LIMIT 1`,
[provider]
);
return rows[0] || null;
const settings = await encryptedDb.getData(TABLE, { provider: provider }, 1);
return settings[0] || null;
}
async function upsertProviderSettings({ provider, api_key, base_url, selected_model, embedding_model }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (provider, api_key, base_url, selected_model, embedding_model, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (provider) DO UPDATE SET
api_key = EXCLUDED.api_key,
base_url = EXCLUDED.base_url,
selected_model = EXCLUDED.selected_model,
embedding_model = EXCLUDED.embedding_model,
updated_at = NOW()
RETURNING *`,
[provider, api_key, base_url, selected_model, embedding_model]
);
return rows[0];
const data = {
provider: provider,
api_key: api_key,
base_url: base_url,
selected_model: selected_model,
embedding_model: embedding_model,
updated_at: new Date()
};
// Проверяем, существует ли запись
const existing = await encryptedDb.getData(TABLE, { provider: provider }, 1);
if (existing.length > 0) {
// Обновляем существующую запись
return await encryptedDb.saveData(TABLE, data, { provider: provider });
} else {
// Создаем новую запись
return await encryptedDb.saveData(TABLE, data);
}
}
async function deleteProviderSettings(provider) {
await db.getQuery()(
`DELETE FROM ${TABLE} WHERE provider = $1`,
[provider]
);
await encryptedDb.deleteData(TABLE, { provider: provider });
}
async function getProviderModels(provider, { api_key, base_url } = {}) {
@@ -111,19 +111,130 @@ async function verifyProviderKey(provider, { api_key, base_url } = {}) {
}
async function getAllLLMModels() {
const { rows } = await db.getQuery()(
`SELECT provider, selected_model FROM ${TABLE} WHERE selected_model IS NOT NULL AND selected_model <> ''`
);
// Возвращаем массив объектов { id, provider }
return rows.map(r => ({ id: r.selected_model, provider: r.provider }));
try {
// Получаем все настройки провайдеров
const providers = await encryptedDb.getData(TABLE, {});
// Собираем все модели из всех провайдеров
const allModels = [];
for (const provider of providers) {
if (provider.selected_model) {
allModels.push({
id: provider.selected_model,
provider: provider.provider
});
}
}
// Для Ollama проверяем реально установленные модели
try {
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
// Проверяем, какие модели установлены в Ollama
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2) {
const modelName = parts[0];
allModels.push({
id: modelName,
provider: 'ollama'
});
}
}
} catch (ollamaError) {
console.error('Error checking Ollama models:', ollamaError);
// Если не удалось проверить Ollama, добавляем базовые модели
allModels.push({ id: 'qwen2.5:7b', provider: 'ollama' });
}
// Убираем дубликаты
const uniqueModels = [];
const seen = new Set();
for (const model of allModels) {
const key = `${model.id}-${model.provider}`;
if (!seen.has(key)) {
seen.add(key);
uniqueModels.push(model);
}
}
return uniqueModels;
} catch (error) {
console.error('Error getting LLM models:', error);
return [];
}
}
async function getAllEmbeddingModels() {
const { rows } = await db.getQuery()(
`SELECT provider, embedding_model FROM ${TABLE} WHERE embedding_model IS NOT NULL AND embedding_model <> ''`
);
// Возвращаем массив объектов { id, provider }
return rows.map(r => ({ id: r.embedding_model, provider: r.provider }));
try {
// Получаем все настройки провайдеров
const providers = await encryptedDb.getData(TABLE, {});
// Собираем все embedding модели из всех провайдеров
const allModels = [];
for (const provider of providers) {
if (provider.embedding_model) {
allModels.push({
id: provider.embedding_model,
provider: provider.provider
});
}
}
// Для Ollama проверяем реально установленные embedding модели
try {
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
// Проверяем, какие embedding модели установлены в Ollama
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2) {
const modelName = parts[0];
// Проверяем, что это embedding модель
if (modelName.includes('embed') || modelName.includes('bge') || modelName.includes('nomic')) {
allModels.push({
id: modelName,
provider: 'ollama'
});
}
}
}
} catch (ollamaError) {
console.error('Error checking Ollama embedding models:', ollamaError);
// Если не удалось проверить Ollama, добавляем базовые embedding модели
allModels.push({ id: 'mxbai-embed-large:latest', provider: 'ollama' });
}
// Убираем дубликаты
const uniqueModels = [];
const seen = new Set();
for (const model of allModels) {
const key = `${model.id}-${model.provider}`;
if (!seen.has(key)) {
seen.add(key);
uniqueModels.push(model);
}
}
return uniqueModels;
} catch (error) {
console.error('Error getting embedding models:', error);
return [];
}
}
module.exports = {

View File

@@ -10,6 +10,7 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const logger = require('../utils/logger');
const { ethers } = require('ethers');
@@ -34,13 +35,20 @@ class AuthService {
if (!message || !signature || !address) return false;
// Нормализуем входящий адрес
const normalizedAddress = ethers.getAddress(address).toLowerCase();
const normalizedAddress = ethers.getAddress(address);
// Восстанавливаем адрес из подписи
const recoveredAddress = ethers.verifyMessage(message, signature);
// Логируем для отладки
logger.info(`[verifySignature] Message: ${message}`);
logger.info(`[verifySignature] Signature: ${signature}`);
logger.info(`[verifySignature] Expected address: ${normalizedAddress}`);
logger.info(`[verifySignature] Recovered address: ${recoveredAddress}`);
logger.info(`[verifySignature] Addresses match: ${ethers.getAddress(recoveredAddress) === normalizedAddress}`);
// Сравниваем нормализованные адреса
return ethers.getAddress(recoveredAddress).toLowerCase() === normalizedAddress;
return ethers.getAddress(recoveredAddress) === normalizedAddress;
} catch (error) {
logger.error('Error in signature verification:', error);
return false;
@@ -58,43 +66,40 @@ class AuthService {
const normalizedAddress = ethers.getAddress(address).toLowerCase();
// Ищем пользователя по адресу в таблице user_identities
const userResult = await db.getQuery()(
`
SELECT u.* FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1
`,
[normalizedAddress]
);
const identities = await encryptedDb.getData('user_identities', {
provider: 'wallet',
provider_id: normalizedAddress
}, 1);
if (userResult.rows.length > 0) {
const user = userResult.rows[0];
if (identities.length > 0) {
const user = await encryptedDb.getData('users', { id: identities[0].user_id }, 1);
if (user.length === 0) {
throw new Error('User not found');
}
const userData = user[0];
// Проверяем роль администратора при каждой аутентификации
const isAdmin = await checkAdminRole(normalizedAddress);
// Если статус админа изменился, обновляем роль в базе данных
if (user.role === 'admin' && !isAdmin) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', user.id]);
logger.info(`Updated user ${user.id} role to user (admin tokens no longer present)`);
return { userId: user.id, isAdmin: false };
} else if (user.role !== 'admin' && isAdmin) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', user.id]);
logger.info(`Updated user ${user.id} role to admin (admin tokens found)`);
return { userId: user.id, isAdmin: true };
if (userData.role === 'admin' && !isAdmin) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', userData.id]);
logger.info(`Updated user ${userData.id} role to user (admin tokens no longer present)`);
return { userId: userData.id, isAdmin: false };
} else if (userData.role !== 'admin' && isAdmin) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userData.id]);
logger.info(`Updated user ${userData.id} role to admin (admin tokens found)`);
return { userId: userData.id, isAdmin: true };
}
return {
userId: user.id,
isAdmin: user.role === 'admin',
userId: userData.id,
isAdmin: userData.role === 'admin',
};
}
// Если пользователь не найден, создаем нового
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'user',
]);
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', ['user']);
const userId = newUserResult.rows[0].id;
// Добавляем идентификатор кошелька (всегда в нижнем регистре)
@@ -209,7 +214,7 @@ class AuthService {
}
// Создание сессии с проверкой роли
async createSession(session, { userId, authenticated, authType, guestId, address }) {
async createSession(session, { userId, authenticated, authType, guestId, address, isAdmin }) {
try {
// Если пользователь аутентифицирован, обрабатываем гостевые сообщения
if (authenticated && guestId) {
@@ -220,6 +225,7 @@ class AuthService {
session.userId = userId;
session.authenticated = authenticated;
session.authType = authType;
session.isAdmin = isAdmin || false;
// Сохраняем адрес кошелька если есть
if (address) {
@@ -237,6 +243,7 @@ class AuthService {
authenticated,
authType,
address,
isAdmin: isAdmin || false,
cookie: session.cookie,
}),
session.id,
@@ -328,7 +335,7 @@ class AuthService {
const email = result.providerId;
// Проверяем, существует ли пользователь с таким email
const userResult = await db.getQuery()('SELECT * FROM users WHERE id = $1', [userId]);
const userResult = await db.getQuery()('SELECT id, role, created_at, updated_at, is_blocked, blocked_at, preferred_language FROM users WHERE id = $1', [userId]);
if (userResult.rows.length === 0) {
return { verified: false };
@@ -428,9 +435,23 @@ class AuthService {
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя
if (session.guestId && isNewUser) {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, session.guestId]
'INSERT INTO guest_user_mapping (user_id, guest_id_encrypted) VALUES ($1, encrypt_text($2, $3)) ON CONFLICT (guest_id_encrypted) DO UPDATE SET user_id = $1',
[userId, session.guestId, encryptionKey]
);
logger.info(`[verifyTelegramAuth] Saved guest ID ${session.guestId} for user ${userId}`);
}
@@ -460,13 +481,27 @@ class AuthService {
// Обновляем роль пользователя в базе данных, если есть админские токены
if (isAdmin) {
try {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Находим userId по адресу
const userResult = await db.getQuery()(
`
SELECT u.id FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1`,
[address.toLowerCase()]
WHERE ui.provider_encrypted = encrypt_text('wallet', $2) AND ui.provider_id_encrypted = encrypt_text($1, $2)`,
[address.toLowerCase(), encryptionKey]
);
if (userResult.rows.length > 0) {
@@ -486,8 +521,8 @@ class AuthService {
`
SELECT u.id, u.role FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = 'wallet' AND ui.provider_id = $1`,
[address.toLowerCase()]
WHERE ui.provider_encrypted = encrypt_text('wallet', $2) AND ui.provider_id_encrypted = encrypt_text($1, $2)`,
[address.toLowerCase(), encryptionKey]
);
if (userResult.rows.length > 0 && userResult.rows[0].role === 'admin') {
@@ -531,7 +566,7 @@ class AuthService {
// Удаляем старые идентификаторы
for (const identity of identitiesToDelete) {
await db.getQuery()('DELETE FROM user_identities WHERE id = $1', [identity.id]);
logger.info(`Deleted old guest identity: ${identity.identity_value}`);
logger.info(`Deleted old guest identity: ${identity.id}`);
}
}
} catch (error) {
@@ -546,11 +581,8 @@ class AuthService {
*/
async getUserIdentities(userId) {
try {
const result = await db.getQuery()(
'SELECT * FROM user_identities WHERE user_id = $1 ORDER BY created_at DESC',
[userId]
);
return result.rows;
const identities = await encryptedDb.getData('user_identities', { user_id: userId }, null, 'created_at DESC');
return identities;
} catch (error) {
logger.error('[getUserIdentities] Error:', error);
throw error;
@@ -611,13 +643,13 @@ class AuthService {
);
// Проверяем, существует ли уже такой идентификатор
const existingResult = await db.getQuery()(
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
[provider, normalizedProviderId]
);
const existingIdentities = await encryptedDb.getData('user_identities', {
provider: provider,
provider_id: normalizedProviderId
}, 1);
if (existingResult.rows.length > 0) {
const existingUserId = existingResult.rows[0].user_id;
if (existingIdentities.length > 0) {
const existingUserId = existingIdentities[0].user_id;
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем
if (existingUserId === userId) {
@@ -779,8 +811,33 @@ class AuthService {
*/
async getUserTokenBalances(address) {
if (!address) return [];
const tokens = await authTokenService.getAllAuthTokens();
const rpcProviders = await rpcProviderService.getAllRpcProviders();
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
// Получаем токены и RPC с расшифровкой
const tokensResult = await db.getQuery()(
'SELECT id, min_balance, created_at, updated_at, decrypt_text(name_encrypted, $1) as name, decrypt_text(address_encrypted, $1) as address, decrypt_text(network_encrypted, $1) as network FROM auth_tokens',
[encryptionKey]
);
const tokens = tokensResult.rows;
const rpcProvidersResult = await db.getQuery()(
'SELECT id, chain_id, created_at, updated_at, decrypt_text(network_id_encrypted, $1) as network_id, decrypt_text(rpc_url_encrypted, $1) as rpc_url FROM rpc_providers',
[encryptionKey]
);
const rpcProviders = rpcProvidersResult.rows;
const rpcMap = {};
for (const rpc of rpcProviders) {
rpcMap[rpc.network_id] = rpc.rpc_url;
@@ -811,6 +868,41 @@ class AuthService {
}
return results;
}
/**
* Проверяет nonce для адреса кошелька
* @param {string} address - адрес кошелька
* @param {string} nonce - nonce для проверки
* @returns {Promise<boolean>} - true если nonce валиден
*/
async verifyNonce(address, nonce) {
try {
// Получаем nonce из базы данных через encryptedDb
const nonceData = await encryptedDb.getData('nonces', {
identity_value: address.toLowerCase()
}, 1);
if (nonceData.length === 0) {
logger.warn(`[verifyNonce] No nonce found for address: ${address}`);
return false;
}
// Получаем nonce из результата
const storedNonce = nonceData[0].nonce;
// Сравниваем с переданным nonce
const isValid = storedNonce === nonce;
if (!isValid) {
logger.warn(`[verifyNonce] Invalid nonce for address: ${address}. Expected: ${storedNonce}, Got: ${nonce}`);
}
return isValid;
} catch (error) {
logger.error(`[verifyNonce] Error verifying nonce for address ${address}:`, error);
return false;
}
}
}
// Создаем и экспортируем единственный экземпляр

View File

@@ -10,35 +10,59 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
async function getAllAuthTokens() {
const { rows } = await db.getQuery()('SELECT * FROM auth_tokens ORDER BY id');
return rows;
const tokens = await encryptedDb.getData('auth_tokens', {}, null, 'id');
return tokens;
}
async function saveAllAuthTokens(authTokens) {
await db.getQuery()('DELETE FROM auth_tokens');
// Удаляем все существующие токены
await encryptedDb.deleteData('auth_tokens', {});
// Сохраняем новые токены
for (const token of authTokens) {
await db.getQuery()(
'INSERT INTO auth_tokens (name, address, network, min_balance) VALUES ($1, $2, $3, $4)',
[token.name, token.address, token.network, token.minBalance]
);
await encryptedDb.saveData('auth_tokens', {
name: token.name,
address: token.address,
network: token.network,
min_balance: token.minBalance == null ? 0 : Number(token.minBalance)
});
}
}
async function upsertAuthToken(token) {
const minBalance = token.minBalance == null ? 0 : Number(token.minBalance);
await db.getQuery()(
`INSERT INTO auth_tokens (name, address, network, min_balance)
VALUES ($1, $2, $3, $4)
ON CONFLICT (address, network) DO UPDATE SET name=EXCLUDED.name, min_balance=EXCLUDED.min_balance`,
[token.name, token.address, token.network, minBalance]
);
// Проверяем, существует ли токен
const existingTokens = await encryptedDb.getData('auth_tokens', {
address: token.address,
network: token.network
}, 1);
if (existingTokens.length > 0) {
// Обновляем существующий токен
await encryptedDb.saveData('auth_tokens', {
name: token.name,
min_balance: minBalance
}, {
address: token.address,
network: token.network
});
} else {
// Создаем новый токен
await encryptedDb.saveData('auth_tokens', {
name: token.name,
address: token.address,
network: token.network,
min_balance: minBalance
});
}
}
async function deleteAuthToken(address, network) {
await db.getQuery()('DELETE FROM auth_tokens WHERE address = $1 AND network = $2', [address, network]);
await encryptedDb.deleteData('auth_tokens', { address, network });
}
module.exports = { getAllAuthTokens, saveAllAuthTokens, upsertAuthToken, deleteAuthToken };

View File

@@ -10,29 +10,39 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
class DbSettingsService {
async getSettings() {
const { rows } = await db.getQuery()('SELECT * FROM db_settings WHERE id = 1');
const rows = await encryptedDb.getData('db_settings', { id: 1 }, 1);
return rows[0];
}
async upsertSettings({ db_host, db_port, db_name, db_user, db_password }) {
const { rows } = await db.getQuery()(
`INSERT INTO db_settings (id, db_host, db_port, db_name, db_user, db_password, updated_at)
VALUES (1, $1, $2, $3, $4, $5, NOW())
ON CONFLICT (id) DO UPDATE SET
db_host = EXCLUDED.db_host,
db_port = EXCLUDED.db_port,
db_name = EXCLUDED.db_name,
db_user = EXCLUDED.db_user,
db_password = EXCLUDED.db_password,
updated_at = NOW()
RETURNING *`,
[db_host, db_port, db_name, db_user, db_password]
);
return rows[0];
const data = {
id: 1,
db_host,
db_port,
db_name,
db_user,
db_password,
updated_at: new Date()
};
// Пытаемся обновить существующую запись
const existing = await this.getSettings();
if (existing) {
return await encryptedDb.saveData('db_settings', data, { id: 1 });
} else {
return await encryptedDb.saveData('db_settings', data);
}
}
/**
* Получить статус шифрования
*/
getEncryptionStatus() {
return encryptedDb.getEncryptionStatus();
}
}

View File

@@ -0,0 +1,313 @@
/**
* 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/HB3-ACCELERATOR
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const { ethers } = require('ethers');
const logger = require('../utils/logger');
const { getRpcUrlByNetworkId } = require('./rpcProviderService');
/**
* Сервис для управления DLE v2 (Digital Legal Entity)
* Современный подход с единым контрактом
*/
class DLEV2Service {
/**
* Создает новое DLE v2 с заданными параметрами
* @param {Object} dleParams - Параметры DLE
* @returns {Promise<Object>} - Результат создания DLE
*/
async createDLE(dleParams) {
try {
logger.info('Начало создания DLE v2 с параметрами:', dleParams);
// Валидация входных данных
this.validateDLEParams(dleParams);
// Подготовка параметров для деплоя
const deployParams = this.prepareDeployParams(dleParams);
// Сохраняем параметры во временный файл
const paramsFile = this.saveParamsToFile(deployParams);
// Копируем параметры во временный файл с предсказуемым именем
const tempParamsFile = path.join(__dirname, '../scripts/deploy/current-params.json');
const deployDir = path.dirname(tempParamsFile);
if (!fs.existsSync(deployDir)) {
fs.mkdirSync(deployDir, { recursive: true });
}
fs.copyFileSync(paramsFile, tempParamsFile);
logger.info(`Файл параметров скопирован успешно`);
// Получаем rpc_url из базы по выбранной сети
const rpcUrl = await getRpcUrlByNetworkId(deployParams.network);
if (!rpcUrl) {
throw new Error(`RPC URL для сети ${deployParams.network} не найден в базе данных`);
}
if (!dleParams.privateKey) {
throw new Error('Приватный ключ для деплоя не передан');
}
// Запускаем скрипт деплоя с нужными переменными окружения
const result = await this.runDeployScript(paramsFile, {
rpcUrl,
privateKey: dleParams.privateKey,
networkId: deployParams.network,
envNetworkKey: deployParams.network.toUpperCase()
});
// Очищаем временные файлы
this.cleanupTempFiles(paramsFile, tempParamsFile);
return result;
} catch (error) {
logger.error('Ошибка при создании DLE v2:', error);
throw error;
}
}
/**
* Валидирует параметры DLE
* @param {Object} params - Параметры для валидации
*/
validateDLEParams(params) {
if (!params.name || params.name.trim() === '') {
throw new Error('Название DLE обязательно');
}
if (!params.symbol || params.symbol.trim() === '') {
throw new Error('Символ токена обязателен');
}
if (!params.location || params.location.trim() === '') {
throw new Error('Местонахождение DLE обязательно');
}
if (!params.partners || !Array.isArray(params.partners)) {
throw new Error('Партнеры должны быть массивом');
}
if (!params.amounts || !Array.isArray(params.amounts)) {
throw new Error('Суммы должны быть массивом');
}
if (params.partners.length !== params.amounts.length) {
throw new Error('Количество партнеров должно соответствовать количеству сумм распределения');
}
if (params.partners.length === 0) {
throw new Error('Должен быть указан хотя бы один партнер');
}
if (params.quorumPercentage > 100) {
throw new Error('Процент кворума не может превышать 100%');
}
// Проверяем адреса партнеров
for (let i = 0; i < params.partners.length; i++) {
if (!ethers.isAddress(params.partners[i])) {
throw new Error(`Неверный адрес партнера ${i + 1}: ${params.partners[i]}`);
}
}
}
/**
* Подготавливает параметры для деплоя
* @param {Object} params - Параметры DLE
* @returns {Object} - Подготовленные параметры
*/
prepareDeployParams(params) {
// Создаем копию объекта, чтобы не изменять исходный
const deployParams = { ...params };
// Преобразуем суммы из строк или чисел в BigNumber, если нужно
deployParams.amounts = params.amounts.map(amount => {
if (typeof amount === 'string' && !amount.startsWith('0x')) {
return ethers.parseEther(amount).toString();
}
return amount.toString();
});
// Преобразуем параметры голосования
deployParams.votingDelay = params.votingDelay || 1;
deployParams.votingPeriod = params.votingPeriod || 45818; // ~1 неделя
deployParams.proposalThreshold = params.proposalThreshold || ethers.parseEther("100000").toString();
deployParams.quorumPercentage = params.quorumPercentage || 4;
deployParams.minTimelockDelay = params.minTimelockDelay || 2;
// Убеждаемся, что isicCodes - это массив
if (!Array.isArray(deployParams.isicCodes)) {
deployParams.isicCodes = [];
}
return deployParams;
}
/**
* Сохраняет параметры во временный файл
* @param {Object} params - Параметры для сохранения
* @returns {string} - Путь к сохраненному файлу
*/
saveParamsToFile(params) {
const tempDir = path.join(__dirname, '../temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
const fileName = `dle-v2-params-${Date.now()}.json`;
const filePath = path.join(tempDir, fileName);
fs.writeFileSync(filePath, JSON.stringify(params, null, 2));
return filePath;
}
/**
* Запускает скрипт деплоя DLE v2
* @param {string} paramsFile - Путь к файлу с параметрами
* @returns {Promise<Object>} - Результат деплоя
*/
runDeployScript(paramsFile, extraEnv = {}) {
return new Promise((resolve, reject) => {
const scriptPath = path.join(__dirname, '../scripts/deploy/create-dle-v2.js');
if (!fs.existsSync(scriptPath)) {
reject(new Error('Скрипт деплоя DLE v2 не найден: ' + scriptPath));
return;
}
// Формируем универсальные переменные окружения
const envVars = {
...process.env,
[`${extraEnv.envNetworkKey}_RPC_URL`]: extraEnv.rpcUrl,
[`${extraEnv.envNetworkKey}_PRIVATE_KEY`]: extraEnv.privateKey
};
// Запускаем скрипт с нужной сетью
const hardhatProcess = spawn('npx', ['hardhat', 'run', scriptPath, '--network', extraEnv.networkId], {
cwd: path.join(__dirname, '..'),
env: envVars,
stdio: 'pipe'
});
let stdout = '';
let stderr = '';
hardhatProcess.stdout.on('data', (data) => {
stdout += data.toString();
logger.info(`[DLE v2 Deploy] ${data.toString().trim()}`);
});
hardhatProcess.stderr.on('data', (data) => {
stderr += data.toString();
logger.error(`[DLE v2 Deploy Error] ${data.toString().trim()}`);
});
hardhatProcess.on('close', (code) => {
if (code === 0) {
try {
// Пытаемся извлечь результат из stdout
const result = this.extractDeployResult(stdout);
resolve(result);
} catch (error) {
logger.error('Ошибка при извлечении результатов деплоя DLE v2:', error);
reject(new Error('Не удалось найти информацию о созданном DLE v2'));
}
} else {
reject(new Error(`Скрипт деплоя DLE v2 завершился с кодом ${code}: ${stderr}`));
}
});
hardhatProcess.on('error', (error) => {
logger.error('Ошибка запуска скрипта деплоя DLE v2:', error);
reject(error);
});
});
}
/**
* Извлекает результат деплоя из stdout
* @param {string} stdout - Вывод скрипта
* @returns {Object} - Результат деплоя
*/
extractDeployResult(stdout) {
// Ищем строки с адресами в выводе
const dleAddressMatch = stdout.match(/DLE v2 задеплоен по адресу: (0x[a-fA-F0-9]{40})/);
const timelockAddressMatch = stdout.match(/Таймлок создан по адресу: (0x[a-fA-F0-9]{40})/);
if (dleAddressMatch && timelockAddressMatch) {
return {
success: true,
data: {
dleAddress: dleAddressMatch[1],
timelockAddress: timelockAddressMatch[1],
version: 'v2'
}
};
}
throw new Error('Не удалось извлечь адреса из вывода скрипта');
}
/**
* Очищает временные файлы
* @param {string} paramsFile - Путь к файлу параметров
* @param {string} tempParamsFile - Путь к временному файлу параметров
*/
cleanupTempFiles(paramsFile, tempParamsFile) {
try {
if (fs.existsSync(paramsFile)) {
fs.unlinkSync(paramsFile);
}
if (fs.existsSync(tempParamsFile)) {
fs.unlinkSync(tempParamsFile);
}
} catch (error) {
logger.warn('Не удалось очистить временные файлы:', error);
}
}
/**
* Получает список всех созданных DLE v2
* @returns {Array<Object>} - Список DLE v2
*/
getAllDLEs() {
try {
const dlesDir = path.join(__dirname, '../contracts-data/dles');
if (!fs.existsSync(dlesDir)) {
return [];
}
const files = fs.readdirSync(dlesDir);
return files
.filter(file => file.endsWith('.json') && file.includes('dle-v2-'))
.map(file => {
try {
const data = JSON.parse(fs.readFileSync(path.join(dlesDir, file), 'utf8'));
return { ...data, _fileName: file };
} catch (error) {
logger.error(`Ошибка при чтении файла ${file}:`, error);
return null;
}
})
.filter(dle => dle !== null);
} catch (error) {
logger.error('Ошибка при получении списка DLE v2:', error);
return [];
}
}
}
module.exports = new DLEV2Service();

View File

@@ -14,7 +14,7 @@ const { pool } = require('../db');
const verificationService = require('./verification-service');
const logger = require('../utils/logger');
const EmailBotService = require('./emailBot.js');
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const authService = require('./auth-service');
const { checkAdminRole } = require('./admin-role');
const { broadcastContactsUpdate } = require('../wsHub');
@@ -31,12 +31,10 @@ class EmailAuth {
}
// Проверяем, существует ли пользователь с таким email
const existingEmailUser = await db.getQuery()(
`SELECT u.id FROM users u
JOIN user_identities i ON u.id = i.user_id
WHERE i.provider = 'email' AND i.provider_id = $1`,
[email.toLowerCase()]
);
const existingEmailUsers = await encryptedDb.getData('user_identities', {
provider: 'email',
provider_id: email.toLowerCase()
}, 1);
// Создаем или получаем ID пользователя
let userId;
@@ -47,16 +45,16 @@ class EmailAuth {
logger.info(
`[initEmailAuth] Using existing authenticated user ${userId} for email ${email}`
);
} else if (existingEmailUser.rows.length > 0) {
} else if (existingEmailUsers.length > 0) {
// Если найден пользователь с таким email, используем его ID
userId = existingEmailUser.rows[0].id;
userId = existingEmailUsers[0].user_id;
logger.info(`[initEmailAuth] Found existing user ${userId} with email ${email}`);
} else {
// Создаем временного пользователя, если нужно будет создать нового
const userResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [
'user',
]);
userId = userResult.rows[0].id;
const newUser = await encryptedDb.saveData('users', {
role: 'user'
});
userId = newUser.id;
session.tempUserId = userId;
logger.info(`[initEmailAuth] Created temporary user ${userId} for email ${email}`);
}
@@ -165,11 +163,10 @@ class EmailAuth {
finalUserId = session.tempUserId;
logger.info(`[checkEmailVerification] Using temporary user ${finalUserId}`);
} else {
const newUserResult = await db.getQuery()(
'INSERT INTO users (role) VALUES ($1) RETURNING id',
['user']
);
finalUserId = newUserResult.rows[0].id;
const newUserResult = await encryptedDb.saveData('users', {
role: 'user'
});
finalUserId = newUserResult.id;
logger.info(`[checkEmailVerification] Created new user ${finalUserId}`);
}
}
@@ -189,9 +186,9 @@ class EmailAuth {
logger.info(`[checkEmailVerification] Role for user ${finalUserId} determined as: ${userRole}`);
// Опционально: Обновить роль в таблице users, если она отличается
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [finalUserId]);
if (currentUser.rows.length > 0 && currentUser.rows[0].role !== userRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [userRole, finalUserId]);
const currentUser = await encryptedDb.getData('users', { id: finalUserId }, 1);
if (currentUser.length > 0 && currentUser[0].role !== userRole) {
await encryptedDb.saveData('users', { role: userRole, id: finalUserId });
logger.info(`[checkEmailVerification] Updated user role in DB to ${userRole}`);
}
} else {

View File

@@ -11,6 +11,7 @@
*/
console.log('[EmailBot] emailBot.js loaded');
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const nodemailer = require('nodemailer');
const Imap = require('imap');
@@ -31,7 +32,24 @@ class EmailBotService {
}
async getSettingsFromDb() {
const { rows } = await db.getQuery()('SELECT * FROM email_settings ORDER BY id LIMIT 1');
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const { rows } = await db.getQuery()(
'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $1) as smtp_host, decrypt_text(smtp_user_encrypted, $1) as smtp_user, decrypt_text(smtp_password_encrypted, $1) as smtp_password, decrypt_text(imap_host_encrypted, $1) as imap_host, decrypt_text(from_email_encrypted, $1) as from_email FROM email_settings ORDER BY id LIMIT 1',
[encryptionKey]
);
if (!rows.length) throw new Error('Email settings not found in DB');
return rows[0];
}
@@ -160,43 +178,68 @@ class EmailBotService {
return;
}
// 1.1 Найти или создать беседу
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
);
let conversationResult = await encryptedDb.getData(
'conversations',
{ user_id: userId },
1,
'updated_at DESC, created_at DESC'
);
let conversation;
if (conversationResult.rows.length === 0) {
if (conversationResult.length === 0) {
const title = `Чат с пользователем ${userId}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[userId, title]
);
conversation = newConv.rows[0];
const newConv = await encryptedDb.saveData(
'conversations',
{ user_id: userId, title: title, created_at: new Date(), updated_at: new Date() }
);
conversation = newConv;
} else {
conversation = conversationResult.rows[0];
conversation = conversationResult[0];
}
// Проверяем, что conversation создан успешно
if (!conversation || !conversation.id) {
logger.error(`[EmailBot] Conversation is undefined or has no id for user ${userId}`);
return;
}
// 2. Сохранять все сообщения с conversation_id
let hasAttachments = parsed.attachments && parsed.attachments.length > 0;
if (hasAttachments) {
for (const att of parsed.attachments) {
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10, $11, $12)`,
[userId, conversation.id, 'user', text, 'email', role, 'in',
att.filename,
att.contentType,
att.size,
att.content,
JSON.stringify({ subject, html })
]
);
await encryptedDb.saveData(
'messages',
{
user_id: userId,
conversation_id: conversation.id,
sender_type: 'user',
content: text,
channel: 'email',
role: role,
direction: 'in',
created_at: new Date(),
attachment_filename: att.filename,
attachment_mimetype: att.contentType,
attachment_size: att.size,
attachment_data: att.content,
metadata: JSON.stringify({ subject, html })
}
);
}
} else {
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[userId, conversation.id, 'user', text, 'email', role, 'in', JSON.stringify({ subject, html })]
);
await encryptedDb.saveData(
'messages',
{
user_id: userId,
conversation_id: conversation.id,
sender_type: 'user',
content: text,
channel: 'email',
role: role,
direction: 'in',
created_at: new Date(),
metadata: JSON.stringify({ subject, html })
}
);
}
// 3. Получить ответ от ИИ (RAG + LLM)
const aiSettings = await aiAssistantSettingsService.getSettings();
@@ -231,11 +274,20 @@ class EmailBotService {
return;
}
// 4. Сохранить ответ в БД с conversation_id
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[userId, conversation.id, 'assistant', aiResponse, 'email', role, 'out', JSON.stringify({ subject, html })]
);
await encryptedDb.saveData(
'messages',
{
user_id: userId,
conversation_id: conversation.id,
sender_type: 'assistant',
content: aiResponse,
channel: 'email',
role: role,
direction: 'out',
created_at: new Date(),
metadata: JSON.stringify({ subject, html })
}
);
// 5. Отправить ответ на email
await this.sendEmail(fromEmail, 'Re: ' + subject, aiResponse);
// После каждого успешного создания пользователя:
@@ -359,10 +411,10 @@ class EmailBotService {
}
}
async getAllEmailSettings() {
const { rows } = await db.getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
return rows;
}
async getAllEmailSettings() {
const settings = await encryptedDb.getData('email_settings', {}, null, 'id');
return settings;
}
}
console.log('[EmailBot] module.exports = EmailBotService');

View File

@@ -0,0 +1,440 @@
/**
* 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/HB3-ACCELERATOR
*/
const db = require('../db');
const fs = require('fs');
const path = require('path');
class EncryptedDataService {
constructor() {
this.encryptionKey = this.loadEncryptionKey();
this.isEncryptionEnabled = !!this.encryptionKey;
if (this.isEncryptionEnabled) {
console.log('🔐 Шифрование базы данных активировано');
console.log('📋 Автоматическое определение зашифрованных колонок');
} else {
console.log('⚠️ Шифрование базы данных отключено - ключ не найден');
}
}
loadEncryptionKey() {
try {
const keyPath = path.join(__dirname, '../../ssl/keys/full_db_encryption.key');
console.log(`[EncryptedDB] Trying key path: ${keyPath}`);
if (fs.existsSync(keyPath)) {
const key = fs.readFileSync(keyPath, 'utf8').trim();
console.log(`[EncryptedDB] Key loaded from: ${keyPath}, length: ${key.length}`);
return key;
}
// Попробуем альтернативный путь относительно корня приложения
const altKeyPath = '/app/ssl/keys/full_db_encryption.key';
console.log(`[EncryptedDB] Trying alternative key path: ${altKeyPath}`);
if (fs.existsSync(altKeyPath)) {
const key = fs.readFileSync(altKeyPath, 'utf8').trim();
console.log(`[EncryptedDB] Key loaded from: ${altKeyPath}, length: ${key.length}`);
return key;
}
console.log(`[EncryptedDB] No key file found, using default key`);
return 'default-key';
} catch (error) {
console.error('❌ Ошибка загрузки ключа шифрования:', error);
return 'default-key';
}
}
/**
* Получить данные из таблицы с автоматической расшифровкой
*/
async getData(tableName, conditions = {}, limit = null, orderBy = null) {
try {
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedQuery(tableName, conditions, limit, orderBy);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
// Строим SELECT с расшифровкой
const selectFields = columns.map(col => {
if (col.column_name.endsWith('_encrypted')) {
const originalName = col.column_name.replace('_encrypted', '');
console.log(`🔓 Расшифровываем поле ${col.column_name} -> ${originalName}`);
if (col.data_type === 'jsonb') {
return `decrypt_json(${col.column_name}, $1) as "${originalName}"`;
} else {
return `decrypt_text(${col.column_name}, $1) as "${originalName}"`;
}
} else if (!col.column_name.includes('_encrypted')) {
// Проверяем, есть ли зашифрованная версия этой колонки
const hasEncryptedVersion = columns.some(encCol =>
encCol.column_name === `${col.column_name}_encrypted`
);
// Если есть зашифрованная версия, пропускаем незашифрованную
if (hasEncryptedVersion) {
console.log(`⚠️ Пропускаем незашифрованное поле ${col.column_name} (есть зашифрованная версия)`);
return null;
}
// Заключаем зарезервированные слова в кавычки
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
if (reservedWords.includes(col.column_name.toLowerCase())) {
return `"${col.column_name}"`;
}
return col.column_name;
}
return null;
}).filter(Boolean).join(', ');
let query = `SELECT ${selectFields} FROM ${tableName}`;
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
const params = hasEncryptedFields ? [this.encryptionKey] : [];
let paramIndex = hasEncryptedFields ? 2 : 1;
// Список зарезервированных слов для WHERE-условий
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => {
const value = conditions[key];
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
// Обрабатываем оператор $in
if (value && typeof value === 'object' && value.$in && Array.isArray(value.$in)) {
const placeholders = value.$in.map(() => `$${paramIndex++}`).join(', ');
const columnName = encryptedColumn ? key :
(reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key);
return `${columnName} IN (${placeholders})`;
}
// Обрабатываем оператор $ne
if (value && typeof value === 'object' && value.$ne !== undefined) {
const columnName = encryptedColumn ? key :
(reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key);
return `${columnName} != $${paramIndex++}`;
}
if (encryptedColumn) {
// Для зашифрованных колонок используем прямое сравнение с зашифрованным значением
return `${key}_encrypted = encrypt_text($${paramIndex++}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
} else {
// Для незашифрованных колонок используем обычное сравнение
// Заключаем зарезервированные слова в кавычки
const columnName = reservedWords.includes(key.toLowerCase()) ? `"${key}"` : key;
return `${columnName} = $${paramIndex++}`;
}
})
.join(' AND ');
query += ` WHERE ${whereClause}`;
// Добавляем параметры для $in операторов
const paramsToAdd = Object.values(conditions).map(value => {
if (value && typeof value === 'object' && value.$in && Array.isArray(value.$in)) {
return value.$in;
}
if (value && typeof value === 'object' && value.$ne !== undefined) {
return value.$ne;
}
return value;
}).flat();
params.push(...paramsToAdd);
}
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
if (limit) {
query += ` LIMIT ${limit}`;
}
console.log(`🔍 [getData] Выполняем запрос:`, query);
console.log(`🔍 [getData] Параметры:`, params);
const { rows } = await db.getQuery()(query, params);
console.log(`📊 Результат запроса из ${tableName}:`, rows);
return rows;
} catch (error) {
console.error(`❌ Ошибка получения данных из ${tableName}:`, error);
throw error;
}
}
/**
* Сохранить данные в таблицу с автоматическим шифрованием
*/
async saveData(tableName, data, whereConditions = null) {
try {
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
return await this.executeUnencryptedSave(tableName, data, whereConditions);
}
// Для таблицы users используем обычные запросы, так как она содержит смешанные колонки
if (tableName === 'users') {
return await this.executeUnencryptedSave(tableName, data, whereConditions);
}
// Получаем информацию о колонках
const { rows: columns } = await db.getQuery()(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position
`, [tableName]);
// Подготавливаем данные для шифрования
const encryptedData = {};
const unencryptedData = {};
const filteredData = {}; // Отфильтрованные данные для параметров
// Проверяем, есть ли зашифрованные поля в таблице
const hasEncryptedFields = columns.some(col => col.column_name.endsWith('_encrypted'));
let paramIndex = hasEncryptedFields ? 2 : 1; // Начинаем с 2, если есть зашифрованные поля, иначе с 1
for (const [key, value] of Object.entries(data)) {
// Проверяем, есть ли зашифрованная версия колонки
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
const unencryptedColumn = columns.find(col => col.column_name === key);
console.log(`🔍 Обрабатываем поле ${key} = "${value}" (тип: ${typeof value})`);
if (encryptedColumn) {
// Если есть зашифрованная колонка, шифруем данные
// Проверяем, что значение не пустое перед шифрованием
if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) {
// Пропускаем пустые значения
console.log(`⚠️ Пропускаем пустое зашифрованное поле ${key}`);
continue;
}
const currentParamIndex = paramIndex++;
filteredData[key] = value; // Добавляем в отфильтрованные данные
console.log(`✅ Добавили зашифрованное поле ${key} в filteredData`);
if (encryptedColumn.data_type === 'jsonb') {
encryptedData[`${key}_encrypted`] = `encrypt_json($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
} else {
encryptedData[`${key}_encrypted`] = `encrypt_text($${currentParamIndex}, ${hasEncryptedFields ? '$1::text' : 'NULL'})`;
}
} else if (unencryptedColumn) {
// Если есть незашифрованная колонка, сохраняем как есть
// Проверяем, что значение не пустое перед сохранением (кроме role и sender_type)
if ((value === null || value === undefined || (typeof value === 'string' && value.trim() === '')) &&
key !== 'role' && key !== 'sender_type') {
// Пропускаем пустые значения, кроме role и sender_type
console.log(`⚠️ Пропускаем пустое незашифрованное поле ${key}`);
continue;
}
filteredData[key] = value; // Добавляем в отфильтрованные данные
unencryptedData[key] = `$${paramIndex++}`;
console.log(`✅ Добавили незашифрованное поле ${key} в filteredData и unencryptedData`);
} else {
// Если колонка не найдена, пропускаем
console.warn(`⚠️ Колонка ${key} не найдена в таблице ${tableName}`);
}
}
const allData = { ...unencryptedData, ...encryptedData };
// Проверяем, есть ли данные для сохранения
if (Object.keys(allData).length === 0) {
console.warn(`⚠️ Нет данных для сохранения в таблице ${tableName} - все значения пустые`);
console.warn(`⚠️ Исходные данные:`, data);
console.warn(`⚠️ Отфильтрованные данные:`, filteredData);
return null;
}
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
if (whereConditions) {
// UPDATE
const setClause = Object.keys(allData)
.map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${quoteReservedWord(key)} = $${paramIndex + index}`)
.join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const allParams = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData), ...Object.values(whereConditions)] : [...Object.values(filteredData), ...Object.values(whereConditions)];
const { rows } = await db.getQuery()(query, allParams);
return rows[0];
} else {
// INSERT
const columns = Object.keys(allData).map(key => quoteReservedWord(key));
const placeholders = Object.keys(allData).map(key => allData[key]).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const params = hasEncryptedFields ? [this.encryptionKey, ...Object.values(filteredData)] : [...Object.values(filteredData)];
const { rows } = await db.getQuery()(query, params);
return rows[0];
}
} catch (error) {
console.error(`❌ Ошибка сохранения данных в ${tableName}:`, error);
throw error;
}
}
/**
* Удалить данные из таблицы
*/
async deleteData(tableName, conditions) {
try {
// Функция для заключения зарезервированных слов в кавычки
const quoteReservedWord = (word) => {
const reservedWords = ['order', 'group', 'user', 'index', 'table', 'column', 'key', 'foreign', 'primary', 'unique', 'check', 'constraint', 'default', 'null', 'not', 'and', 'or', 'as', 'on', 'in', 'is', 'like', 'between', 'exists', 'all', 'any', 'some', 'distinct', 'case', 'when', 'then', 'else', 'end', 'limit', 'offset', 'having', 'union', 'intersect', 'except', 'with', 'recursive'];
return reservedWords.includes(word.toLowerCase()) ? `"${word}"` : word;
};
// Проверяем, включено ли шифрование
if (!this.isEncryptionEnabled) {
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
// Для зашифрованных таблиц - пока используем обычный DELETE
// TODO: Добавить логику для зашифрованных условий WHERE
let query = `DELETE FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${quoteReservedWord(key)} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
const { rows } = await db.getQuery()(query, params);
return rows;
} catch (error) {
console.error(`❌ Ошибка удаления данных из ${tableName}:`, error);
throw error;
}
}
/**
* Получить статус шифрования
*/
getEncryptionStatus() {
return {
enabled: this.isEncryptionEnabled,
keyExists: !!this.encryptionKey,
keyPath: path.join(__dirname, '../../ssl/keys/full_db_encryption.key')
};
}
/**
* Проверить, нужно ли шифровать колонку
*/
shouldEncryptColumn(column) {
const encryptableTypes = ['text', 'varchar', 'character varying', 'json', 'jsonb'];
return encryptableTypes.includes(column.data_type) &&
!column.column_name.includes('_encrypted') &&
!['created_at', 'updated_at', 'id'].includes(column.column_name);
}
/**
* Выполнить незашифрованный запрос (fallback)
*/
async executeUnencryptedQuery(tableName, conditions = {}, limit = null, orderBy = null) {
let query = `SELECT * FROM ${tableName}`;
const params = [];
let paramIndex = 1;
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions)
.map(key => `${key} = $${paramIndex++}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
params.push(...Object.values(conditions));
}
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
if (limit) {
query += ` LIMIT ${limit}`;
}
const { rows } = await db.getQuery()(query, params);
return rows;
}
/**
* Выполнить незашифрованное сохранение (fallback)
*/
async executeUnencryptedSave(tableName, data, whereConditions = null) {
if (whereConditions) {
// UPDATE
const setClause = Object.keys(data)
.map((key, index) => `${key} = $${index + 1}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${key} = $${Object.keys(data).length + index + 1}`)
.join(' AND ');
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
const params = [...Object.values(data), ...Object.values(whereConditions)];
const { rows } = await db.getQuery()(query, params);
return rows[0];
} else {
// INSERT
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
const query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) RETURNING *`;
const { rows } = await db.getQuery()(query, values);
return rows[0];
}
}
}
module.exports = new EncryptedDataService();

View File

@@ -12,6 +12,7 @@
console.log('[identity-service] loaded');
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const logger = require('../utils/logger');
const { getLinkedWallet } = require('./wallet-service');
@@ -53,7 +54,7 @@ class IdentityService {
* @param {number} userId - ID пользователя
* @param {string} provider - Тип идентификатора (wallet, email, telegram)
* @param {string} providerId - Значение идентификатора
* @param {boolean} verified - Флаг верификации идентификатора
* @param {boolean} verified - Флаг верификации идентификатора (не используется в БД)
* @returns {Promise<object>} - Результат операции
*/
async saveIdentity(userId, provider, providerId, verified = true) {
@@ -79,10 +80,10 @@ class IdentityService {
);
try {
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, normalizedProviderId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: normalizedProviderId
});
return { success: true };
} catch (guestError) {
logger.error(
@@ -99,54 +100,42 @@ class IdentityService {
logger.warn(`[IdentityService] Invalid provider type: ${normalizedProvider}`);
return {
success: false,
error: `Invalid provider type. Allowed types: ${allowedProviders.join(', ')}`,
error: `Invalid provider type: ${normalizedProvider}`,
};
}
logger.info(
`[IdentityService] Saving identity for user ${userId}: ${normalizedProvider}:${normalizedProviderId}`
);
// Проверяем, существует ли уже такой идентификатор
const existingResult = await db.getQuery()(
`SELECT user_id FROM user_identities WHERE provider = $1 AND provider_id = $2`,
[normalizedProvider, normalizedProviderId]
);
const existingIdentity = await this.findIdentity(userId, normalizedProvider);
if (existingIdentity) {
// Обновляем существующий идентификатор
await encryptedDb.saveData('user_identities', {
provider: normalizedProvider,
provider_id: normalizedProviderId
}, {
user_id: userId,
provider: normalizedProvider
});
if (existingResult.rows.length > 0) {
const existingUserId = existingResult.rows[0].user_id;
// Если идентификатор уже принадлежит этому пользователю, ничего не делаем
if (existingUserId === userId) {
logger.info(
`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already exists for user ${userId}`
`[IdentityService] Updated identity for user ${userId}: ${normalizedProvider}=${normalizedProviderId}`
);
} else {
// Если идентификатор принадлежит другому пользователю, логируем это
logger.warn(
`[IdentityService] Identity ${normalizedProvider}:${normalizedProviderId} already belongs to user ${existingUserId}, not user ${userId}`
);
return {
success: false,
error: `Identity already belongs to another user (${existingUserId})`,
};
}
} else {
// Создаем новую запись
await db.getQuery()(
`INSERT INTO user_identities (user_id, provider, provider_id)
VALUES ($1, $2, $3)`,
[userId, normalizedProvider, normalizedProviderId]
);
// Создаем новый идентификатор
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: normalizedProvider,
provider_id: normalizedProviderId
});
logger.info(
`[IdentityService] Created new identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`
`[IdentityService] Saved new identity for user ${userId}: ${normalizedProvider}=${normalizedProviderId}`
);
}
return { success: true };
} catch (error) {
logger.error(
`[IdentityService] Error saving identity ${provider}:${providerId} for user ${userId}:`,
`[IdentityService] Error saving identity for user ${userId}:`,
error
);
return { success: false, error: error.message };
@@ -160,18 +149,9 @@ class IdentityService {
*/
async getUserIdentities(userId) {
try {
if (!userId) {
logger.warn('[IdentityService] Missing userId parameter');
return [];
}
const result = await db.getQuery()(
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
[userId]
);
logger.info(`[IdentityService] Found ${result.rows.length} identities for user ${userId}`);
return result.rows;
const identities = await encryptedDb.getData('user_identities', { user_id: userId });
logger.info(`[IdentityService] Found ${identities.length} identities for user ${userId}`);
return identities;
} catch (error) {
logger.error(`[IdentityService] Error getting identities for user ${userId}:`, error);
return [];
@@ -179,121 +159,70 @@ class IdentityService {
}
/**
* Получает все идентификаторы пользователя определенного типа
* Получает идентификаторы пользователя по типу провайдера
* @param {number} userId - ID пользователя
* @param {string} provider - Тип идентификатора
* @param {string} provider - Тип провайдера
* @returns {Promise<Array>} - Массив идентификаторов
*/
async getUserIdentitiesByProvider(userId, provider) {
try {
if (!userId || !provider) {
logger.warn(`[IdentityService] Missing parameters: userId=${userId}, provider=${provider}`);
return [];
}
const result = await db.getQuery()(
`SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2`,
[userId, provider]
);
logger.info(
`[IdentityService] Found ${result.rows.length} ${provider} identities for user ${userId}`
);
return result.rows.map((row) => row.provider_id);
const identities = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: provider.toLowerCase()
});
return identities;
} catch (error) {
logger.error(
`[IdentityService] Error getting ${provider} identities for user ${userId}:`,
error
);
logger.error(`[IdentityService] Error getting identities by provider for user ${userId}:`, error);
return [];
}
}
/**
* Находит пользователя по идентификатору
* @param {string} provider - Тип идентификатора
* @param {string} provider - Тип провайдера
* @param {string} providerId - Значение идентификатора
* @returns {Promise<object|null>} - Информация о пользователе или null
* @returns {Promise<object|null>} - Пользователь или null
*/
async findUserByIdentity(provider, providerId) {
try {
if (!provider || !providerId) {
logger.warn(
`[IdentityService] Missing parameters: provider=${provider}, providerId=${providerId}`
);
return null;
}
// Нормализуем значения
const { provider: normalizedProvider, providerId: normalizedProviderId } =
this.normalizeIdentity(provider, providerId);
const result = await db.getQuery()(
`SELECT u.id, u.role FROM users u
JOIN user_identities ui ON u.id = ui.user_id
WHERE ui.provider = $1 AND ui.provider_id = $2`,
[normalizedProvider, normalizedProviderId]
);
const identities = await encryptedDb.getData('user_identities', {
provider: normalizedProvider,
provider_id: normalizedProviderId
}, 1);
if (result.rows.length === 0) {
logger.info(
`[IdentityService] No user found with identity ${normalizedProvider}:${normalizedProviderId}`
);
if (identities.length === 0) {
return null;
}
logger.info(
`[IdentityService] Found user ${result.rows[0].id} with identity ${normalizedProvider}:${normalizedProviderId}`
);
return result.rows[0];
const userId = identities[0].user_id;
const users = await encryptedDb.getData('users', { id: userId }, 1);
return users.length > 0 ? users[0] : null;
} catch (error) {
logger.error(
`[IdentityService] Error finding user by identity ${provider}:${providerId}:`,
error
);
logger.error(`[IdentityService] Error finding user by identity:`, error);
return null;
}
}
/**
* Находит конкретный идентификатор пользователя по его типу.
* Возвращает первую найденную запись.
* Находит конкретный идентификатор пользователя
* @param {number} userId - ID пользователя
* @param {string} provider - Тип идентификатора (например, 'wallet', 'email')
* @returns {Promise<object|null>} - Объект идентификатора (содержит provider_id) или null
* @param {string} provider - Тип провайдера
* @returns {Promise<object|null>} - Идентификатор или null
*/
async findIdentity(userId, provider) {
try {
if (!userId || !provider) {
logger.warn(`[IdentityService] Missing parameters for findIdentity: userId=${userId}, provider=${provider}`);
return null;
}
const identities = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: provider.toLowerCase()
}, 1);
// Нормализуем провайдера
const normalizedProvider = provider.toLowerCase();
const result = await db.getQuery()(
`SELECT provider, provider_id, created_at, updated_at
FROM user_identities
WHERE user_id = $1 AND provider = $2
LIMIT 1`,
[userId, normalizedProvider]
);
if (result.rows.length === 0) {
logger.info(`[IdentityService] No ${normalizedProvider} identity found for user ${userId}`);
return null;
}
logger.info(
`[IdentityService] Found ${normalizedProvider} identity for user ${userId}: ${result.rows[0].provider_id}`
);
return result.rows[0]; // Возвращаем всю строку (включая provider_id)
return identities.length > 0 ? identities[0] : null;
} catch (error) {
logger.error(
`[IdentityService] Error finding ${provider} identity for user ${userId}:`,
error
);
logger.error(`[IdentityService] Error finding identity for user ${userId}:`, error);
return null;
}
}
@@ -337,10 +266,10 @@ class IdentityService {
// Сохраняем гостевые идентификаторы в guest_user_mapping
if (session.guestId) {
try {
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, session.guestId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: session.guestId
});
results.push({ type: 'guest', result: { success: true } });
} catch (error) {
logger.error(`[IdentityService] Error saving guest ID for user ${userId}:`, error);
@@ -350,10 +279,10 @@ class IdentityService {
if (session.previousGuestId && session.previousGuestId !== session.guestId) {
try {
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, session.previousGuestId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: session.previousGuestId
});
results.push({ type: 'previousGuest', result: { success: true } });
} catch (error) {
logger.error(
@@ -392,92 +321,75 @@ class IdentityService {
return { success: false, error: 'Missing required parameters' };
}
// Начинаем транзакцию
const client = await db.pool.connect();
try {
await client.query('BEGIN');
// Получаем все идентификаторы исходного пользователя
const identitiesResult = await client.query(
`SELECT provider, provider_id FROM user_identities WHERE user_id = $1`,
[fromUserId]
);
const identities = await encryptedDb.getData('user_identities', { user_id: fromUserId });
// Переносим каждый идентификатор
for (const identity of identitiesResult.rows) {
await client.query(
`INSERT INTO user_identities (user_id, provider, provider_id)
VALUES ($1, $2, $3)
ON CONFLICT (provider, provider_id) DO NOTHING`,
[toUserId, identity.provider, identity.provider_id]
);
for (const identity of identities) {
// Создаем новый идентификатор для целевого пользователя
await encryptedDb.saveData('user_identities', {
user_id: toUserId,
provider: identity.provider,
provider_id: identity.provider_id
});
// Удаляем старый идентификатор
await client.query(
`DELETE FROM user_identities
WHERE user_id = $1 AND provider = $2 AND provider_id = $3`,
[fromUserId, identity.provider, identity.provider_id]
);
await encryptedDb.deleteData('user_identities', {
user_id: fromUserId,
provider: identity.provider,
provider_id: identity.provider_id
});
}
// Мигрируем гостевые идентификаторы из новой таблицы guest_user_mapping
const guestMappingsResult = await client.query(
`SELECT guest_id, processed FROM guest_user_mapping WHERE user_id = $1`,
[fromUserId]
);
// Мигрируем гостевые идентификаторы
const guestMappings = await encryptedDb.getData('guest_user_mapping', { user_id: fromUserId });
// Переносим каждый гостевой идентификатор
for (const mapping of guestMappingsResult.rows) {
await client.query(
`INSERT INTO guest_user_mapping (user_id, guest_id, processed)
VALUES ($1, $2, $3)
ON CONFLICT (guest_id) DO UPDATE
SET user_id = $1, processed = EXCLUDED.processed OR guest_user_mapping.processed`,
[toUserId, mapping.guest_id, mapping.processed]
);
for (const mapping of guestMappings) {
await encryptedDb.saveData('guest_user_mapping', {
user_id: toUserId,
guest_id: mapping.guest_id,
processed: mapping.processed
});
}
// Удаляем старые гостевые маппинги
await client.query(`DELETE FROM guest_user_mapping WHERE user_id = $1`, [fromUserId]);
await encryptedDb.deleteData('guest_user_mapping', { user_id: fromUserId });
// Переносим все сообщения
await client.query(
`UPDATE messages
SET user_id = $1
WHERE user_id = $2`,
[toUserId, fromUserId]
);
const messages = await encryptedDb.getData('messages', { user_id: fromUserId });
for (const message of messages) {
await encryptedDb.saveData('messages', {
...message,
user_id: toUserId
});
await encryptedDb.deleteData('messages', { id: message.id });
}
// Переносим все диалоги
await client.query(
`UPDATE conversations
SET user_id = $1
WHERE user_id = $2`,
[toUserId, fromUserId]
);
const conversations = await encryptedDb.getData('conversations', { user_id: fromUserId });
for (const conversation of conversations) {
await encryptedDb.saveData('conversations', {
...conversation,
user_id: toUserId
});
await encryptedDb.deleteData('conversations', { id: conversation.id });
}
// Переносим настройки пользователя
await client.query(
`UPDATE user_preferences
SET user_id = $1
WHERE user_id = $2`,
[toUserId, fromUserId]
);
// Завершаем транзакцию
await client.query('COMMIT');
const preferences = await encryptedDb.getData('user_preferences', { user_id: fromUserId });
for (const preference of preferences) {
await encryptedDb.saveData('user_preferences', {
...preference,
user_id: toUserId
});
await encryptedDb.deleteData('user_preferences', { id: preference.id });
}
logger.info(
`[IdentityService] Successfully migrated data from user ${fromUserId} to ${toUserId}`
);
return { success: true };
} catch (error) {
await client.query('ROLLBACK');
logger.error(`[IdentityService] Transaction error:`, error);
return { success: false, error: error.message };
} finally {
client.release();
}
} catch (error) {
logger.error(`[IdentityService] Error migrating user data:`, error);
return { success: false, error: error.message };
@@ -496,14 +408,12 @@ class IdentityService {
for (const [provider, providerId] of Object.entries(identities)) {
if (!providerId) continue;
const result = await db.getQuery()(
`SELECT DISTINCT user_id
FROM user_identities
WHERE provider = $1 AND provider_id = $2`,
[provider, providerId]
);
const users = await encryptedDb.getData('user_identities', {
provider: provider,
provider_id: providerId
});
result.rows.forEach((row) => userIds.add(row.user_id));
users.forEach((user) => userIds.add(user.user_id));
}
return Array.from(userIds);
@@ -527,12 +437,13 @@ class IdentityService {
return { success: false, error: 'Missing required parameters' };
}
const { provider: normalizedProvider, providerId: normalizedProviderId } = this.normalizeIdentity(provider, providerId);
const result = await db.getQuery()(
`DELETE FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3`,
[userId, normalizedProvider, normalizedProviderId]
);
const result = await encryptedDb.deleteData('user_identities', {
user_id: userId,
provider: normalizedProvider,
provider_id: normalizedProviderId
});
logger.info(`[IdentityService] Deleted identity ${normalizedProvider}:${normalizedProviderId} for user ${userId}`);
return { success: true, deleted: result.rowCount };
return { success: true, deleted: result.length };
} catch (error) {
logger.error(`[IdentityService] Error deleting identity ${provider}:${providerId} for user ${userId}:`, error);
return { success: false, error: error.message };
@@ -551,8 +462,10 @@ class IdentityService {
let isNew = false;
if (!user) {
// Создаем пользователя
const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', ['user']);
const userId = newUserResult.rows[0].id;
const newUser = await encryptedDb.saveData('users', {
role: 'user'
});
const userId = newUser.id;
await this.saveIdentity(userId, provider, providerId, true);
user = { id: userId, role: 'user' };
isNew = true;
@@ -567,7 +480,11 @@ class IdentityService {
role = isAdmin ? 'admin' : 'user';
// Обновляем роль в users, если изменилась
if (user.role !== role) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, user.id]);
await encryptedDb.saveData('users', {
role: role
}, {
id: user.id
});
}
}
return { userId: user.id, role, isNew };

View File

@@ -10,22 +10,26 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const vectorSearch = require('./vectorSearchClient');
const { getProviderSettings } = require('./aiProviderSettingsService');
console.log('[RAG] ragService.js loaded');
// Простой кэш для RAG результатов
const ragCache = new Map();
const RAG_CACHE_TTL = 5 * 60 * 1000; // 5 минут
async function getTableData(tableId) {
console.log(`[RAG] getTableData called for tableId: ${tableId}`);
const columns = (await db.query('SELECT * FROM user_columns WHERE table_id = $1', [tableId])).rows;
const columns = await encryptedDb.getData('user_columns', { table_id: tableId });
console.log(`[RAG] Found ${columns.length} columns:`, columns.map(col => ({ id: col.id, name: col.name, purpose: col.options?.purpose })));
const rows = (await db.query('SELECT * FROM user_rows WHERE table_id = $1', [tableId])).rows;
const rows = await encryptedDb.getData('user_rows', { table_id: tableId });
console.log(`[RAG] Found ${rows.length} rows:`, rows.map(row => ({ id: row.id, name: row.name })));
const cellValues = (await db.query('SELECT * FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId])).rows;
const cellValues = await encryptedDb.getData('user_cell_values', { row_id: { $in: rows.map(row => row.id) } });
console.log(`[RAG] Found ${cellValues.length} cell values`);
const getColId = purpose => columns.find(col => col.options?.purpose === purpose)?.id;
@@ -66,6 +70,14 @@ async function getTableData(tableId) {
async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10 }) {
console.log(`[RAG] ragAnswer called: tableId=${tableId}, userQuestion="${userQuestion}"`);
// Проверяем кэш
const cacheKey = `${tableId}:${userQuestion}:${product}`;
const cached = ragCache.get(cacheKey);
if (cached && (Date.now() - cached.timestamp) < RAG_CACHE_TTL) {
console.log(`[RAG] Returning cached result for: ${cacheKey}`);
return cached.result;
}
const data = await getTableData(tableId);
console.log(`[RAG] Got ${data.length} rows from database`);
@@ -110,7 +122,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
// Поиск
let results = [];
if (rowsForUpsert.length > 0) {
results = await vectorSearch.search(tableId, userQuestion, 3);
results = await vectorSearch.search(tableId, userQuestion, 2); // Уменьшаем до 2 результатов
console.log(`[RAG] Search completed, got ${results.length} results`);
// Подробное логирование результатов поиска
@@ -153,7 +165,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
});
}
return {
const result = {
answer: best?.metadata?.answer,
context: best?.metadata?.context,
product: best?.metadata?.product,
@@ -161,6 +173,14 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
date: best?.metadata?.date,
score: best?.score,
};
// Кэшируем результат
ragCache.set(cacheKey, {
result,
timestamp: Date.now()
});
return result;
}
/**
@@ -169,16 +189,11 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
*/
async function getAllPlaceholdersWithValues() {
// Получаем все плейсхолдеры и их значения (берём первое значение для каждого плейсхолдера)
const result = await db.getQuery()(`
SELECT c.placeholder, cv.value
FROM user_columns c
JOIN user_cell_values cv ON c.id = cv.column_id
WHERE c.placeholder IS NOT NULL AND c.placeholder != ''
ORDER BY c.id, cv.id
`);
const result = await encryptedDb.getData('user_columns', {});
// Группируем по плейсхолдеру (берём первое значение)
const map = {};
for (const row of result.rows) {
for (const row of result) {
if (row.placeholder && !(row.placeholder in map)) {
map[row.placeholder] = row.value;
}

View File

@@ -10,39 +10,56 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
async function getAllRpcProviders() {
const { rows } = await db.getQuery()('SELECT * FROM rpc_providers ORDER BY id');
return rows;
const providers = await encryptedDb.getData('rpc_providers', {}, null, 'id');
return providers;
}
async function saveAllRpcProviders(rpcConfigs) {
await db.getQuery()('DELETE FROM rpc_providers');
// Удаляем все существующие провайдеры
await encryptedDb.deleteData('rpc_providers', {});
// Сохраняем новые провайдеры
for (const cfg of rpcConfigs) {
await db.query(
'INSERT INTO rpc_providers (network_id, rpc_url, chain_id) VALUES ($1, $2, $3)',
[cfg.networkId, cfg.rpcUrl, cfg.chainId || null]
);
await encryptedDb.saveData('rpc_providers', {
network_id: cfg.networkId,
rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null
});
}
}
async function upsertRpcProvider(cfg) {
await db.query(
`INSERT INTO rpc_providers (network_id, rpc_url, chain_id)
VALUES ($1, $2, $3)
ON CONFLICT (network_id) DO UPDATE SET rpc_url=EXCLUDED.rpc_url, chain_id=EXCLUDED.chain_id`,
[cfg.networkId, cfg.rpcUrl, cfg.chainId || null]
);
// Проверяем, существует ли провайдер
const existing = await encryptedDb.getData('rpc_providers', { network_id: cfg.networkId }, 1);
if (existing.length > 0) {
// Обновляем существующий провайдер
await encryptedDb.saveData('rpc_providers', {
rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null
}, {
network_id: cfg.networkId
});
} else {
// Создаем новый провайдер
await encryptedDb.saveData('rpc_providers', {
network_id: cfg.networkId,
rpc_url: cfg.rpcUrl,
chain_id: cfg.chainId || null
});
}
}
async function deleteRpcProvider(networkId) {
await db.getQuery()('DELETE FROM rpc_providers WHERE network_id = $1', [networkId]);
await encryptedDb.deleteData('rpc_providers', { network_id: networkId });
}
async function getRpcUrlByNetworkId(networkId) {
const { rows } = await db.getQuery()('SELECT rpc_url FROM rpc_providers WHERE network_id = $1', [networkId]);
return rows[0]?.rpc_url || null;
const providers = await encryptedDb.getData('rpc_providers', { network_id: networkId }, 1);
return providers[0]?.rpc_url || null;
}
module.exports = { getAllRpcProviders, saveAllRpcProviders, upsertRpcProvider, deleteRpcProvider, getRpcUrlByNetworkId };

View File

@@ -11,7 +11,7 @@
*/
const logger = require('../utils/logger');
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const { processGuestMessages } = require('../routes/chat');
/**
@@ -50,11 +50,8 @@ class SessionService {
async linkGuestMessages(session, userId) {
try {
// Получаем все гостевые ID для текущего пользователя из таблицы
const guestIdsResult = await db.getQuery()(
'SELECT guest_id FROM guest_user_mapping WHERE user_id = $1',
[userId]
);
const userGuestIds = guestIdsResult.rows.map((row) => row.guest_id);
const guestIdsResult = await encryptedDb.getData('guest_user_mapping', { user_id: userId });
const userGuestIds = guestIdsResult.map((row) => row.guest_id);
// Собираем все гостевые ID, которые нужно обработать
const guestIdsToProcess = new Set();
@@ -66,10 +63,10 @@ class SessionService {
guestIdsToProcess.add(session.guestId);
// Записываем связь с пользователем в новую таблицу
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, session.guestId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: session.guestId
});
}
}
@@ -80,10 +77,10 @@ class SessionService {
guestIdsToProcess.add(session.previousGuestId);
// Записываем связь с пользователем в новую таблицу
await db.getQuery()(
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
[userId, session.previousGuestId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: session.previousGuestId
});
}
}
@@ -98,24 +95,18 @@ class SessionService {
// Логируем только если есть что обрабатывать
if (guestIdsToProcess.size > 0) {
logger.info(
`[linkGuestMessages] Processing ${guestIdsToProcess.size} guest IDs for user ${userId}`
`[SessionService] Linking ${guestIdsToProcess.size} guest IDs to user ${userId}: ${Array.from(guestIdsToProcess).join(', ')}`
);
// Обрабатываем сообщения для каждого гостевого ID
for (const guestId of guestIdsToProcess) {
await this.processGuestMessagesWrapper(userId, guestId);
}
}
// Обрабатываем все собранные гостевые ID
for (const guestId of guestIdsToProcess) {
await this.processGuestMessagesWrapper(userId, guestId);
// Помечаем guestId как обработанный в базе данных
await db.getQuery()(
'UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1',
[guestId]
);
}
return { success: true };
return { success: true, processedCount: guestIdsToProcess.size };
} catch (error) {
logger.error('[linkGuestMessages] Error:', error);
logger.error(`[SessionService] Error linking guest messages:`, error);
return { success: false, error: error.message };
}
}
@@ -127,12 +118,9 @@ class SessionService {
*/
async isGuestIdProcessed(guestId) {
try {
const result = await db.getQuery()(
'SELECT processed FROM guest_user_mapping WHERE guest_id = $1',
[guestId]
);
const result = await encryptedDb.getData('guest_user_mapping', { guest_id: guestId });
return result.rows.length > 0 && result.rows[0].processed === true;
return result.length > 0 && result[0].processed === true;
} catch (error) {
logger.error(`[isGuestIdProcessed] Error checking guest ID ${guestId}:`, error);
return false;
@@ -208,17 +196,14 @@ class SessionService {
logger.info(`[SessionService] Attempting to retrieve session ${sessionId}`);
const result = await db.getQuery()(
'SELECT sess FROM session WHERE sid = $1',
[sessionId]
);
const result = await encryptedDb.getData('session', { sid: sessionId });
if (result.rows.length === 0) {
if (result.length === 0) {
logger.info(`[SessionService] No session found with ID ${sessionId}`);
return null;
}
const sessionData = result.rows[0].sess;
const sessionData = result[0].sess;
logger.info(`[SessionService] Retrieved session data for ${sessionId}`);
return sessionData;
@@ -294,13 +279,16 @@ class SessionService {
logger.info('[SessionService] Starting cleanup of processedGuestIds from sessions');
// Используем один SQL-запрос для обновления всех сессий
const result = await db.getQuery()(
`UPDATE session
SET sess = (sess::jsonb - 'processedGuestIds')::json
WHERE sess::text LIKE '%"processedGuestIds"%'`
);
const result = await encryptedDb.getData('session', { sess: { $regex: '.*"processedGuestIds":' } });
logger.info(`[SessionService] Cleaned processedGuestIds from ${result.rowCount} sessions`);
for (const session of result) {
const sessJson = JSON.parse(session.sess);
delete sessJson.processedGuestIds;
session.sess = JSON.stringify(sessJson);
await encryptedDb.saveData('session', session);
}
logger.info(`[SessionService] Cleaned processedGuestIds from ${result.length} sessions`);
return true;
} catch (error) {
logger.error('[SessionService] Error during cleanup:', error);

View File

@@ -12,6 +12,7 @@
const { Telegraf } = require('telegraf');
const logger = require('../utils/logger');
const encryptedDb = require('./encryptedDatabaseService');
const db = require('../db');
const authService = require('./auth-service');
const verificationService = require('./verification-service');
@@ -19,7 +20,7 @@ const crypto = require('crypto');
const identityService = require('./identity-service');
const aiAssistant = require('./ai-assistant');
const { checkAdminRole } = require('./admin-role');
const { broadcastContactsUpdate } = require('../wsHub');
const { broadcastContactsUpdate, broadcastChatMessage } = require('../wsHub');
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
const { ragAnswer, generateLLMResponse } = require('./ragService');
const { isUserBlocked } = require('../utils/userUtils');
@@ -29,17 +30,23 @@ let telegramSettingsCache = null;
async function getTelegramSettings() {
if (telegramSettingsCache) return telegramSettingsCache;
const { rows } = await db.getQuery()('SELECT * FROM telegram_settings ORDER BY id LIMIT 1');
if (!rows.length) throw new Error('Telegram settings not found in DB');
telegramSettingsCache = rows[0];
const settings = await encryptedDb.getData('telegram_settings', {}, 1);
if (!settings.length) throw new Error('Telegram settings not found in DB');
telegramSettingsCache = settings[0];
return telegramSettingsCache;
}
// Создание и настройка бота
async function getBot() {
console.log('[TelegramBot] getBot() called');
if (!botInstance) {
console.log('[TelegramBot] Creating new bot instance...');
const settings = await getTelegramSettings();
console.log('[TelegramBot] Got settings, creating Telegraf instance...');
botInstance = new Telegraf(settings.bot_token);
console.log('[TelegramBot] Telegraf instance created');
// Обработка команды /start
botInstance.command('start', (ctx) => {
@@ -51,49 +58,51 @@ async function getBot() {
const text = ctx.message.text.trim();
// 1. Если команда — пропустить
if (text.startsWith('/')) return;
// Отправляем индикатор печати для улучшения UX
const typingAction = ctx.replyWithChatAction('typing');
// 2. Проверка: это потенциальный код?
const isPotentialCode = (str) => /^[A-Z0-9]{6}$/i.test(str);
if (isPotentialCode(text)) {
await typingAction;
try {
// Получаем код верификации для всех активных кодов с провайдером telegram
const codeResult = await db.getQuery()(
`SELECT * FROM verification_codes
WHERE code = $1
AND provider = 'telegram'
AND used = false
AND expires_at > NOW()`,
[text.toUpperCase()]
);
const codes = await encryptedDb.getData('verification_codes', {
code: text.toUpperCase(),
provider: 'telegram',
used: false
}, 1);
if (codeResult.rows.length === 0) {
if (codes.length === 0) {
ctx.reply('Неверный код подтверждения');
return;
}
const verification = codeResult.rows[0];
const verification = codes[0];
const providerId = verification.provider_id;
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть
let userId;
let userRole = 'user'; // Роль по умолчанию
// Отмечаем код как использованный
await db.getQuery()('UPDATE verification_codes SET used = true WHERE id = $1', [
verification.id,
]);
await encryptedDb.saveData('verification_codes', {
used: true
}, {
id: verification.id
});
logger.info('Starting Telegram auth process for code:', text);
// Проверяем, существует ли уже пользователь с таким Telegram ID
const existingTelegramUser = await db.getQuery()(
`SELECT ui.user_id
FROM user_identities ui
WHERE ui.provider = 'telegram' AND ui.provider_id = $1`,
[ctx.from.id.toString()]
);
const existingTelegramUsers = await encryptedDb.getData('user_identities', {
provider: 'telegram',
provider_id: ctx.from.id.toString()
}, 1);
if (existingTelegramUser.rows.length > 0) {
if (existingTelegramUsers.length > 0) {
// Если пользователь с таким Telegram ID уже существует, используем его
userId = existingTelegramUser.rows[0].user_id;
userId = existingTelegramUsers[0].user_id;
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
} else {
// Если код верификации был связан с существующим пользователем, используем его
@@ -101,12 +110,11 @@ async function getBot() {
// Используем userId из кода верификации
userId = linkedUserId;
// Связываем Telegram с этим пользователем
await db.getQuery()(
`INSERT INTO user_identities
(user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`,
[userId, 'telegram', ctx.from.id.toString()]
);
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: 'telegram',
provider_id: ctx.from.id.toString()
});
logger.info(
`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`
);
@@ -114,12 +122,11 @@ async function getBot() {
// Проверяем, есть ли пользователь, связанный с гостевым идентификатором
let existingUserWithGuestId = null;
if (providerId) {
const guestUserResult = await db.getQuery()(
`SELECT user_id FROM guest_user_mapping WHERE guest_id = $1`,
[providerId]
);
if (guestUserResult.rows.length > 0) {
existingUserWithGuestId = guestUserResult.rows[0].user_id;
const guestUserResult = await encryptedDb.getData('guest_user_mapping', {
guest_id: providerId
}, 1);
if (guestUserResult.length > 0) {
existingUserWithGuestId = guestUserResult[0].user_id;
logger.info(
`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`
);
@@ -129,38 +136,35 @@ async function getBot() {
if (existingUserWithGuestId) {
// Используем существующего пользователя и добавляем ему Telegram идентификатор
userId = existingUserWithGuestId;
await db.getQuery()(
`INSERT INTO user_identities
(user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`,
[userId, 'telegram', ctx.from.id.toString()]
);
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: 'telegram',
provider_id: ctx.from.id.toString()
});
logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`);
} else {
// Создаем нового пользователя, если не нашли существующего
const userResult = await db.getQuery()(
'INSERT INTO users (created_at, role) VALUES (NOW(), $1) RETURNING id',
['user']
);
userId = userResult.rows[0].id;
const userResult = await encryptedDb.saveData('users', {
created_at: new Date(),
role: 'user'
});
userId = userResult.id;
// Связываем Telegram с новым пользователем
await db.getQuery()(
`INSERT INTO user_identities
(user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())`,
[userId, 'telegram', ctx.from.id.toString()]
);
await encryptedDb.saveData('user_identities', {
user_id: userId,
provider: 'telegram',
provider_id: ctx.from.id.toString()
});
// Если был гостевой ID, связываем его с новым пользователем
if (providerId) {
await db.getQuery()(
`INSERT INTO guest_user_mapping
(user_id, guest_id)
VALUES ($1, $2)
ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`,
[userId, providerId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: userId,
guest_id: providerId
}, {
user_id: userId
});
}
logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`);
@@ -180,25 +184,35 @@ async function getBot() {
logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`);
// Опционально: Обновить роль в таблице users
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0 && currentUser.rows[0].role !== userRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [userRole, userId]);
const currentUser = await encryptedDb.getData('users', {
id: userId
}, 1);
if (currentUser.length > 0 && currentUser[0].role !== userRole) {
await encryptedDb.saveData('users', {
role: userRole
}, {
id: userId
});
logger.info(`[TelegramBot] Updated user role in DB to ${userRole}`);
}
} else {
logger.info(`[TelegramBot] No linked wallet found for user ${userId}. Checking current DB role.`);
// Если кошелька нет, берем текущую роль из базы
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0) {
userRole = currentUser.rows[0].role;
const currentUser = await encryptedDb.getData('users', {
id: userId
}, 1);
if (currentUser.length > 0) {
userRole = currentUser[0].role;
}
}
} catch (roleCheckError) {
logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError);
// В случае ошибки берем роль из базы или оставляем 'user'
try {
const currentUser = await db.getQuery()('SELECT role FROM users WHERE id = $1', [userId]);
if (currentUser.rows.length > 0) { userRole = currentUser.rows[0].role; }
const currentUser = await encryptedDb.getData('users', {
id: userId
}, 1);
if (currentUser.length > 0) { userRole = currentUser[0].role; }
} catch (dbError) { /* ignore */ }
}
} else {
@@ -214,37 +228,28 @@ async function getBot() {
try {
// Ищем сессию, где есть userId и она не истекла (проверка expires_at)
// Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько
const sessionResult = await db.getQuery()(
`SELECT sid FROM session
WHERE sess ->> 'userId' = $1
AND expire > NOW()
ORDER BY expire DESC
LIMIT 1`,
[userId?.toString()] // Используем optional chaining и преобразуем в строку
);
const sessionResult = await encryptedDb.getData('session', {
'sess->>userId': userId?.toString()
}, 1, 'expire', 'DESC');
if (sessionResult.rows.length > 0) {
activeSessionId = sessionResult.rows[0].sid;
if (sessionResult.length > 0) {
activeSessionId = sessionResult[0].sid;
logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`);
// Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram
const updateResult = await db.getQuery()(
`UPDATE session
SET sess = (sess::jsonb || $1::jsonb)::json
WHERE sid = $2`,
[
JSON.stringify({
// authenticated: true, // Не перезаписываем, т.к. сессия уже должна быть аутентифицирована
authType: 'telegram', // Обновляем тип аутентификации
telegramId: ctx.from.id.toString(),
telegramUsername: ctx.from.username,
telegramFirstName: ctx.from.first_name,
role: userRole, // Записываем определенную роль
// userId: userId?.toString() // userId уже должен быть в сессии
}),
activeSessionId // Обновляем по найденному session ID
]
);
const updateResult = await encryptedDb.saveData('session', {
sess: JSON.stringify({
// authenticated: true, // Не перезаписываем, т.к. сессия уже должна быть аутентифицирована
authType: 'telegram', // Обновляем тип аутентификации
telegramId: ctx.from.id.toString(),
telegramUsername: ctx.from.username,
telegramFirstName: ctx.from.first_name,
role: userRole, // Записываем определенную роль
// userId: userId?.toString() // userId уже должен быть в сессии
})
}, {
sid: activeSessionId
});
if (updateResult.rowCount > 0) {
logger.info(`[telegramBot] Session ${activeSessionId} updated successfully with Telegram data for user ${userId}`);
@@ -277,31 +282,36 @@ async function getBot() {
}
return;
}
// 3. Всё остальное — чат с ИИ-ассистентом
// 3. Всё остальное — чат с ИИ-ассистентом
try {
const telegramId = ctx.from.id.toString();
// 1. Найти или создать пользователя
const { userId, role } = await identityService.findOrCreateUserWithRole('telegram', telegramId);
if (await isUserBlocked(userId)) {
await ctx.reply('Вы заблокированы. Сообщения не принимаются.');
return;
}
// 1.1 Найти или создать беседу
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
);
let conversationResult = await encryptedDb.getData('conversations', {
user_id: userId
}, 1, 'updated_at', 'DESC', 'created_at', 'DESC');
let conversation;
if (conversationResult.rows.length === 0) {
if (conversationResult.length === 0) {
const title = `Чат с пользователем ${userId}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[userId, title]
);
conversation = newConv.rows[0];
const newConv = await encryptedDb.saveData('conversations', {
user_id: userId,
title: title,
created_at: new Date(),
updated_at: new Date()
});
conversation = newConv;
} else {
conversation = conversationResult.rows[0];
conversation = conversationResult[0];
}
// 2. Сохранять все сообщения с conversation_id
let content = text;
let attachmentMeta = {};
@@ -330,80 +340,171 @@ async function getBot() {
mimeType = ctx.message.video.mime_type || 'video/mp4';
fileSize = ctx.message.video.file_size;
}
// Асинхронная загрузка файлов
if (fileId) {
// Скачиваем файл
const fileLink = await ctx.telegram.getFileLink(fileId);
const res = await fetch(fileLink.href);
attachmentBuffer = await res.buffer();
attachmentMeta = {
attachment_filename: fileName,
attachment_mimetype: mimeType,
attachment_size: fileSize,
attachment_data: attachmentBuffer
};
try {
const fileLink = await ctx.telegram.getFileLink(fileId);
const res = await fetch(fileLink.href);
attachmentBuffer = await res.buffer();
attachmentMeta = {
attachment_filename: fileName,
attachment_mimetype: mimeType,
attachment_size: fileSize,
attachment_data: attachmentBuffer
};
} catch (fileError) {
logger.error('[TelegramBot] Error downloading file:', fileError);
// Продолжаем без файла
}
}
// Сохраняем сообщение в БД
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10, $11)`,
[userId, conversation.id, 'user', content, 'telegram', role, 'in',
attachmentMeta.attachment_filename || null,
attachmentMeta.attachment_mimetype || null,
attachmentMeta.attachment_size || null,
attachmentMeta.attachment_data || null
]
);
if (!conversation || !conversation.id) {
logger.error(`[TelegramBot] Conversation is undefined or has no id for user ${userId}`);
await ctx.reply('Произошла ошибка при создании диалога. Попробуйте позже.');
return;
}
const userMessage = await encryptedDb.saveData('messages', {
user_id: userId,
conversation_id: conversation.id,
sender_type: 'user',
content: content,
channel: 'telegram',
role: role,
direction: 'in',
created_at: new Date(),
attachment_filename: attachmentMeta.attachment_filename || null,
attachment_mimetype: attachmentMeta.attachment_mimetype || null,
attachment_size: attachmentMeta.attachment_size || null,
attachment_data: attachmentMeta.attachment_data || null
});
// Отправляем WebSocket уведомление о пользовательском сообщении
try {
const decryptedUserMessage = await encryptedDb.getData('messages', { id: userMessage.id }, 1);
if (decryptedUserMessage && decryptedUserMessage[0]) {
broadcastChatMessage(decryptedUserMessage[0], userId);
}
} catch (wsError) {
logger.error('[TelegramBot] WebSocket notification error for user message:', wsError);
}
if (await isUserBlocked(userId)) {
logger.info(`[TelegramBot] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`);
return;
}
// 3. Получить ответ от ИИ (RAG + LLM)
const aiSettings = await aiAssistantSettingsService.getSettings();
let ragTableId = null;
if (aiSettings && aiSettings.selected_rag_tables) {
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
? aiSettings.selected_rag_tables[0]
: aiSettings.selected_rag_tables;
}
let aiResponse;
if (ragTableId) {
// Сначала ищем ответ через RAG
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
aiResponse = ragResult.answer;
} else {
aiResponse = await generateLLMResponse({
userQuestion: content,
context: ragResult && ragResult.context ? ragResult.context : '',
answer: ragResult && ragResult.answer ? ragResult.answer : '',
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
history: null,
model: aiSettings ? aiSettings.model : undefined,
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
});
// 3. Получить ответ от ИИ (RAG + LLM) - асинхронно
const aiResponsePromise = (async () => {
const aiSettings = await aiAssistantSettingsService.getSettings();
let ragTableId = null;
if (aiSettings && aiSettings.selected_rag_tables) {
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
? aiSettings.selected_rag_tables[0]
: aiSettings.selected_rag_tables;
}
} else {
aiResponse = await aiAssistant.getResponse(content, 'auto');
}
// Загружаем историю сообщений для контекста (ограничиваем до 5 сообщений)
let history = null;
try {
const recentMessages = await encryptedDb.getData('messages', {
conversation_id: conversation.id
}, 5, 'created_at DESC');
if (recentMessages && recentMessages.length > 0) {
// Преобразуем сообщения в формат для AI
history = recentMessages.reverse().map(msg => ({
role: msg.sender_type === 'user' ? 'user' : 'assistant',
content: msg.content || '' // content уже расшифрован encryptedDb
}));
}
} catch (historyError) {
logger.error('[TelegramBot] Error loading message history:', historyError);
}
let aiResponse;
if (ragTableId) {
// Сначала ищем ответ через RAG
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.3) {
aiResponse = ragResult.answer;
} else {
aiResponse = await generateLLMResponse({
userQuestion: content,
context: ragResult && ragResult.context ? ragResult.context : '',
answer: ragResult && ragResult.answer ? ragResult.answer : '',
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
history: history,
model: aiSettings ? aiSettings.model : undefined,
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
});
}
} else {
// Используем системный промпт из настроек, если RAG не используется
const systemPrompt = aiSettings ? aiSettings.system_prompt : '';
aiResponse = await aiAssistant.getResponse(content, 'auto', history, systemPrompt);
}
return aiResponse;
})();
// Ждем ответ от ИИ с таймаутом
const aiResponse = await Promise.race([
aiResponsePromise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('AI response timeout')), 60000)
)
]);
// 4. Сохранить ответ в БД с conversation_id
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`,
[userId, conversation.id, 'assistant', aiResponse, 'telegram', role, 'out']
);
const aiMessage = await encryptedDb.saveData('messages', {
user_id: userId,
conversation_id: conversation.id,
sender_type: 'assistant',
content: aiResponse,
channel: 'telegram',
role: role,
direction: 'out',
created_at: new Date()
});
// 5. Отправить ответ пользователю
await ctx.reply(aiResponse);
// 6. Отправить WebSocket уведомление
try {
const decryptedAiMessage = await encryptedDb.getData('messages', { id: aiMessage.id }, 1);
if (decryptedAiMessage && decryptedAiMessage[0]) {
broadcastChatMessage(decryptedAiMessage[0], userId);
}
} catch (wsError) {
logger.error('[TelegramBot] WebSocket notification error:', wsError);
}
} catch (error) {
logger.error('[TelegramBot] Ошибка при обработке сообщения:', error);
await ctx.reply('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.');
}
});
// Запуск бота
await botInstance.launch();
logger.info('[TelegramBot] Бот запущен');
// Запуск бота с таймаутом
console.log('[TelegramBot] Before botInstance.launch()');
try {
// Запускаем бота с таймаутом
const launchPromise = botInstance.launch();
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Telegram bot launch timeout')), 10000); // 10 секунд таймаут
});
await Promise.race([launchPromise, timeoutPromise]);
console.log('[TelegramBot] After botInstance.launch()');
logger.info('[TelegramBot] Бот запущен');
} catch (error) {
console.error('[TelegramBot] Error launching bot:', error);
// Не выбрасываем ошибку, чтобы не блокировать запуск сервера
console.log('[TelegramBot] Bot launch failed, but continuing...');
}
}
return botInstance;
@@ -436,12 +537,12 @@ async function initTelegramAuth(session) {
const guestId = session.guestId || tempId;
// Связываем гостевой ID с текущим пользователем
await db.getQuery()(
`INSERT INTO guest_user_mapping (user_id, guest_id)
VALUES ($1, $2)
ON CONFLICT (guest_id) DO UPDATE SET user_id = $1`,
[session.userId, guestId]
);
await encryptedDb.saveData('guest_user_mapping', {
user_id: session.userId,
guest_id: guestId
}, {
user_id: session.userId
});
logger.info(
`[initTelegramAuth] Linked guestId ${guestId} to authenticated user ${session.userId}`
@@ -475,8 +576,8 @@ function clearSettingsCache() {
}
async function getAllBots() {
const { rows } = await db.getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
return rows;
const settings = await encryptedDb.getData('telegram_settings', {}, 1, 'id');
return settings;
}
module.exports = {

View File

@@ -10,26 +10,25 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
async function deleteUserById(userId) {
console.log('[DELETE] Вызван deleteUserById для userId:', userId);
const query = db.getQuery();
try {
await query('BEGIN');
console.log('[DELETE] Начинаем удаление user_identities для userId:', userId);
const resIdentities = await query('DELETE FROM user_identities WHERE user_id = $1', [userId]);
console.log('[DELETE] Удалено user_identities:', resIdentities.rowCount);
const resIdentities = await encryptedDb.deleteData('user_identities', { user_id: userId });
console.log('[DELETE] Удалено user_identities:', resIdentities.length);
console.log('[DELETE] Начинаем удаление messages для userId:', userId);
const resMessages = await query('DELETE FROM messages WHERE user_id = $1', [userId]);
console.log('[DELETE] Удалено messages:', resMessages.rowCount);
const resMessages = await encryptedDb.deleteData('messages', { user_id: userId });
console.log('[DELETE] Удалено messages:', resMessages.length);
console.log('[DELETE] Начинаем удаление пользователя из users:', userId);
const result = await query('DELETE FROM users WHERE id = $1 RETURNING *', [userId]);
console.log('[DELETE] Результат удаления пользователя:', result.rowCount, result.rows);
await query('COMMIT');
return result.rowCount;
const result = await encryptedDb.deleteData('users', { id: userId });
console.log('[DELETE] Результат удаления пользователя:', result.length, result);
return result.length;
} catch (e) {
await query('ROLLBACK');
console.error('[DELETE] Ошибка при удалении пользователя:', e);
throw e;
}

View File

@@ -10,7 +10,7 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const logger = require('../utils/logger');
class VerificationService {
@@ -39,23 +39,21 @@ class VerificationService {
`Creating verification code for ${provider}:${providerId}, userId: ${userId || 'null'}`
);
// Если userId не указан, добавляем запись без ссылки на пользователя
if (userId === null || userId === undefined) {
await db.getQuery()(
`INSERT INTO verification_codes
(code, provider, provider_id, expires_at)
VALUES ($1, $2, $3, $4)`,
[code, provider, providerId, expiresAt]
);
} else {
await db.getQuery()(
`INSERT INTO verification_codes
(code, provider, provider_id, user_id, expires_at)
VALUES ($1, $2, $3, $4, $5)`,
[code, provider, providerId, userId, expiresAt]
);
const data = {
code: code,
provider: provider,
provider_id: providerId,
expires_at: expiresAt,
used: false
};
// Если userId указан, добавляем его
if (userId !== null && userId !== undefined) {
data.user_id = userId;
}
await encryptedDb.saveData('verification_codes', data);
logger.info(`Verification code created successfully for ${provider}:${providerId}`);
return code;
} catch (error) {
@@ -79,53 +77,52 @@ class VerificationService {
logger.info(`Normalized code: ${normalizedCode}`);
// Проверим, есть ли такой код в базе (для отладки)
const checkResult = await db.getQuery()(
`SELECT code FROM verification_codes
WHERE provider = $1
AND provider_id = $2
AND used = false
AND expires_at > NOW()`,
[provider, providerId]
);
const checkResult = await encryptedDb.getData('verification_codes', {
provider: provider,
provider_id: providerId,
used: false
});
if (checkResult.rows.length > 0) {
if (checkResult.length > 0) {
logger.info(
`Found codes for ${provider}:${providerId}: ${JSON.stringify(checkResult.rows.map((r) => r.code))}`
`Found codes for ${provider}:${providerId}: ${JSON.stringify(checkResult.map((r) => r.code))}`
);
} else {
logger.warn(`No active codes found for ${provider}:${providerId}`);
}
const result = await db.getQuery()(
`SELECT * FROM verification_codes
WHERE code = $1
AND provider = $2
AND provider_id = $3
AND used = false
AND expires_at > NOW()`,
[normalizedCode, provider, providerId]
);
const result = await encryptedDb.getData('verification_codes', {
code: normalizedCode,
provider: provider,
provider_id: providerId,
used: false
}, 1);
if (result.rows.length === 0) {
logger.warn(
`Invalid or expired code for ${provider}:${providerId}. Input: ${normalizedCode}`
);
return { success: false, error: 'Неверный или истекший код' };
if (result.length === 0) {
logger.warn(`No valid verification code found for ${provider}:${providerId}`);
return { valid: false, message: 'Invalid or expired code' };
}
const verification = result.rows[0];
const verificationCode = result[0];
// Проверяем срок действия
if (new Date(verificationCode.expires_at) < new Date()) {
logger.warn(`Verification code expired for ${provider}:${providerId}`);
return { valid: false, message: 'Code has expired' };
}
// Отмечаем код как использованный
await db.getQuery()(
'UPDATE verification_codes SET used = true WHERE id = $1',
[verification.id]
);
await encryptedDb.saveData('verification_codes', {
used: true
}, {
id: verificationCode.id
});
logger.info(`Code verified successfully for ${provider}:${providerId}`);
logger.info(`Verification code verified successfully for ${provider}:${providerId}`);
return {
success: true,
userId: verification.user_id,
providerId: verification.provider_id,
valid: true,
userId: verificationCode.user_id,
message: 'Code verified successfully'
};
} catch (error) {
logger.error('Error verifying code:', {
@@ -141,15 +138,19 @@ class VerificationService {
// Очистка истекших кодов
async cleanupExpiredCodes() {
try {
const result = await db.getQuery()(
'DELETE FROM verification_codes WHERE expires_at <= NOW() RETURNING id'
);
logger.info(`Cleaned up ${result.rowCount} expired verification codes`);
const expiredCodes = await encryptedDb.getData('verification_codes', {
expires_at: { $lt: new Date() }
});
for (const code of expiredCodes) {
await encryptedDb.deleteData('verification_codes', { id: code.id });
}
logger.info(`Cleaned up ${expiredCodes.length} expired verification codes`);
} catch (error) {
logger.error('Error cleaning up expired codes:', error);
}
}
}
const verificationService = new VerificationService();
module.exports = verificationService;
module.exports = new VerificationService();

View File

@@ -10,21 +10,19 @@
* GitHub: https://github.com/HB3-ACCELERATOR
*/
const db = require('../db');
const encryptedDb = require('./encryptedDatabaseService');
const logger = require('../utils/logger');
// Получение связанного кошелька
async function getLinkedWallet(userId) {
logger.info(`[getLinkedWallet] Called with userId: ${userId} (Type: ${typeof userId})`);
try {
const result = await db.getQuery()(
`SELECT provider_id as address
FROM user_identities
WHERE user_id = $1 AND provider = 'wallet'`,
[userId]
);
logger.info(`[getLinkedWallet] DB query result for userId ${userId}:`, result.rows);
const address = result.rows[0]?.address;
const result = await encryptedDb.getData('user_identities', {
user_id: userId,
provider: 'wallet'
}, 1);
logger.info(`[getLinkedWallet] DB query result for userId ${userId}:`, result);
const address = result[0]?.provider_id;
logger.info(`[getLinkedWallet] Returning address: ${address} for userId ${userId}`);
return address;
} catch (error) {

View File

@@ -33,19 +33,47 @@ function generateVerificationCode(length = 6) {
// Проверка существования идентификатора пользователя
async function checkUserIdentity(userId, provider, providerId) {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
const result = await db.getQuery()(
'SELECT * FROM user_identities WHERE user_id = $1 AND provider = $2 AND provider_id = $3',
[userId, provider, providerId]
'SELECT * FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $4) AND provider_id_encrypted = encrypt_text($3, $4)',
[userId, provider, providerId, encryptionKey]
);
return result.rows.length > 0;
}
// Добавление новой идентификации
async function addUserIdentity(userId, provider, providerId) {
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
let encryptionKey = 'default-key';
try {
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
if (fs.existsSync(keyPath)) {
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
}
} catch (keyError) {
console.error('Error reading encryption key:', keyError);
}
try {
await db.getQuery()(
'INSERT INTO user_identities (user_id, provider, provider_id) VALUES ($1, $2, $3)',
[userId, provider, providerId]
'INSERT INTO user_identities (user_id, provider_encrypted, provider_id_encrypted) VALUES ($1, encrypt_text($2, $4), encrypt_text($3, $4))',
[userId, provider, providerId, encryptionKey]
);
return true;
} catch (error) {

View File

@@ -13,38 +13,203 @@
const WebSocket = require('ws');
let wss = null;
const wsClients = new Set();
// Храним клиентов по userId для персонализированных уведомлений
const wsClients = new Map(); // userId -> Set of WebSocket connections
function initWSS(server) {
wss = new WebSocket.Server({ server, path: '/ws' });
wss.on('connection', (ws) => {
wsClients.add(ws);
ws.on('close', () => wsClients.delete(ws));
wss.on('connection', (ws, req) => {
console.log('🔌 [WebSocket] Новое подключение');
// Добавляем клиента в общий список
if (!wsClients.has('anonymous')) {
wsClients.set('anonymous', new Set());
}
wsClients.get('anonymous').add(ws);
// Обработка сообщений от клиента
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
console.log('📨 [WebSocket] Получено сообщение:', data);
if (data.type === 'auth' && data.userId) {
// Аутентификация пользователя
authenticateUser(ws, data.userId);
}
} catch (error) {
console.error('❌ [WebSocket] Ошибка парсинга сообщения:', error);
}
});
ws.on('close', () => {
console.log('🔌 [WebSocket] Соединение закрыто');
// Удаляем клиента из всех списков
for (const [userId, clients] of wsClients.entries()) {
clients.delete(ws);
if (clients.size === 0) {
wsClients.delete(userId);
}
}
});
ws.on('error', (error) => {
console.error('❌ [WebSocket] Ошибка соединения:', error);
});
});
console.log('🚀 [WebSocket] Сервер запущен на /ws');
}
function authenticateUser(ws, userId) {
console.log(`🔐 [WebSocket] Аутентификация пользователя ${userId}`);
// Удаляем из анонимных
if (wsClients.has('anonymous')) {
wsClients.get('anonymous').delete(ws);
}
// Добавляем в список пользователя
if (!wsClients.has(userId.toString())) {
wsClients.set(userId.toString(), new Set());
}
wsClients.get(userId.toString()).add(ws);
// Отправляем подтверждение
ws.send(JSON.stringify({
type: 'auth-success',
userId: userId
}));
}
function broadcastContactsUpdate() {
for (const ws of wsClients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'contacts-updated' }));
console.log('📢 [WebSocket] Отправка обновления контактов всем клиентам');
for (const [userId, clients] of wsClients.entries()) {
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'contacts-updated' }));
}
}
}
}
function broadcastMessagesUpdate() {
for (const ws of wsClients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'messages-updated' }));
console.log('📢 [WebSocket] Отправка обновления сообщений всем клиентам');
for (const [userId, clients] of wsClients.entries()) {
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'messages-updated' }));
}
}
}
}
function broadcastChatMessage(message) {
for (const ws of wsClients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'chat-message', message }));
function broadcastChatMessage(message, targetUserId = null) {
console.log(`📢 [WebSocket] Отправка сообщения чата`, {
messageId: message.id,
targetUserId
});
if (targetUserId) {
// Отправляем конкретному пользователю
const userClients = wsClients.get(targetUserId.toString());
if (userClients) {
for (const ws of userClients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'chat-message',
message
}));
}
}
}
} else {
// Отправляем всем
for (const [userId, clients] of wsClients.entries()) {
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'chat-message',
message
}));
}
}
}
}
}
module.exports = { initWSS, broadcastContactsUpdate, broadcastMessagesUpdate, broadcastChatMessage };
function broadcastConversationUpdate(conversationId, targetUserId = null) {
console.log(`📢 [WebSocket] Отправка обновления диалога`, {
conversationId,
targetUserId
});
const payload = {
type: 'conversation-updated',
conversationId
};
if (targetUserId) {
// Отправляем конкретному пользователю
const userClients = wsClients.get(targetUserId.toString());
if (userClients) {
for (const ws of userClients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
}
} else {
// Отправляем всем
for (const [userId, clients] of wsClients.entries()) {
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(payload));
}
}
}
}
}
function getConnectedUsers() {
const users = [];
for (const [userId, clients] of wsClients.entries()) {
if (userId !== 'anonymous' && clients.size > 0) {
users.push({
userId: parseInt(userId),
connections: clients.size
});
}
}
return users;
}
function getStats() {
let totalConnections = 0;
let anonymousConnections = 0;
for (const [userId, clients] of wsClients.entries()) {
if (userId === 'anonymous') {
anonymousConnections = clients.size;
}
totalConnections += clients.size;
}
return {
totalConnections,
anonymousConnections,
authenticatedUsers: getConnectedUsers(),
totalUsers: wsClients.size - (wsClients.has('anonymous') ? 1 : 0)
};
}
module.exports = {
initWSS,
broadcastContactsUpdate,
broadcastMessagesUpdate,
broadcastChatMessage,
broadcastConversationUpdate,
getConnectedUsers,
getStats
};

View File

@@ -1688,6 +1688,20 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
better-queue-memory@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/better-queue-memory/-/better-queue-memory-1.0.4.tgz#f390d6b30bb3b36aaf2ce52b37a483e8a7a81a22"
integrity sha512-SWg5wFIShYffEmJpI6LgbL8/3Dqhku7xI1oEiy6FroP9DbcZlG0ZDjxvPdP9t7hTGW40IpIcC6zVoGT1oxjOuA==
better-queue@^3.8.12:
version "3.8.12"
resolved "https://registry.yarnpkg.com/better-queue/-/better-queue-3.8.12.tgz#15c18923d0f9778be94f19c3ef2bd85c632d0db3"
integrity sha512-D9KZ+Us+2AyaCz693/9AyjTg0s8hEmkiM/MB3i09cs4MdK1KgTSGJluXRYmOulR69oLZVo2XDFtqsExDt8oiLA==
dependencies:
better-queue-memory "^1.0.1"
node-eta "^0.9.0"
uuid "^9.0.0"
bignumber.js@^9.0.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.0.tgz#bdba7e2a4c1a2eba08290e8dcad4f36393c92acd"
@@ -4912,6 +4926,11 @@ node-emoji@^1.10.0:
dependencies:
lodash "^4.17.21"
node-eta@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-eta/-/node-eta-0.9.0.tgz#9fb0b099bcd2a021940e603c64254dc003d9a7a8"
integrity sha512-mTCTZk29tmX1OGfVkPt63H3c3VqXrI2Kvua98S7iUIB/Gbp0MNw05YtUomxQIxnnKMyRIIuY9izPcFixzhSBrA==
node-fetch@^2.6.7, node-fetch@^2.6.9, node-fetch@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
@@ -6871,7 +6890,7 @@ uuid@^10.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
uuid@^9.0.1:
uuid@^9.0.0, uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==

38
backup-database.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Скрипт для создания резервных копий базы данных DLE
# Запускать: ./backup-database.sh
# Настройки
BACKUP_DIR="./backups"
DB_NAME="${DB_NAME:-dapp_db}"
DB_USER="${DB_USER:-dapp_user}"
DB_PASSWORD="${DB_PASSWORD:-dapp_password}"
CONTAINER_NAME="dapp-postgres"
# Создаём папку для бэкапов если её нет
mkdir -p "$BACKUP_DIR"
# Имя файла бэкапа с датой и временем
BACKUP_FILE="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql"
echo "🔒 Создание резервной копии базы данных..."
echo "📁 Файл: $BACKUP_FILE"
# Создаём бэкап
docker exec "$CONTAINER_NAME" pg_dump -U "$DB_USER" "$DB_NAME" > "$BACKUP_FILE"
if [ $? -eq 0 ]; then
echo "✅ Бэкап успешно создан!"
echo "📊 Размер файла: $(du -h "$BACKUP_FILE" | cut -f1)"
# Удаляем старые бэкапы (оставляем только последние 10)
echo "🧹 Удаление старых бэкапов..."
ls -t "$BACKUP_DIR"/backup_*.sql | tail -n +11 | xargs -r rm
echo "📋 Последние бэкапы:"
ls -lh "$BACKUP_DIR"/backup_*.sql | tail -5
else
echo "❌ Ошибка при создании бэкапа!"
exit 1
fi

Binary file not shown.

73
decrypt-all-tables.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Скрипт для расшифровки всех таблиц
# Использование: ./decrypt-all-tables.sh
ENCRYPTION_KEY=$(cat ./ssl/keys/full_db_encryption.key)
echo "🔓 Расшифровка всех таблиц..."
# Получаем список всех таблиц
TABLES=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
ORDER BY table_name;")
# Функция для расшифровки таблицы
decrypt_table() {
local table_name="$1"
echo "🔓 Расшифровка таблицы: $table_name"
# Получаем зашифрованные колонки
local encrypted_columns=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT column_name
FROM information_schema.columns
WHERE table_name = '$table_name'
AND table_schema = 'public'
AND column_name LIKE '%_encrypted'
ORDER BY ordinal_position;")
if [ -z "$encrypted_columns" ]; then
echo " ⏭️ Нет зашифрованных колонок"
return
fi
echo " 📝 Зашифрованные колонки:"
echo "$encrypted_columns" | while read -r column_name; do
if [ -n "$column_name" ]; then
echo " $column_name"
# Определяем тип колонки
data_type=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT data_type FROM information_schema.columns
WHERE table_name = '$table_name' AND column_name = '$column_name' AND table_schema = 'public';" | xargs)
# Определяем первичный ключ для таблицы
primary_key=$(docker exec dapp-postgres psql -U dapp_user -d dapp_db -t -c "
SELECT column_name FROM information_schema.key_column_usage
WHERE table_name = '$table_name' AND constraint_name LIKE '%_pkey'
AND table_schema = 'public' LIMIT 1;" | xargs)
if [ "$data_type" = "jsonb" ] || [ "$data_type" = "json" ]; then
# Расшифровываем json/jsonb
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
SELECT $primary_key, decrypt_json($column_name, '$ENCRYPTION_KEY') as ${column_name%_encrypted}_decrypted
FROM $table_name WHERE $column_name IS NOT NULL LIMIT 5;"
else
# Расшифровываем текстовые
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
SELECT $primary_key, decrypt_text($column_name, '$ENCRYPTION_KEY') as ${column_name%_encrypted}_decrypted
FROM $table_name WHERE $column_name IS NOT NULL LIMIT 5;"
fi
fi
done
}
# Расшифровываем каждую таблицу
echo "$TABLES" | while read -r table_name; do
if [ -n "$table_name" ]; then
decrypt_table "$table_name"
fi
done
echo "✅ Расшифровка завершена!"

View File

@@ -20,8 +20,8 @@ services:
POSTGRES_DB: ${DB_NAME:-dapp_db}
POSTGRES_USER: ${DB_USER:-dapp_user}
POSTGRES_PASSWORD: ${DB_PASSWORD:-dapp_password}
ports:
- '5432:5432'
# ports:
# - '5432:5432' # Закрываем доступ к базе данных извне
healthcheck:
test:
- CMD-SHELL
@@ -40,9 +40,26 @@ services:
max-file: "3"
volumes:
- ollama_data:/root/.ollama
ports:
- '11434:11434'
# ports:
# - '11434:11434' # Закрываем - используется только backend'ом
command: serve
deploy:
resources:
limits:
cpus: '3.5'
memory: 12G
reservations:
cpus: '2.0'
memory: 6G
environment:
- OLLAMA_HOST=0.0.0.0
- OLLAMA_ORIGINS=*
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
vector-search:
build:
context: ./vector-search
@@ -63,8 +80,8 @@ services:
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- OLLAMA_EMBED_MODEL=${OLLAMA_EMBEDDINGS_MODEL:-mxbai-embed-large:latest}
ports:
- '8001:8001'
# ports:
# - '8001:8001' # Закрываем - используется только backend'ом
backend:
build:
context: ./backend
@@ -86,6 +103,7 @@ services:
volumes:
- ./backend:/app
- ./frontend/dist:/app/frontend_dist:ro
- ./ssl:/app/ssl:ro
- /var/run/docker.sock:/var/run/docker.sock
environment:
- NODE_ENV=${NODE_ENV:-development}
@@ -123,7 +141,7 @@ services:
- ./frontend:/app
- frontend_node_modules:/app/node_modules
ports:
- '5173:5173'
- '5173:5173' # Закрываем - используем nginx
command: yarn run dev -- --host 0.0.0.0
ollama-setup:
image: curlimages/curl:latest
@@ -181,6 +199,29 @@ services:
depends_on:
- backend
# Автоматический бэкап базы данных
backup-service:
image: postgres:16-alpine
container_name: dapp-backup-service
restart: unless-stopped
volumes:
- ./backup-database.sh:/backup.sh:ro
- ./backups:/backups
- postgres_data:/var/lib/postgresql/data:ro
environment:
- PGPASSWORD=${DB_PASSWORD:-dapp_password}
depends_on:
- postgres
command: >
sh -c "
echo 'Backup service started'
while true; do
sleep 86400
echo 'Starting daily backup...'
/backup.sh
done
"
volumes:
postgres_data:
ollama_data:

View File

@@ -0,0 +1,693 @@
# Детальный план задач: Управление Digital Legal Entity (DLE)
## Общие принципы разработки
### Архитектурные требования
- **Single-Chain Governance**: Голосование происходит только в одной выбранной сети
- **Мультиподпись токен-холдеров**: Все операции требуют кворума подписей
- **Настраиваемые таймлоки**: Инициатор устанавливает задержку для каждого предложения
- **Cross-chain исполнение**: Решения выполняются во всех целевых сетях
- **Без админских ролей**: Только коллективное управление через токен-холдеров
### Технический стек
- **Frontend**: Vue.js 3 + Composition API
- **Web3**: ethers.js или web3.js
- **Контракты**: Solidity + OpenZeppelin + ERC-4337
- **Стили**: Scoped CSS с переменными
---
## 1. БЛОК "ПРЕДЛОЖЕНИЯ" (`/management/proposals`)
### Задача 1.1: Создание предложений
**Описание**: Пользователь создает предложение для выполнения операции в DLE
**Что нужно сделать**:
- [ ] Добавить форму создания предложения с полями:
- Тип операции (управление токенами, казна, модули, настройки)
- Описание операции (текстовое поле)
- Выбор governance-сети (выпадающий список)
- Выбор целевых сетей для исполнения (мультиселект)
- Таймлок задержка в часах (числовое поле)
- [ ] Подключить к контракту метод `createProposal(operation, targetChains, timelockDelay)`
- [ ] Добавить валидацию: только токен-холдеры могут создавать предложения
- [ ] Показывать предварительный расчет газа для создания предложения
**Время**: 8-12 часов
### Задача 1.2: Подписание предложений
**Описание**: Токен-холдеры подписывают предложения для достижения кворума
**Что нужно сделать**:
- [ ] Отображать список активных предложений с прогрессом подписей
- [ ] Показывать для каждого предложения:
- Текущее количество подписей / требуемый кворум
- Список подписавших с их балансами токенов
- Время до истечения таймлока
- [ ] Добавить кнопку "Подписать" для токен-холдеров
- [ ] Подключить к контракту метод `signProposal(proposalId)`
- [ ] Проверять баланс токенов при подписании
- [ ] Обновлять UI в реальном времени при новых подписях
**Время**: 10-14 часов
### Задача 1.3: Исполнение предложений
**Описание**: Выполнение предложений после достижения кворума и истечения таймлока
**Что нужно сделать**:
- [ ] Отображать статус предложений: "Ожидание", "Кворум достигнут", "Готово к исполнению", "Исполнено"
- [ ] Добавить кнопку "Исполнить" для предложений готовых к исполнению
- [ ] Подключить к контракту метод `executeProposal(proposalId)`
- [ ] Показывать прогресс исполнения в целевых сетях
- [ ] Обрабатывать ошибки исполнения и показывать fallback статусы
- [ ] Добавить возможность отмены предложений до истечения таймлока
**Время**: 8-12 часов
### Задача 1.4: История и фильтрация
**Описание**: Просмотр истории предложений с возможностью фильтрации
**Что нужно сделать**:
- [ ] Добавить фильтры по:
- Статусу предложения
- Типу операции
- Governance-сети
- Периоду времени
- [ ] Реализовать поиск по описанию предложения
- [ ] Добавить пагинацию для больших списков
- [ ] Показывать детали каждого предложения в модальном окне
- [ ] Добавить ссылки на блокчейн-эксплореры для транзакций
**Время**: 6-8 часов
---
## 2. БЛОК "ТОКЕНЫ DLE" (`/management/tokens`)
### Задача 2.1: Информация о токенах
**Описание**: Отображение основной информации о токенах DLE
**Что нужно сделать**:
- [ ] Показывать общий supply токенов
- [ ] Отображать баланс текущего пользователя
- [ ] Показывать процент кворума для голосования
- [ ] Отображать текущую рыночную стоимость токена (если доступна)
- [ ] Подключить к контракту методы `totalSupply()`, `balanceOf(address)`
- [ ] Обновлять данные в реальном времени при изменениях
**Время**: 4-6 часов
### Задача 2.2: Передача токенов
**Описание**: Перевод токенов между участниками через кворум
**Что нужно сделать**:
- [ ] Создать форму перевода с полями:
- Адрес получателя
- Количество токенов
- Описание перевода
- [ ] Создавать предложение для перевода токенов
- [ ] Показывать статус предложения перевода
- [ ] Валидировать баланс отправителя
- [ ] Подключать к контракту через систему предложений
**Время**: 6-8 часов
### Задача 2.3: Распределение токенов
**Описание**: Массовое распределение токенов новым участникам
**Что нужно сделать**:
- [ ] Создать форму распределения с возможностью добавления множественных получателей
- [ ] Поля для каждого получателя: адрес, количество токенов
- [ ] Кнопки "Добавить получателя" и "Удалить получателя"
- [ ] Создавать предложение для распределения
- [ ] Показывать общую сумму распределения
- [ ] Валидировать общий supply и права на минтинг
**Время**: 8-10 часов
### Задача 2.4: Список держателей токенов
**Описание**: Отображение всех держателей токенов с их балансами
**Что нужно сделать**:
- [ ] Получать список всех держателей токенов
- [ ] Отображать для каждого держателя:
- Адрес кошелька
- Количество токенов
- Процент от общего supply
- Дата последней активности
- [ ] Добавить сортировку по балансу, активности, адресу
- [ ] Реализовать поиск по адресу
- [ ] Показывать топ-10 держателей отдельно
**Время**: 6-8 часов
---
## 3. БЛОК "КВОРУМ" (`/management/quorum`)
### Задача 3.1: Текущие настройки кворума
**Описание**: Отображение текущих параметров кворума
**Что нужно сделать**:
- [ ] Показывать текущий процент кворума для голосования
- [ ] Отображать минимальную задержку голосования
- [ ] Показывать период голосования
- [ ] Отображать порог для создания предложений
- [ ] Подключать к контракту для получения актуальных значений
- [ ] Показывать когда были изменены последние настройки
**Время**: 4-6 часов
### Задача 3.2: Изменение настроек кворума
**Описание**: Создание предложения для изменения параметров кворума
**Что нужно сделать**:
- [ ] Создать форму изменения настроек с полями:
- Новый процент кворума (1-100%)
- Новая задержка голосования (в часах)
- Новый период голосования (в днях)
- Новый порог для предложений (в токенах)
- Причина изменения
- [ ] Валидировать значения (кворум не менее 51%, разумные периоды)
- [ ] Создавать предложение для изменения настроек
- [ ] Показывать предварительный расчет влияния изменений
- [ ] Добавить подтверждение критических изменений
**Время**: 8-10 часов
### Задача 3.3: История изменений кворума
**Описание**: Просмотр истории изменений параметров кворума
**Что нужно сделать**:
- [ ] Отображать список всех изменений кворума
- [ ] Показывать для каждого изменения:
- Старые и новые значения
- Кто инициировал изменение
- Когда было предложено и когда принято
- Причину изменения
- [ ] Добавить фильтрацию по периоду и типу изменений
- [ ] Показывать статистику изменений (частота, тренды)
**Время**: 6-8 часов
---
## 4. БЛОК "МОДУЛИ DLE" (`/management/modules`)
### Задача 4.1: Список установленных модулей
**Описание**: Отображение всех установленных модулей DLE
**Что нужно сделать**:
- [ ] Получать список всех модулей из контракта
- [ ] Отображать для каждого модуля:
- Название и описание
- Адрес контракта модуля
- Версию модуля
- Статус (активен/неактивен)
- Дата установки
- [ ] Показывать общую статистику модулей
- [ ] Добавить поиск и фильтрацию по модулям
**Время**: 6-8 часов
### Задача 4.2: Установка новых модулей
**Описание**: Добавление новых модулей через голосование
**Что нужно сделать**:
- [ ] Создать форму установки модуля с полями:
- Название модуля
- Адрес контракта модуля
- Описание функциональности
- Параметры инициализации
- Причина установки
- [ ] Валидировать адрес контракта и совместимость
- [ ] Создавать предложение для установки модуля
- [ ] Показывать предварительную проверку модуля
- [ ] Добавить возможность тестирования модуля перед установкой
**Время**: 10-12 часов
### Задача 4.3: Управление модулями
**Описание**: Активация, деактивация и удаление модулей
**Что нужно сделать**:
- [ ] Добавить кнопки управления для каждого модуля:
- Активировать/деактивировать
- Обновить версию
- Удалить модуль
- [ ] Создавать предложения для каждого действия
- [ ] Показывать зависимости между модулями
- [ ] Предупреждать о критических операциях
- [ ] Добавить возможность отката изменений
**Время**: 8-10 часов
### Задача 4.4: Интерфейсы модулей
**Описание**: Встраивание интерфейсов управления модулями
**Что нужно сделать**:
- [ ] Создать систему встраивания интерфейсов модулей
- [ ] Каждый модуль может предоставлять свой UI
- [ ] Безопасное взаимодействие между основным приложением и модулями
- [ ] Единообразный стиль для всех модулей
- [ ] Добавить документацию для разработчиков модулей
**Время**: 12-16 часов
---
## 5. БЛОК "DLE" (Интеграция с другими DLE) (`/management/dle`)
### Задача 5.1: Список подключенных DLE
**Описание**: Отображение DLE, с которыми установлена интеграция
**Что нужно сделать**:
- [ ] Показывать карточки всех подключенных DLE
- [ ] Для каждого DLE отображать:
- Название и описание
- Адрес контракта
- Количество токенов этого DLE в нашем балансе
- Процент голосов, который мы можем использовать
- Статус подключения
- [ ] Добавить возможность удаления DLE из списка
- [ ] Показывать общую статистику интеграций
**Время**: 6-8 часов
### Задача 5.2: Добавление новых DLE
**Описание**: Подключение новых DLE для участия в их голосованиях
**Что нужно сделать**:
- [ ] Создать форму добавления DLE с полями:
- Название DLE
- Адрес контракта DLE
- Описание и назначение
- Причина подключения
- [ ] Валидировать адрес и проверять существование контракта
- [ ] Проверять баланс токенов этого DLE
- [ ] Создавать предложение для добавления DLE
- [ ] Показывать предварительную информацию о DLE
**Время**: 8-10 часов
### Задача 5.3: Участие в голосованиях других DLE
**Описание**: Голосование в предложениях других DLE через наш кворум
**Что нужно сделать**:
- [ ] Отображать активные предложения других DLE
- [ ] Показывать для каждого предложения:
- Описание и тип операции
- Наш возможный вес голоса
- Текущий статус голосования
- Время до истечения
- [ ] Создавать внутренние предложения для голосования в других DLE
- [ ] Собирать кворум подписей для внешнего голосования
- [ ] Отправлять голос в целевое DLE после достижения кворума
**Время**: 12-16 часов
### Задача 5.4: Встраивание интерфейсов управления
**Описание**: Управление другими DLE через встроенные интерфейсы
**Что нужно сделать**:
- [ ] Создать систему встраивания интерфейсов других DLE
- [ ] Безопасное отображение внешних интерфейсов в iframe
- [ ] Передача контекста аутентификации
- [ ] Обработка событий и обновлений
- [ ] Единообразный стиль и навигация
- [ ] Добавить возможность открытия в новой вкладке
**Время**: 16-20 часов
---
## 6. БЛОК "КАЗНА" (`/management/treasury`)
### Задача 6.1: Баланс казны
**Описание**: Отображение всех активов казны DLE
**Что нужно сделать**:
- [ ] Показывать балансы всех токенов в казне:
- Нативные токены (ETH, MATIC, BNB и т.д.)
- ERC-20 токены
- NFT и другие активы
- [ ] Отображать общую стоимость в USD
- [ ] Показывать историю изменений баланса
- [ ] Добавить графики движения средств
- [ ] Подключать к контракту для получения актуальных данных
**Время**: 8-10 часов
### Задача 6.2: Операции с казной
**Описание**: Выполнение финансовых операций через кворум
**Что нужно сделать**:
- [ ] Создать формы для различных операций:
- Отправка средств (нативные токены, ERC-20)
- Получение средств
- Обмен токенов
- Инвестиции в DeFi протоколы
- [ ] Создавать предложения для каждой операции
- [ ] Валидировать балансы и права доступа
- [ ] Показывать предварительный расчет газа и комиссий
- [ ] Добавить подтверждение критических операций
**Время**: 12-16 часов
### Задача 6.3: Распределение дивидендов
**Описание**: Выплата дивидендов токен-холдерам
**Что нужно сделать**:
- [ ] Создать форму распределения дивидендов:
- Выбор токена для выплаты
- Общая сумма дивидендов
- Пропорциональное распределение по балансам
- [ ] Показывать предварительный расчет выплат каждому держателю
- [ ] Создавать предложение для выплаты дивидендов
- [ ] Отслеживать статус выплат
- [ ] Показывать историю дивидендных выплат
**Время**: 10-12 часов
### Задача 6.4: Бюджетирование и отчеты
**Описание**: Планирование и контроль расходов казны
**Что нужно сделать**:
- [ ] Создать систему бюджетирования:
- Установка лимитов расходов
- Категоризация операций
- Планирование расходов
- [ ] Генерировать отчеты:
- Месячные/квартальные отчеты
- Анализ доходов и расходов
- Прогнозы движения средств
- [ ] Добавить уведомления о превышении лимитов
- [ ] Экспорт отчетов в PDF/CSV
**Время**: 12-16 часов
---
## 7. БЛОК "АНАЛИТИКА" (`/management/analytics`)
### Задача 7.1: Ключевые метрики
**Описание**: Отображение основных показателей DLE
**Что нужно сделать**:
- [ ] Показывать ключевые метрики:
- Общая стоимость активов (TVL)
- Количество активных участников
- Количество предложений за период
- Средняя доходность
- [ ] Добавить сравнение с предыдущими периодами
- [ ] Показывать тренды и изменения
- [ ] Подключать реальные данные из контрактов
**Время**: 8-10 часов
### Задача 7.2: Графики и визуализация
**Описание**: Создание интерактивных графиков и диаграмм
**Что нужно сделать**:
- [ ] Интегрировать библиотеку для графиков (Chart.js, D3.js)
- [ ] Создать графики:
- Стоимость токенов во времени
- Активность участников по дням/неделям
- Распределение токенов между держателями
- Движение средств в казне
- [ ] Добавить интерактивность (зум, фильтры, детализация)
- [ ] Реализовать экспорт графиков
**Время**: 12-16 часов
### Задача 7.3: Статистика и отчеты
**Описание**: Детальная аналитика и отчетность
**Что нужно сделать**:
- [ ] Создать разделы статистики:
- Топ держателей токенов
- Самые активные участники
- История предложений и голосований
- Финансовая статистика
- [ ] Добавить фильтры по периодам
- [ ] Создать настраиваемые дашборды
- [ ] Реализовать автоматическую генерацию отчетов
**Время**: 10-14 часов
### Задача 7.4: Прогнозирование и тренды
**Описание**: Анализ трендов и прогнозирование
**Что нужно сделать**:
- [ ] Анализировать исторические данные для выявления трендов
- [ ] Создавать прогнозы:
- Движение стоимости токенов
- Активность участников
- Финансовые показатели
- [ ] Показывать аномалии и важные события
- [ ] Добавить уведомления о значимых изменениях
**Время**: 14-18 часов
---
## 8. БЛОК "ИСТОРИЯ" (`/management/history`)
### Задача 8.1: Лог всех операций
**Описание**: Отображение полной истории операций DLE
**Что нужно сделать**:
- [ ] Собирать и отображать все типы операций:
- Создание и исполнение предложений
- Операции с токенами
- Казначейские операции
- Изменения настроек
- Модульные операции
- [ ] Для каждой операции показывать:
- Тип и описание
- Время выполнения
- Участники
- Статус выполнения
- Ссылки на транзакции
**Время**: 10-12 часов
### Задача 8.2: Фильтрация и поиск
**Описание**: Удобный поиск и фильтрация истории
**Что нужно сделать**:
- [ ] Добавить фильтры по:
- Типу операции
- Периоду времени
- Участникам
- Статусу
- Сетям
- [ ] Реализовать полнотекстовый поиск
- [ ] Добавить сохранение фильтров
- [ ] Создать быстрые фильтры (сегодня, неделя, месяц)
**Время**: 8-10 часов
### Задача 8.3: Детали операций
**Описание**: Подробная информация о каждой операции
**Что нужно сделать**:
- [ ] Создать модальные окна с деталями операций
- [ ] Показывать полную информацию:
- Параметры операции
- Участники и их роли
- Результат выполнения
- Ошибки и предупреждения
- [ ] Добавить ссылки на блокчейн-эксплореры
- [ ] Показывать связанные операции
**Время**: 8-10 часов
### Задача 8.4: Экспорт и отчеты
**Описание**: Экспорт истории в различные форматы
**Что нужно сделать**:
- [ ] Реализовать экспорт в форматы:
- CSV для анализа в Excel
- PDF для отчетов
- JSON для интеграции
- [ ] Добавить настройки экспорта (период, типы операций)
- [ ] Создать шаблоны отчетов
- [ ] Добавить автоматическую отправку отчетов
**Время**: 6-8 часов
---
## 9. БЛОК "НАСТРОЙКИ" (`/management/settings`)
### Задача 9.1: Основные настройки DLE
**Описание**: Управление основной информацией о DLE
**Что нужно сделать**:
- [ ] Создать форму основных настроек:
- Название DLE
- Символ токена
- Описание и назначение
- Местонахождение
- Веб-сайт
- [ ] Подключать к контракту для сохранения изменений
- [ ] Валидировать данные перед сохранением
- [ ] Показывать историю изменений
**Время**: 6-8 часов
### Задача 9.2: Настройки безопасности
**Описание**: Управление параметрами безопасности
**Что нужно сделать**:
- [ ] Создать форму настроек безопасности:
- Минимальный кворум для голосования
- Максимальная длительность предложений
- Порог для экстренных действий
- Задержка таймлока по умолчанию
- Дополнительные настройки (делегирование, KYC)
- [ ] Добавить предупреждения о критических изменениях
- [ ] Создавать предложения для изменения настроек
- [ ] Показывать влияние изменений на безопасность
**Время**: 10-12 часов
### Задача 9.3: Настройки сети
**Описание**: Управление сетевыми параметрами
**Что нужно сделать**:
- [ ] Создать форму настроек сети:
- Выбор поддерживаемых сетей
- Сеть по умолчанию для governance
- RPC endpoints для каждой сети
- Настройки синхронизации
- [ ] Тестировать подключение к сетям
- [ ] Показывать статус каждой сети
- [ ] Добавить возможность добавления новых сетей
**Время**: 8-10 часов
### Задача 9.4: Резервное копирование
**Описание**: Экспорт и импорт настроек
**Что нужно сделать**:
- [ ] Реализовать экспорт всех настроек в JSON
- [ ] Создать импорт настроек из файла
- [ ] Валидировать импортируемые данные
- [ ] Добавить предварительный просмотр изменений
- [ ] Создать автоматическое резервное копирование
**Время**: 6-8 часов
### Задача 9.5: Опасная зона
**Описание**: Критические операции с DLE
**Что нужно сделать**:
- [ ] Создать раздел опасных операций:
- Сброс настроек к значениям по умолчанию
- Полное удаление DLE
- Экстренная остановка операций
- [ ] Добавить множественные подтверждения
- [ ] Показывать последствия каждой операции
- [ ] Создать логирование критических действий
**Время**: 8-10 часов
---
## ОБЩИЕ ЗАДАЧИ ДЛЯ ВСЕХ БЛОКОВ
### Задача 0.1: Подготовка инфраструктуры
**Описание**: Настройка базовой инфраструктуры для всех блоков
**Что нужно сделать**:
- [ ] Настроить web3-провайдеры для всех поддерживаемых сетей
- [ ] Создать абстракции для работы с контрактами
- [ ] Настроить обработку ошибок и уведомления
- [ ] Создать систему кэширования данных
- [ ] Настроить мониторинг состояния сетей
**Время**: 16-20 часов
### Задача 0.2: Интеграция с контрактами
**Описание**: Подключение всех блоков к смарт-контрактам
**Что нужно сделать**:
- [ ] Создать интерфейсы для всех методов контрактов
- [ ] Реализовать обработку событий контрактов
- [ ] Настроить подписки на изменения состояния
- [ ] Добавить обработку ошибок транзакций
- [ ] Реализовать retry механизмы для неудачных транзакций
**Время**: 20-24 часа
### Задача 0.3: Тестирование и отладка
**Описание**: Комплексное тестирование всех функций
**Что нужно сделать**:
- [ ] Создать unit-тесты для всех компонентов
- [ ] Написать e2e тесты для основных сценариев
- [ ] Протестировать работу в разных сетях
- [ ] Провести нагрузочное тестирование
- [ ] Исправить найденные ошибки
**Время**: 24-32 часа
### Задача 0.4: Оптимизация и производительность
**Описание**: Улучшение производительности и пользовательского опыта
**Что нужно сделать**:
- [ ] Оптимизировать загрузку данных
- [ ] Добавить lazy loading для больших списков
- [ ] Реализовать кэширование на клиенте
- [ ] Оптимизировать размер бандла
- [ ] Добавить прогресс-индикаторы для длительных операций
**Время**: 16-20 часов
---
## ПРИОРИТЕТЫ РАЗРАБОТКИ
### Высокий приоритет (MVP)
1. Предложения - базовая функциональность
2. Токены DLE - основные операции
3. Кворум - текущие настройки
4. Настройки - основные параметры
### Средний приоритет
1. Казна - базовые операции
2. История - лог операций
3. Аналитика - ключевые метрики
4. Модули - список и управление
### Низкий приоритет
1. DLE - интеграция с другими DLE
2. Расширенная аналитика
3. Сложные отчеты
4. Встраивание интерфейсов
---
## ОЦЕНКА ВРЕМЕНИ
### Общее время разработки: 280-380 часов
**Разбивка по блокам:**
- Предложения: 32-46 часов
- Токены DLE: 24-32 часа
- Кворум: 18-24 часа
- Модули DLE: 36-46 часов
- DLE (интеграция): 42-54 часа
- Казна: 42-54 часа
- Аналитика: 44-58 часа
- История: 32-40 часов
- Настройки: 38-48 часов
- Общие задачи: 76-96 часов
**Рекомендации:**
- Начать с MVP (высокий приоритет)
- Разрабатывать параллельно несколько блоков
- Использовать готовые компоненты где возможно
- Регулярно тестировать интеграцию с контрактами

240
docs/ENCRYPTION_GUIDE.md Normal file
View File

@@ -0,0 +1,240 @@
# 🔐 Полное шифрование всех таблиц в DLE
## 📋 Обзор
Этот подход шифрует **ВСЕ текстовые данные** во **ВСЕХ таблицах** базы данных.
## 🎯 Что шифруется
### **✅ ВСЕ текстовые колонки во ВСЕХ таблицах:**
- `text` - текстовые поля
- `varchar` - строки переменной длины
- `character varying` - строки переменной длины
- `json` - JSON данные
- `jsonb` - бинарные JSON данные
### **❌ НЕ шифруются:**
- `id` - идентификаторы
- `created_at`, `updated_at` - временные метки
- `integer`, `numeric`, `boolean` - числовые и логические типы
- Колонки, уже содержащие `_encrypted` в названии
## 🚀 Пошаговая инструкция
### **Шаг 1: Запуск полного шифрования**
```bash
chmod +x encrypt-all-tables.sh
./encrypt-all-tables.sh
```
### **Шаг 2: Проверка шифрования**
```bash
./decrypt-all-tables.sh
```
### **Шаг 3: Обновление кода приложения**
#### **A. Использование универсального сервиса**
```javascript
const encryptedDataService = require('./services/encryptedDataService');
// Получение данных с автоматической расшифровкой
const users = await encryptedDataService.getData('users', { role: 'admin' });
// Сохранение данных с автоматическим шифрованием
const newUser = await encryptedDataService.saveData('users', {
name: 'Иван Иванов',
email: 'ivan@example.com',
preferences: { theme: 'dark' }
});
// Обновление данных
const updatedUser = await encryptedDataService.saveData('users',
{ name: 'Иван Петров' },
{ id: 1 }
);
// Удаление данных
await encryptedDataService.deleteData('users', { id: 1 });
```
#### **B. Проверка статуса шифрования**
```javascript
const status = await encryptedDataService.getEncryptionStatus();
console.log('Статус шифрования:', status);
// {
// hasEncryptionKey: true,
// encryptedTables: [
// { table_name: 'users', encrypted_columns: '3' },
// { table_name: 'messages', encrypted_columns: '2' }
// ],
// totalEncryptedColumns: 15
// }
```
### **Шаг 4: Обновление существующих роутов**
#### **Пример обновления роута пользователей:**
```javascript
// Было:
router.get('/users', async (req, res) => {
const { rows } = await db.getQuery()('SELECT * FROM users');
res.json(rows);
});
// Стало:
router.get('/users', async (req, res) => {
try {
const users = await encryptedDataService.getData('users');
res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
```
#### **Пример обновления роута сообщений:**
```javascript
// Было:
router.post('/messages', async (req, res) => {
const { content, user_id } = req.body;
const { rows } = await db.getQuery()(
'INSERT INTO messages (content, user_id) VALUES ($1, $2) RETURNING *',
[content, user_id]
);
res.json(rows[0]);
});
// Стало:
router.post('/messages', async (req, res) => {
try {
const { content, user_id } = req.body;
const message = await encryptedDataService.saveData('messages', {
content,
user_id
});
res.json(message);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
```
### **Шаг 5: Тестирование**
```bash
# Проверить работу зашифрованных данных
curl -X GET http://localhost:8000/api/users
curl -X POST http://localhost:8000/api/messages -H "Content-Type: application/json" -d '{"content":"Тестовое сообщение","user_id":1}'
# Проверить расшифровку
./decrypt-all-tables.sh
```
### **Шаг 6: Удаление незашифрованных колонок**
```bash
# ВНИМАНИЕ: Это необратимая операция!
./remove-unencrypted-columns.sh
```
## 🔑 Управление ключами
### **Ключ шифрования**
- **Файл**: `./ssl/keys/full_db_encryption.key`
- **Размер**: 32 байта (base64)
- **Алгоритм**: AES-256-CBC
### **Безопасность ключа**
```bash
# Права доступа
chmod 600 ./ssl/keys/full_db_encryption.key
# Резервная копия
cp ./ssl/keys/full_db_encryption.key ./ssl/keys/full_db_encryption.key.backup
# Проверка целостности
sha256sum ./ssl/keys/full_db_encryption.key
```
## 🛡️ Дополнительные меры безопасности
### **1. Шифрование томов Docker**
```yaml
# docker-compose.yml
volumes:
postgres_data:
driver: local
driver_opts:
type: none
o: bind
device: /path/to/encrypted/storage
```
### **2. SSL/TLS для PostgreSQL**
```yaml
# docker-compose.yml
services:
postgres:
command: >
postgres
-c ssl=on
-c ssl_cert_file=/etc/ssl/certs/server.crt
-c ssl_key_file=/etc/ssl/certs/server.key
```
### **3. Шифрование переменных окружения**
```bash
# Для оставшихся переменных окружения
./encrypt-env.sh
```
## 🔍 Мониторинг и аудит
### **Логирование доступа к зашифрованным данным**
```javascript
// В сервисе добавить логирование
console.log(`🔐 Доступ к зашифрованным данным: ${tableName} в ${new Date().toISOString()}`);
```
### **Проверка целостности**
```bash
# Скрипт для проверки целостности зашифрованных данных
./verify-encryption.sh
```
## ⚠️ Важные замечания
### **1. Производительность**
- Шифрование/расшифровка добавляет задержку
- Используйте кэширование для часто используемых данных
- Рассмотрите индексы для зашифрованных колонок
### **2. Резервное копирование**
- **Обязательно** делайте бэкап ключа шифрования
- **Обязательно** делайте бэкап базы данных
- Храните ключ отдельно от данных
### **3. Восстановление**
```bash
# Восстановление из бэкапа
docker exec dapp-postgres psql -U dapp_user -d dapp_db < backup.sql
# Восстановление ключа
cp ./ssl/keys/full_db_encryption.key.backup ./ssl/keys/full_db_encryption.key
```
### **4. Совместимость**
- Приложение работает с зашифрованными и незашифрованными данными
- Fallback на незашифрованные данные при отсутствии ключа
- Постепенная миграция существующих данных
## 🎯 Результат
После применения полного шифрования:
-ВСЕ текстовые данные зашифрованы в БД
- ✅ Ключ шифрования хранится отдельно
- ✅ Приложение работает с зашифрованными данными
- ✅ Fallback на незашифрованные данные при отсутствии ключа
- ✅ Универсальный сервис для работы с данными
- ✅ Возможность ротации ключей
**Максимальная безопасность данных достигнута!** 🔒

200
docs/MIGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,200 @@
# 🔄 Перенос зашифрованных данных между серверами
## 📋 Обзор
При переносе зашифрованных данных важно передать **и данные, и ключ шифрования** вместе.
## 🎯 Способы переноса
### **1. 🔑 Перенос с ключом шифрования (Рекомендуемый)**
#### **Создание миграционного пакета:**
```bash
chmod +x migrate-encrypted-data.sh
./migrate-encrypted-data.sh
```
#### **Что включается в пакет:**
- ✅ Бэкап базы данных (зашифрованные данные)
- ✅ Ключ шифрования (`full_db_encryption.key`)
- ✅ Скрипты шифрования/расшифровки
- ✅ Сервис для работы с зашифрованными данными
### **2. 🌐 Перенос через SSH**
```bash
# На исходном сервере
./migrate-encrypted-data.sh
# Выберите опцию 2 - SSH
# Автоматически создастся архив и отправится на целевой сервер
```
### **3. ☁️ Перенос через облачное хранилище**
```bash
# На исходном сервере
./migrate-encrypted-data.sh
# Выберите опцию 3 - Облачное хранилище
# Архив загрузится в S3/другое облачное хранилище
# Скачайте на целевой сервер и восстановите
```
### **4. 💾 Перенос через локальный носитель**
```bash
# На исходном сервере
./migrate-encrypted-data.sh
# Выберите опцию 4 - Локальный носитель
# Скопируйте архив на USB/SSD/другой носитель
# Перенесите на целевой сервер
```
## 🚀 Пошаговая инструкция
### **Этап 1: Подготовка исходного сервера**
#### **A. Создание миграционного пакета:**
```bash
# Создаём полный бэкап с ключом
./migrate-encrypted-data.sh
# Выберите опцию 1 - "Создать бэкап с ключом"
# Результат: migration_package_YYYYMMDD_HHMMSS.tar.gz
```
#### **B. Проверка содержимого:**
```bash
# Просмотр содержимого архива
tar -tzf migration_package_*.tar.gz
# Должно содержать:
# - encrypted_backup_*.sql (бэкап БД)
# - ssl/keys/full_db_encryption.key (ключ шифрования)
# - encrypt-all-tables.sh (скрипт шифрования)
# - decrypt-all-tables.sh (скрипт расшифровки)
# - backend/services/encryptedDataService.js (сервис)
```
### **Этап 2: Перенос на целевой сервер**
#### **A. Копирование архива:**
```bash
# Способ 1: SCP
scp migration_package_*.tar.gz user@target-server:/tmp/
# Способ 2: USB/локальный носитель
# Скопируйте файл на носитель и перенесите физически
# Способ 3: Облачное хранилище
# Скачайте архив из S3/другого хранилища
```
#### **B. Восстановление на целевом сервере:**
```bash
# 1. Распаковка архива
tar -xzf migration_package_*.tar.gz -C /path/to/your/app/
# 2. Восстановление ключа шифрования
chmod 600 ssl/keys/full_db_encryption.key
# 3. Восстановление базы данных
docker exec dapp-postgres psql -U dapp_user dapp_db < encrypted_backup_*.sql
# 4. Проверка целостности
./migrate-encrypted-data.sh
# Выберите опцию 5 - "Проверить целостность"
```
### **Этап 3: Проверка работоспособности**
#### **A. Проверка подключения к БД:**
```bash
# Проверяем подключение
docker exec dapp-postgres pg_isready -U dapp_user -d dapp_db
# Проверяем зашифрованные данные
docker exec dapp-postgres psql -U dapp_user -d dapp_db -c "
SELECT
table_name,
COUNT(*) as encrypted_columns
FROM information_schema.columns
WHERE table_schema = 'public'
AND column_name LIKE '%_encrypted'
GROUP BY table_name;"
```
#### **B. Тестирование приложения:**
```bash
# Запускаем приложение
docker-compose up -d
# Проверяем API
curl -X GET http://localhost:8000/api/health
curl -X GET http://localhost:8000/api/users
```
## 🔐 Безопасность при переносе
### **1. Защита ключа шифрования:**
```bash
# Правильные права доступа
chmod 600 ssl/keys/full_db_encryption.key
# Резервная копия ключа
cp ssl/keys/full_db_encryption.key ssl/keys/full_db_encryption.key.backup
# Проверка целостности
sha256sum ssl/keys/full_db_encryption.key
```
### **2. Безопасная передача:**
- Используйте SSH для передачи
- Шифруйте архив дополнительно
- Используйте защищённые каналы связи
### **3. Очистка после переноса:**
```bash
# Удаляем временные файлы
rm -rf migration_backups/
rm -f /tmp/migration_package_*.tar.gz
# Очищаем историю команд
history -c
```
## ⚠️ Важные замечания
### **1. Совместимость версий:**
- Убедитесь, что версии PostgreSQL одинаковые
- Проверьте совместимость расширений (pgcrypto)
- Убедитесь в совместимости Docker образов
### **2. Размер данных:**
- Зашифрованные данные занимают больше места
- Учитывайте размер при планировании переноса
- Используйте сжатие для больших баз данных
### **3. Время простоя:**
- Миграция может занять время
- Планируйте время простоя
- Используйте репликацию для минимизации простоя
### **4. Восстановление:**
```bash
# В случае проблем с миграцией
# Восстановите из резервной копии
docker exec dapp-postgres psql -U dapp_user -d dapp_db < backup.sql
# Восстановите ключ
cp ssl/keys/full_db_encryption.key.backup ssl/keys/full_db_encryption.key
```
## 🎯 Результат
После успешной миграции:
-Все данные перенесены с шифрованием
- ✅ Ключ шифрования восстановлен
- ✅ Приложение работает на новом сервере
- ✅ Безопасность данных сохранена
**Миграция зашифрованных данных завершена успешно!** 🔒

409
docs/SMART_CONTRACTS.md Normal file
View File

@@ -0,0 +1,409 @@
# Смарт Контракты Digital Legal Entity (DLE)
## Основной смарт контракт DLE
### Концепция
Адрес смарт контракта одновременно выполняет функции банковского счета и контактных данных (как email/телефонный номер).
### Требования
#### 1. Токен управления
- Пользователь заполняет форму в приложении для ручного деплоя
- Токен дает права голоса держателям
токен передается только через кворум мультиподписей
#### 2. Казначейские функции
- Управление финансами через голосование токен-холдеров
- Мультиподпись токен-холдеров для выполнения транзакций (проверка баланса)
- Функции банковского счета
- НЕТ админских ролей - все через коллективное голосование
#### 3. Система голосования с мультиподписью
- Голосование за деплой дополнительных смарт контрактов через мультиподпись
- Кворум подписей токен-холдеров для принятия решений (проверка баланса)
- Токен-холдеры управляют всеми операциями через систему подписей
- Настраиваемые таймлоки для каждого предложения
- НЕТ админских ролей - только коллективное управление
#### 4. Система настраиваемых таймлоков (отдельный модуль)
- **Архитектура**: Отдельный контракт TimelockController, создаваемый при деплое DLE
- **Настройки при деплое**:
- Минимальная задержка таймлока (настраиваемая)
- Максимальная задержка таймлока (настраиваемая)
- Задержка по умолчанию (настраиваемая)
- Возможность настройки индивидуальной задержки для каждого предложения
- **Функции модуля**:
- Инициатор предложения устанавливает индивидуальную задержку
- Динамическое изменение параметров таймлока через голосование
- Отмена предложений до истечения таймлока
- Выполнение предложений после истечения таймлока
- **Пример параметров**: 1 день задержки, 7 дней голосования, 2 дня timelock
#### 5. Модульная система
- Настройка отдельных модулей с формами в приложении
- Деплой дополнительных смарт контрактов через голосование
- Расширяемость функционала
#### 6. Коммуникационные функции
- Прием сообщений от криптокошельков и смарт контрактов
- Прием звонков (аудио/видео) от владельцев кошельков
- Адрес контракта = универсальный контакт (как email/телефон)
- Кворум мультиподписей токен-холдеров для приема звонков и отправки сообщений
- НЕТ админских ролей - все через коллективное голосование
#### 7. Функции акционерного общества
- Права голоса пропорционально токенам
- Управление через коллективные решения токен-холдеров
- Прозрачность всех операций
- НЕТ единичных администраторов - только коллективное управление через кворум подписей
### Иерархическая система голосования DLE
#### Концепция
DLE может владеть токенами других DLE и участвовать в их голосовании через систему кворума подписей.
#### Механизм работы
1. **DLE A** владеет токенами **DLE B**
2. **Голос DLE A** в **DLE B** прямо пропорционален количеству токенов **DLE B** на балансе **DLE A**
3. Для участия в голосовании **DLE B** холдеры **DLE A** должны собрать **кворум мультиподписей** внутри **DLE A**
4. После достижения кворума подписей **DLE A** может голосовать в **DLE B** как единое целое
#### Пример
- **DLE A** владеет **10% токенов DLE B**
- Кворум в **DLE B** = **51%**
- Холдеры **DLE A** голосуют за подпись в **DLE B**
- **DLE B** получает от **DLE A** подпись на **10% голосов**
#### Технические требования
- Система сбора мультиподписей внутри DLE для внешнего голосования
- Проверка кворума подписей перед активацией голоса DLE
- Прямо пропорциональный подсчет голосов по количеству токенов
- Интерфейсы для взаимодействия между DLE
### Межприложное взаимодействие DLE
#### Концепция
Каждое DLE имеет свое веб3 приложение с интерфейсом управления. DLE могут взаимодействовать через встраивание интерфейсов.
#### Архитектура взаимодействия
- **DLE A** (домен 1) + **Веб3 приложение A** с интерфейсом
- **DLE B** (домен 2) + **Веб3 приложение B** с интерфейсом
- **Голосование** происходит через блокчейн между доменами
#### Вариант реализации (рекомендуемый)
**Встраивание интерфейса DLE B в приложение DLE A**
##### Преимущества:
- **Безопасность**: Холдеры DLE A не покидают свое приложение
- **Защита от фишинга**: Пользователи всегда в знакомой среде
- **Контроль**: DLE A контролирует безопасность интерфейса
- **Удобство**: Единый интерфейс для управления всеми DLE
- **Аудит**: Все действия отслеживаются в одном месте
##### Техническая реализация:
```javascript
// В приложении DLE A
function DLEBManagementInterface({ dleBAddress }) {
return (
<div className="dle-b-management">
<h3>Управление DLE B</h3>
<VotingInterface targetDLE={dleBAddress} />
<ProposalInterface targetDLE={dleBAddress} />
<TreasuryInterface targetDLE={dleBAddress} />
</div>
);
}
```
##### Пример интерфейса:
- URL: `http://localhost:5173/dle-management`
- Встраивание компонентов управления DLE B
- Безопасное подписание транзакций для DLE B
- Проверка прав через мультиподпись
### Технические требования
- Один адрес = универсальная точка входа
- Безопасность мультиподписи
- Масштабируемость через модули
- Поддержка аудио/видео коммуникации
- Совместимость с существующими стандартами (ERC-20, ERC-721)
- Иерархическая система голосования между DLE
- Межприложное взаимодействие через встраивание интерфейсов
## Мульти-чейн архитектура DLE
### Концепция
DLE должен функционировать в нескольких блокчейн-сетях с одинаковым адресом для обеспечения максимального удобства и универсальности.
### Требования
#### 1. CREATE2 детерминистический деплой
- Использование CREATE2 opcode для предсказуемых адресов
- Одинаковый адрес смарт-контракта во всех EVM-совместимых сетях
- Factory контракт с одинаковым адресом во всех целевых сетях
- Детерминистический salt на основе пользовательских данных
#### 2. Синхронные токены управления
- Одинаковое количество токенов для каждого партнера во всех сетях
- Синхронизация операций с токенами между всеми развернутыми сетями
- Все операции с токенами только через мультиподпись и кворум
- Защита от double-spending и рассинхронизации
#### 3. Cross-chain система голосования
- Возможность выбора сети для инициации голосования
- Кворум рассчитывается по токенам в выбранной сети
- Результаты голосования синхронизируются во все сети
- Выполнение решений может происходить в любой из развернутых сетей
#### 4. Cross-chain синхронизация операций
- Функция связывания операций между сетями
- Атомарное выполнение операций во всех целевых сетях
- Система откатов при сбоях в одной из сетей
- Таймауты и fallback механизмы
#### 3. Single-Chain Governance система
- Инициатор предложения выбирает ОДНУ сеть для голосования
- Все токен-холдеры участвуют в мультиподписи только в выбранной сети
- Инициатор устанавливает таймлок для предложения
- Исполнение решения происходит во всех целевых сетях
#### 4. Упрощенная cross-chain архитектура
- Голосование и кворум ТОЛЬКО в одной governance сети
- Проверка балансов токен-холдеров при подписании
- Настраиваемый таймлок для каждого предложения
- Исполнение во всех выбранных целевых сетях
#### 5. Мульти-сетевой деплой
- Выбор множественных сетей для деплоя из интерфейса
- Автоматический расчет стоимости деплоя по всем сетям
- Поддержка различных EVM-совместимых сетей
- Возможность добавления новых сетей после первоначального деплоя
### Поддерживаемые сети
- деинамическое и только те которые добавлены в таблицу rpc провайдеров
### Архитектура синхронизации
#### Cross-Chain операции
```solidity
contract DLE_CrossChainSync {
struct CrossChainOperation {
uint256[] targetChains; // Целевые сети для выполнения
bytes[] callData; // Данные для выполнения
uint256 executedChains; // Количество выполненных сетей
bool isCompleted; // Статус завершения
uint256 timeout; // Время истечения операции
}
function executeMultiChainOperation(bytes32 operationId, uint256 chainId) external;
function syncTokenOperation(address[] holders, uint256[] amounts, uint256[] chains) external;
}
```
#### Типы синхронизируемых операций
- **Передача токенов** между партнерами
- **Минтинг новых токенов** для новых участников
- **Сжигание токенов** при выходе участников
- **Изменение прав доступа** и ролей
- **Выполнение решений голосования**
### Упрощенная архитектура governance
#### Single-Chain Governance контракт
```solidity
contract DLE_Governance {
struct Proposal {
bytes operation; // Операция для выполнения
uint256[] targetChains; // Целевые сети для исполнения
uint256 timelock; // Время исполнения (timestamp)
uint256 governanceChain; // Сеть где проходит голосование
address initiator; // Инициатор предложения
bytes[] signatures; // Подписи токен-холдеров
bool executed; // Статус исполнения
}
function createProposal(
bytes calldata operation,
uint256[] calldata targetChains,
uint256 timelockDelay
) external onlyTokenHolder returns (uint256 proposalId);
function signProposal(uint256 proposalId) external onlyTokenHolder;
function executeProposal(uint256 proposalId) external;
}
```
#### Типы операций
- **Управление токенами** - перевод, минтинг, сжигание
- **Финансовые операции** - выплата дивидендов, инвестиции
- **Emergency действия** - пауза, разморозка, восстановление
- **Модульные операции** - добавление/удаление функциональности
### Безопасность мульти-чейн операций
#### Требования к безопасности
- Кворум для cross-chain операций не менее 67%
- Подтверждение операций в нескольких блоках (минимум 12)
- Система откатов при сбоях синхронизации
- Мониторинг состояния во всех сетях
#### Механизмы защиты
- **Timelock для критических операций** - задержка выполнения
- **Emergency pause** - остановка операций при обнаружении проблем
- **Fallback сеть** - резервная сеть при сбоях основной
- **Валидация состояния** - проверка консистентности данных
### Безопасность Single-Chain Governance
#### Требования к безопасности
- Участие только верифицированных токен-холдеров
- Проверка балансов токенов на момент подписания
- Настраиваемый кворум подписей (минимум 51%)
- Настраиваемый таймлок для всех операций (кроме emergency)
#### Механизмы защиты
- **Token-holder verification** - только владельцы токенов участвуют
- **Balance verification** - проверка баланса при каждой подписи
- **Flexible timelock** - инициатор устанавливает задержку
- **Governance chain selection** - инициатор устанавливает сеть для голосования
- **Atomic execution** - операция выполняется во всех сетях или не выполняется
- **Fallback mechanisms** - исполнение в доступных сетях при сбоях
### Пользовательский интерфейс
#### Форма мульти-чейн деплоя
- Выбор целевых сетей из выпадающего списка
- Предварительный расчет стоимости деплоя
- Настройки single-chain governance
#### Создание предложений
- Выбор governance сети для голосования
- Установка таймлока инициатором
- Выбор целевых сетей для исполнения
- Описание операции и параметров
#### Участие в голосовании
- Подписание предложений в governance сети
- Проверка баланса токенов при подписи
- Отображение прогресса сбора подписей
- Статус исполнения в целевых сетях
### Технические требования мульти-чейн
- Детерминистический деплой через CREATE2
- Синхронизация состояния между сетями
- Защита от MEV-атак при cross-chain операциях
- Оптимизация газа для массовых операций
- Поддержка различных bridge протоколов
### Технические требования упрощенной архитектуры
- Детерминистический деплой через CREATE2
- Single-chain governance для безопасности
- Token-holder verification при каждой подписи
- Настраиваемые таймлоки для разных типов операций
- Атомарное исполнение во всех целевых сетях
- Fallback механизмы при недоступности сетей
## Технические решения
### ERC-4337 (Account Abstraction) как основа
#### Концепция
ERC-4337 предоставляет стандартную инфраструктуру для смарт-контракт кошельков с универсальностью (один адрес во всех цепочках) и готовыми решениями для оптимизации газа.
#### Компоненты ERC-4337
- **Smart Contract Wallets** - встроенная мультиподпись
- **Bundlers** - оптимизация газа через агрегацию транзакций
- **Paymasters** - гибкая оплата транзакций
- **Account Abstraction** - универсальность и стандартизация
#### Преимущества использования ERC-4337
-**Универсальность** - один адрес во всех блокчейнах
-**Готовые решения** - проверенная экосистема
-**Безопасность** - прошедший аудит стандарт
-**Оптимизация** - встроенная экономия газа
-**Совместимость** - стандартные интерфейсы
### Варианты технической реализации
#### Вариант 1: Собственный контракт + ERC-4337
**Создание собственного DLE контракта с использованием компонентов ERC-4337**
##### Архитектура:
```
Собственный DLE контракт
├── ERC-4337 компоненты (импорт/наследование)
│ ├── Account Abstraction логика
│ ├── Bundler совместимость
│ └── Paymaster поддержка
└── DLE Logic (ваша уникальная логика)
├── Governance Token
├── Single-Chain Voting System
├── Communication
├── Treasury
└── Multi-Chain Execution
```
### Лицензия ERC-4337
ERC-4337 распространяется под лицензией **CC0** (Public Domain), что означает полную свободу использования.
## Готовые компоненты с аудитом
### 1. **OpenZeppelin** (аудит: ConsenSys Diligence)
-**ERC-20** - токены управления
-**Governance** - система голосования
-**Access Control** - роли и разрешения
-**Multisig** - мультиподпись
-**Timelock** - задержки выполнения
### 2. **ERC-4337** (аудит: Trail of Bits)
-**Account Abstraction** - универсальность
-**Smart Contract Wallets** - кошельки
-**Bundlers** - оптимизация газа
### 3. **Проверенные паттерны**
-**Diamond Pattern** (EIP-2535) - модульность
-**Proxy Pattern** - обновляемость
-**Factory Pattern** - создание контрактов
## Архитектура безопасного контракта DLE
### **Сборка в один безопасный контракт:**
```solidity
contract DLE is ERC20, Governor, TimelockController {
// Single-chain governance логика
// + готовые компоненты с аудитом
// + проверенные паттерны
}
```
### **Компоненты для интеграции:**
- **ERC-20** - токен управления DLE
- **Governor** - система голосования с мультиподписью
- **TimelockController** - настраиваемые таймлоки
- **Account Abstraction** - универсальность адреса
## Преимущества упрощенного подхода:
### ✅ **Безопасность**
- Все компоненты протестированы и проаудированы
- Устранены риски cross-chain мостов
- Single-chain governance снижает сложность
### ✅ **Эффективность**
- Использование готовых решений OpenZeppelin
- Быстрая разработка с проверенными компонентами
- Меньше ошибок благодаря упрощенной архитектуре
### ✅ **Надежность**
- Временем проверенные решения
- Простая логика мультиподписи токен-холдеров
- Понятные механизмы таймлоков
### ✅ **Совместимость**
- Стандартные интерфейсы Ethereum
- Совместимость с существующими кошельками
- Легкая интеграция с DeFi протоколами

View File

@@ -0,0 +1,429 @@
# Техническое задание: Digital Legal Entity (DLE)
## 1. Общие сведения
### 1.1 Назначение системы
Создание смарт-контракта DLE (Digital Legal Entity) - универсальной цифровой юридической сущности, которая объединяет функции акционерного общества, банковского счета и контактных данных в одном адресе блокчейна.
### 1.2 Цель разработки
Разработка безопасного, масштабируемого и функционального смарт-контракта для токенизации акционерных обществ с поддержкой иерархического управления и коммуникационных функций.
### 1.3 Технический подход
Использование готовых проаудированных компонентов (OpenZeppelin, ERC-4337) с добавлением уникальной бизнес-логики DLE.
## 2. Функциональные требования
### 2.1 Основные функции DLE
#### 2.1.1 Токен управления
- **Описание**: ERC-20 токен для управления DLE
- **Функции**:
- Минтинг токенов при создании DLE
- Распределение токенов между участниками
- Делегирование голосов
- Проверка баланса токенов
#### 2.1.2 Система голосования с мультиподписью
- **Описание**: Система принятия решений через кворум подписей токен-холдеров
- **Функции**:
- Создание предложений (любым токен-холдером)
- Сбор подписей от токен-холдеров (проверка баланса токенов)
- Проверка кворума подписей (% от общего количества токенов)
- Выполнение предложений после достижения кворума
- Динамический выбор governance сети при создании предложения
- Динамическая настройка таймлока для каждого предложения
- **Принцип**: НЕТ админских ролей - ВСЕ управление через токен-холдеров
#### 2.1.3 Настраиваемые таймлоки
- **Описание**: Система задержек для каждого предложения через отдельный модуль TimelockController
- **Архитектура**: Отдельный контракт TimelockController, создаваемый при деплое DLE
- **Параметры**:
- Минимальная задержка таймлока (настраиваемая при деплое)
- Максимальная задержка таймлока (настраиваемая при деплое)
- Задержка по умолчанию (настраиваемая при деплое)
- Возможность настройки индивидуальной задержки для каждого предложения
- **Функции**:
- Инициатор предложения устанавливает индивидуальную задержку
- Динамическое изменение параметров таймлока через голосование
- Отмена предложений до истечения таймлока
- Выполнение предложений после истечения таймлока
#### 2.1.4 Казначейские функции
- **Описание**: Управление финансами DLE
- **Функции**:
- Прием и отправка криптовалют
- Управление токенами
- Распределение дивидендов
- Бюджетирование
#### 2.1.5 Коммуникационные функции
- **Описание**: Прием сообщений и звонков
- **Функции**:
- Прием текстовых сообщений
- Прием аудио/видео звонков
- Кворум для коммуникационных действий
- Хранение истории коммуникаций
#### 2.1.6 Модульная система
- **Описание**: Расширяемая архитектура
- **Функции**:
- Добавление новых модулей
- Управление модулями через голосование
- Изоляция модулей
- Обновление модулей
### 2.2 Иерархическая система голосования
#### 2.2.1 Меж-DLE взаимодействие
- **Описание**: DLE может владеть токенами других DLE
- **Функции**:
- Проверка владения токенами других DLE
- Сбор кворума подписей для внешнего голосования
- Участие в голосовании других DLE
- Пропорциональный подсчет голосов
#### 2.2.2 Кворум подписей
- **Описание**: Система сбора подписей для внешнего голосования
- **Функции**:
- Создание запросов на внешнее голосование
- Сбор подписей от токен холдеров
- Проверка достижения кворума
- Активация голоса в целевой DLE
### 2.3 Межприложное взаимодействие
#### 2.3.1 Встраивание интерфейсов
- **Описание**: Управление DLE B через приложение DLE A
- **Функции**:
- Встраивание интерфейса управления
- Безопасное подписание транзакций
- Проверка прав доступа
- Аудит действий
### 2.4 Мульти-чейн архитектура
#### 2.4.1 CREATE2 детерминистический деплой
- **Описание**: Создание DLE с одинаковым адресом во всех EVM-сетях
- **Функции**:
- Использование CREATE2 opcode для предсказуемых адресов
- Factory контракт с фиксированным адресом во всех сетях
- Генерация детерминистического salt на основе пользовательских данных
- Предварительное вычисление адреса DLE до деплоя
#### 2.4.2 Синхронизация токенов управления
- **Описание**: Синхронное управление токенами во всех развернутых сетях
- **Функции**:
- Одинаковое распределение токенов для партнеров во всех сетях
- Cross-chain синхронизация операций с токенами
- Атомарное выполнение операций во всех целевых сетях
- Защита от рассинхронизации и double-spending
#### 2.4.3 Cross-chain система голосования
- **Описание**: Голосование с выбором сети и синхронизацией результатов
- **Функции**:
- Выбор сети для инициации голосования
- Расчет кворума по токенам в выбранной сети
- Синхронизация результатов во все развернутые сети
- Выполнение решений в любой из целевых сетей
#### 2.4.3 Single-Chain Governance система
- **Описание**: Упрощенная система голосования в одной выбранной сети
- **Функции**:
- Инициатор выбирает governance сеть для голосования
- Все токен-холдеры участвуют только в выбранной сети
- Инициатор устанавливает таймлок для предложения
- Проверка балансов токенов при каждой подписи
- Исполнение решения во всех целевых сетях
#### 2.4.4 Мульти-сетевой деплой
- **Описание**: Одновременный деплой в несколько блокчейн-сетей
- **Функции**:
- Выбор множественных сетей из интерфейса
- Автоматический расчет общей стоимости деплоя
- Параллельное развертывание во всех выбранных сетях
- Возможность добавления новых сетей после первоначального деплоя
#### 2.4.5 Упрощенные cross-chain операции
- **Описание**: Исполнение решений во всех целевых сетях после single-chain голосования
- **Функции**:
- Атомарное исполнение во всех выбранных сетях
- Fallback исполнение в доступных сетях при сбоях
- Мониторинг статуса исполнения операций
- Откат операций при критических сбоях
## 3. Технические требования
### 3.1 Архитектура смарт-контракта
#### 3.1.1 Основная структура
```solidity
contract DLE is ERC20, Governor, TimelockController {
// Ваша уникальная логика DLE
// + готовые компоненты с аудитом
// + проверенные паттерны
}
```
#### 3.1.2 Компоненты для интеграции
- **ERC-20** - токен управления DLE
- **Governor** - система голосования с мультиподписью
- **TimelockController** - настраиваемые таймлоки
- **Account Abstraction** - универсальность адреса
#### 3.1.3 Мульти-чейн архитектура
```solidity
// Factory для детерминистического деплоя
contract DLEFactory {
function createDLE(
bytes32 salt,
DLEConfig memory config,
uint256[] memory targetChains
) external returns (address predictedAddress);
function predictAddress(bytes32 salt, DLEConfig memory config)
external view returns (address);
}
// Single-Chain Governance
contract DLE_Governance {
struct Proposal {
bytes operation;
uint256[] targetChains;
uint256 timelock;
uint256 governanceChain;
address initiator;
bytes[] signatures;
bool executed;
}
function createProposal(bytes calldata operation, uint256[] calldata targetChains, uint256 timelockDelay) external;
function signProposal(uint256 proposalId) external onlyTokenHolder;
function executeProposal(uint256 proposalId) external;
}
// Основной контракт DLE с упрощенной архитектурой
contract DLE is ERC20, Governor, TimelockController {
// Single-chain governance
mapping(uint256 => bool) public supportedChains;
mapping(uint256 => Proposal) public proposals;
// Проверка токен-холдеров
modifier onlyTokenHolder() {
require(balanceOf(msg.sender) > 0, "Must hold tokens");
_;
}
// Исполнение в целевых сетях
function executeInTargetChains(bytes calldata operation, uint256[] calldata chains) external;
}
```
#### 3.1.4 Компоненты для интеграции
- **ERC-20** - токен управления DLE
- **Governor** - система голосования с мультиподписью
- **TimelockController** - отдельный модуль настраиваемых таймлоков
- **Account Abstraction** - универсальность адреса
- **CREATE2 Factory** - детерминистический деплой
- **Single-Chain Governance** - упрощенное управление через одну сеть
### 3.2 Готовые компоненты с аудитом
#### 3.2.1 OpenZeppelin (аудит: ConsenSys Diligence)
- ERC-20 - токены управления
- Governance - система голосования
- Access Control - роли и разрешения
- Multisig - мультиподпись
- Timelock - задержки выполнения
#### 3.2.2 ERC-4337 (аудит: Trail of Bits)
- Account Abstraction - универсальность
- Smart Contract Wallets - кошельки
- Bundlers - оптимизация газа
#### 3.2.3 Проверенные паттерны
- Diamond Pattern (EIP-2535) - модульность
- Proxy Pattern - обновляемость
- Factory Pattern - создание контрактов
### 3.3 Безопасность
#### 3.3.1 Требования к безопасности
- Полный аудит смарт-контракта
- Тестирование всех функций
- Проверка уязвимостей
- Соответствие стандартам безопасности
#### 3.3.2 Меры безопасности
- Использование проаудированных компонентов
- Проверенные паттерны разработки
- Изоляция рисков
- Поэтапная разработка с тестированием
### 3.4 Производительность
#### 3.4.1 Оптимизация газа
- Минимизация стоимости транзакций
- Эффективные алгоритмы
- Использование bundlers (ERC-4337)
- Оптимизация хранения данных
#### 3.4.2 Масштабируемость
- Поддержка большого количества участников
- Эффективная обработка голосований
- Оптимизация меж-DLE взаимодействий
- Модульная архитектура
## 4. Интерфейсы и интеграции
### 4.1 Веб3 приложение
#### 4.1.1 Функции приложения
- Создание DLE через форму
- Управление DLE
- Участие в голосованиях
- Просмотр истории транзакций
#### 4.1.2 Межприложное взаимодействие
- Встраивание интерфейсов других DLE
- Безопасное подписание транзакций
- Проверка прав доступа
- Аудит действий
#### 4.1.3 Мульти-чейн интерфейс с single-chain governance
- Выбор целевых сетей для деплоя DLE
- Отображение предсказанного адреса DLE
- Расчет стоимости деплоя по всем сетям
- Мониторинг статуса деплоя во всех сетях
- Выбор governance сети для создания предложений
- Установка таймлока инициатором предложения
- Подписание предложений токен-холдерами в governance сети
- Мониторинг исполнения в целевых сетях
- История операций и голосований
### 4.2 API и интеграции
#### 4.2.1 Внешние интеграции
- Оракулы для внешних данных
- Интеграция с DeFi протоколами
- Поддержка различных блокчейнов
- API для внешних приложений
## 5. Этапы разработки
### 5.1 Этап 1: Базовая функциональность
- Создание основного контракта DLE
- Интеграция ERC-20 токенов
- Базовая система голосования с настраиваемым кворумом
- Простые казначейские функции
- CREATE2 Factory для детерминистического деплоя
- Настройки времени голосования (период, задержка)
### 5.2 Этап 2: Расширенная функциональность
- Система мультиподписи
- Отдельный модуль TimelockController с настраиваемыми параметрами
- Коммуникационные функции
- Модульная система
- Мульти-сетевой деплой в тестовых сетях
- Базовая cross-chain синхронизация
### 5.3 Этап 3: Мульти-чейн архитектура
- Полная cross-chain синхронизация токенов
- Система голосования с выбором сети
- Cross-chain операции с откатами
- Мониторинг состояния во всех сетях
- Emergency pause и fallback механизмы
- Иерархическая система голосования между DLE
### 5.4 Этап 4: Межприложное взаимодействие
- Встраивание интерфейсов других DLE
- Безопасное cross-chain подписание
- Оптимизация газа для мульти-сетевых операций
- Интеграция с bridge протоколами
- Расширенное тестирование мульти-чейн функций
### 5.5 Этап 5: Аудит и запуск
- Профессиональный аудит всех компонентов
- Аудит мульти-чейн безопасности
- Тестирование в различных сетевых условиях
- Исправление уязвимостей
- Финальное тестирование cross-chain операций
- Развертывание в продакшн во всех целевых сетях
## 6. Требования к тестированию
### 6.1 Unit тесты
- Тестирование всех функций контракта
- Проверка граничных случаев
- Тестирование безопасности
- Проверка производительности
### 6.2 Integration тесты
- Тестирование взаимодействия модулей
- Проверка меж-DLE взаимодействий
- Тестирование веб3 приложения
- Проверка API интеграций
### 6.3 E2E тесты
- Полный цикл создания DLE
- Тестирование голосований
- Проверка коммуникационных функций
- Тестирование межприложного взаимодействия
## 7. Документация
### 7.1 Техническая документация
- Описание архитектуры
- API документация
- Руководство по развертыванию
- Руководство по безопасности
### 7.2 Пользовательская документация
- Руководство пользователя
- FAQ
- Видеоуроки
- Поддержка
## 8. Критерии приемки
### 8.1 Функциональные критерии
- Все функции работают согласно требованиям
- Система голосования функционирует корректно
- Меж-DLE взаимодействие работает
- Коммуникационные функции активны
- CREATE2 деплой создает одинаковые адреса во всех сетях
- Cross-chain синхронизация токенов работает корректно
- Голосование с выбором сети функционирует
- Мульти-сетевой деплой завершается успешно во всех целевых сетях
### 8.2 Критерии безопасности
- Прохождение профессионального аудита
- Отсутствие критических уязвимостей
- Соответствие стандартам безопасности
- Проверка всех сценариев атак
- Безопасность cross-chain операций
- Защита от MEV-атак при мульти-чейн операциях
- Корректная работа откатов при сбоях синхронизации
- Валидация кворума во всех поддерживаемых сетях
### 8.3 Критерии производительности
- Оптимизация газа
- Быстрая обработка транзакций
- Масштабируемость системы
- Стабильная работа под нагрузкой
- Эффективная синхронизация между сетями
- Минимальные задержки cross-chain операций
- Оптимальное использование ресурсов во всех сетях
- Быстрое восстановление после сбоев синхронизации
## 9. Лицензии и правовые аспекты
### 9.1 Используемые лицензии
- OpenZeppelin - MIT лицензия
- ERC-4337 - CC0 лицензия
- Собственный код - Proprietary
### 9.2 Патентные аспекты
- Низкий патентный риск для концепции DLE
- Использование открытых стандартов
- Защита уникальных функций
- Консультации с юристами при необходимости

249
docs/ai-queue-system.md Normal file
View File

@@ -0,0 +1,249 @@
# Система очереди AI запросов
## Обзор
Система очереди AI запросов предназначена для управления нагрузкой на Ollama и обеспечения стабильной работы AI ассистента. Она предотвращает перегрузку модели, обеспечивает приоритизацию запросов и предоставляет мониторинг производительности.
## Архитектура
### Компоненты
1. **AIQueueService** (`backend/services/ai-queue.js`) - основной сервис очереди
2. **AI Queue Routes** (`backend/routes/ai-queue.js`) - API маршруты для управления очередью
3. **AIQueueMonitor** (`frontend/src/components/AIQueueMonitor.vue`) - компонент мониторинга
4. **Обновленный Chat Route** - поддержка очереди в чате
### Технологии
- **better-queue** - библиотека для управления очередью
- **Node.js** - серверная часть
- **Vue.js** - клиентская часть
- **Chart.js** - визуализация статистики
## Конфигурация очереди
### Основные параметры
```javascript
{
concurrent: 2, // Количество одновременных запросов
maxTimeout: 180000, // Максимальное время выполнения (3 мин)
afterProcessDelay: 1000, // Задержка между задачами (1 сек)
maxRetries: 2, // Максимальное количество повторных попыток
retryDelay: 5000 // Задержка между повторными попытками (5 сек)
}
```
### Приоритизация
Система автоматически определяет приоритет задач:
- **Администраторы**: +10 к приоритету
- **Срочные запросы**: +20 к приоритету
- **Чат**: +5 к приоритету
- **Анализ**: +3 к приоритету
- **Генерация**: +1 к приоритету
- **Короткие запросы** (<100 символов): +2 к приоритету
- **Долгое ожидание** (>30 сек): +5 к приоритету
## API Endpoints
### Получение статистики
```http
GET /api/ai-queue/stats
```
### Добавление задачи в очередь
```http
POST /api/ai-queue/task
Content-Type: application/json
{
"message": "Текст сообщения",
"language": "ru",
"history": [...],
"systemPrompt": "...",
"rules": {...},
"type": "chat"
}
```
### Получение статуса задачи
```http
GET /api/ai-queue/task/:taskId
```
### Управление очередью (только для админов)
```http
POST /api/ai-queue/control
Content-Type: application/json
{
"action": "pause|resume|clear"
}
```
### Информация о производительности
```http
GET /api/ai-queue/performance
```
## Использование в чате
### Обычный режим (без очереди)
```http
POST /api/chat/message
```
### Режим с очередью
```http
POST /api/chat/message-queued
```
## Мониторинг
### Компонент AIQueueMonitor
Компонент предоставляет:
- **Статус очереди**: активна, ожидает, пуста, ошибка
- **Количество задач**: в очереди и выполняющихся
- **Производительность**: успешность, среднее время обработки
- **Детальная статистика**: общее количество, ошибки, время
- **Управление**: пауза, возобновление, очистка (для админов)
- **График производительности**: реальное время
### Интеграция в интерфейс
```vue
<template>
<AIQueueMonitor :isAdmin="userIsAdmin" />
</template>
<script>
import AIQueueMonitor from '@/components/AIQueueMonitor.vue'
export default {
components: {
AIQueueMonitor
}
}
</script>
```
## Ограничения и защита
### Rate Limiting
- **Ограничение по пользователю**: 10 запросов в минуту
- **Размер сообщения**: максимум 10,000 символов
- **Валидация**: проверка обязательных полей
### Фильтрация
- Проверка формата сообщения
- Валидация ID запроса
- Проверка размера сообщения
- Контроль частоты запросов
### Слияние задач
- Объединение одинаковых запросов от одного пользователя
- Обновление метаданных при повторных запросах
## Производительность
### Оптимизации
1. **Кэширование**: ответы кэшируются на 5 минут
2. **Проверка здоровья**: мониторинг состояния модели
3. **Ограничение ресурсов**: Docker контейнер с лимитами
4. **Таймауты**: защита от зависших запросов
### Мониторинг
- Время обработки запросов
- Успешность выполнения
- Размер очереди
- Количество ошибок
- Статистика по пользователям
## Устранение неполадок
### Частые проблемы
1. **Медленные ответы**
- Проверить размер очереди
- Увеличить количество concurrent задач
- Проверить ресурсы Ollama
2. **Ошибки таймаута**
- Увеличить maxTimeout
- Проверить состояние модели
- Очистить очередь
3. **Высокая нагрузка**
- Уменьшить concurrent задачи
- Включить rate limiting
- Добавить задержки между задачами
### Логи
```bash
# Просмотр логов очереди
docker logs dapp-backend | grep AIQueue
# Статистика очереди
curl http://localhost:8000/api/ai-queue/stats
# Проверка здоровья
curl http://localhost:8000/api/health
```
## Настройка для продакшена
### Рекомендуемые параметры
```javascript
{
concurrent: 1, // Один запрос за раз для стабильности
maxTimeout: 300000, // 5 минут таймаут
afterProcessDelay: 2000, // 2 секунды между запросами
maxRetries: 1, // Одна повторная попытка
retryDelay: 10000 // 10 секунд между попытками
}
```
### Мониторинг
- Настройка алертов при высокой нагрузке
- Логирование всех операций
- Метрики производительности
- Автоматическое масштабирование
### Безопасность
- Аутентификация для всех endpoints
- Авторизация для управления очередью
- Валидация всех входных данных
- Защита от DDoS атак
## Разработка
### Добавление новых типов задач
1. Обновить функцию `getTaskPriority`
2. Добавить обработку в `processTask`
3. Обновить документацию
### Расширение мониторинга
1. Добавить новые метрики в `getStats`
2. Обновить компонент `AIQueueMonitor`
3. Добавить новые API endpoints
### Интеграция с другими сервисами
1. Подключение к Redis для персистентности
2. Интеграция с системами мониторинга
3. Подключение к лог-агрегаторам

Some files were not shown because too many files have changed in this diff Show More