ваше сообщение коммита
This commit is contained in:
14
.gitignore
vendored
14
.gitignore
vendored
@@ -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
|
||||
|
||||
# ========================================
|
||||
# ПАТЕНТНЫЕ ДОКУМЕНТЫ - НЕ ПУБЛИКОВАТЬ!
|
||||
# ========================================
|
||||
|
||||
@@ -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
381
backend/contracts/DLE.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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: инициализировано дефолтными значениями');
|
||||
|
||||
1498
backend/db/data/countries.json
Normal file
1498
backend/db/data/countries.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
136
backend/db/data/jurisdiction_codes_analysis.md
Normal file
136
backend/db/data/jurisdiction_codes_analysis.md
Normal 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
176
backend/db/data/kbk.json
Normal 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": "Доходы от предпринимательской и иной приносящей доход деятельности"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
backend/db/data/kpp_codes.json
Normal file
44
backend/db/data/kpp_codes.json
Normal 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
264
backend/db/data/okpd2.json
Normal 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
344
backend/db/data/oktmo.json
Normal 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
260
backend/db/data/okun.json
Normal 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
6712
backend/db/data/okved.json
Normal file
File diff suppressed because it is too large
Load Diff
64
backend/db/data/operation-codes.json
Normal file
64
backend/db/data/operation-codes.json
Normal 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": "Платежное требование с приложением (электронное)"
|
||||
}
|
||||
]
|
||||
}
|
||||
64
backend/db/data/report-forms.json
Normal file
64
backend/db/data/report-forms.json
Normal 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
240
backend/db/data/tnved.json
Normal 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": "Лампы, трубки электронные термоионные, холодного катода или фотокатодные"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 $$
|
||||
|
||||
@@ -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 $$;
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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 $$;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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 $$
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
-- Обрабатываем 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 $$;
|
||||
-- 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 $$;
|
||||
|
||||
-- 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. Создаем диагностическую функцию
|
||||
|
||||
11
backend/db/migrations/015_disable_admin_role_trigger.sql
Normal file
11
backend/db/migrations/015_disable_admin_role_trigger.sql
Normal 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 и других сервисах
|
||||
@@ -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 'Имя пользователя';
|
||||
|
||||
@@ -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 $$;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
-- Загружаем данные во временную таблицу
|
||||
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 '"');
|
||||
|
||||
-- Вставляем данные в основную таблицу
|
||||
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 $$;
|
||||
|
||||
-- 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; -- Временная таблица удалится после коммита
|
||||
-- 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;
|
||||
|
||||
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; -- Временная таблица удалится после коммита
|
||||
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;
|
||||
|
||||
-- 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 '"');
|
||||
-- Загружаем данные во временные таблицы
|
||||
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 '/mnt/isic_csv_data/isic_structure.csv' -- <--- ПУТЬ СООТВЕТСТВУЕТ ТОМУ, ЧТО В volumes
|
||||
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 '"');
|
||||
|
||||
-- 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;
|
||||
-- Переносим и объединяем данные из временных таблиц в основную таблицу 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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 $$;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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 $$;
|
||||
@@ -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 $$;
|
||||
@@ -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 для проверки аутентификации
|
||||
*/
|
||||
|
||||
@@ -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
188
backend/routes/ai-queue.js
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
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 (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]
|
||||
);
|
||||
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]
|
||||
);
|
||||
|
||||
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 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]
|
||||
) {
|
||||
// Расшифровываем 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;
|
||||
|
||||
|
||||
@@ -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
102
backend/routes/countries.js
Normal 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
173
backend/routes/dleV2.js
Normal 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;
|
||||
@@ -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
78
backend/routes/kpp.js
Normal 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;
|
||||
@@ -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
173
backend/routes/ollama.js
Normal 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;
|
||||
184
backend/routes/russian-classifiers.js
Normal file
184
backend/routes/russian-classifiers.js
Normal 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;
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
190
backend/scripts/deploy/create-dle-v2.js
Normal file
190
backend/scripts/deploy/create-dle-v2.js
Normal 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);
|
||||
});
|
||||
110
backend/scripts/fix-rag-columns.js
Normal file
110
backend/scripts/fix-rag-columns.js
Normal 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 };
|
||||
120
backend/scripts/test-ai-queue-docker.js
Normal file
120
backend/scripts/test-ai-queue-docker.js
Normal 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 };
|
||||
116
backend/scripts/test-ai-queue.js
Normal file
116
backend/scripts/test-ai-queue.js
Normal 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 };
|
||||
82
backend/scripts/test-encrypted-tables.js
Normal file
82
backend/scripts/test-encrypted-tables.js
Normal 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 };
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
377
backend/services/ai-queue.js
Normal file
377
backend/services/ai-queue.js
Normal 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;
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем и экспортируем единственный экземпляр
|
||||
|
||||
@@ -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 };
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
313
backend/services/dleV2Service.js
Normal file
313
backend/services/dleV2Service.js
Normal 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();
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
440
backend/services/encryptedDatabaseService.js
Normal file
440
backend/services/encryptedDatabaseService.js
Normal 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();
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
195
backend/wsHub.js
195
backend/wsHub.js
@@ -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
|
||||
};
|
||||
@@ -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
38
backup-database.sh
Executable 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
|
||||
BIN
bfg-1.14.0.jar
BIN
bfg-1.14.0.jar
Binary file not shown.
73
decrypt-all-tables.sh
Executable file
73
decrypt-all-tables.sh
Executable 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 "✅ Расшифровка завершена!"
|
||||
@@ -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:
|
||||
|
||||
693
docs/DLE_MANAGEMENT_TASKS.md
Normal file
693
docs/DLE_MANAGEMENT_TASKS.md
Normal 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
240
docs/ENCRYPTION_GUIDE.md
Normal 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
200
docs/MIGRATION_GUIDE.md
Normal 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
409
docs/SMART_CONTRACTS.md
Normal 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 протоколами
|
||||
429
docs/TECHNICAL_SPECIFICATION.md
Normal file
429
docs/TECHNICAL_SPECIFICATION.md
Normal 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
249
docs/ai-queue-system.md
Normal 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
Reference in New Issue
Block a user