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

This commit is contained in:
2025-09-24 13:05:20 +03:00
parent de0f8aecf2
commit 76cde4b53d
45 changed files with 2167 additions and 2854 deletions

View File

@@ -35,8 +35,8 @@ async function upsertProviderSettings({ provider, api_key, base_url, selected_mo
const existing = await encryptedDb.getData(TABLE, { provider: provider }, 1);
if (existing.length > 0) {
// Обновляем существующую запись
return await encryptedDb.saveData(TABLE, data, { provider: provider });
// Обновляем существующую запись по ID
return await encryptedDb.saveData(TABLE, data, { id: existing[0].id });
} else {
// Создаем новую запись
return await encryptedDb.saveData(TABLE, data);
@@ -120,29 +120,51 @@ async function getAllLLMModels() {
for (const provider of providers) {
if (provider.selected_model) {
allModels.push({
id: provider.selected_model,
provider: provider.provider
});
// Фильтруем embedding модели - они не должны быть в списке LLM
const modelName = provider.selected_model.toLowerCase();
const isEmbeddingModel = modelName.includes('embed') ||
modelName.includes('embedding') ||
modelName.includes('bge') ||
modelName.includes('nomic') ||
modelName.includes('text-embedding') ||
modelName.includes('mxbai') ||
modelName.includes('sentence') ||
modelName.includes('ada-002') ||
modelName.includes('text-embedding-ada') ||
modelName.includes('text-embedding-3');
if (!isEmbeddingModel) {
allModels.push({
id: provider.selected_model,
provider: provider.provider
});
}
}
}
// Для Ollama проверяем реально установленные модели
// Для Ollama проверяем реально установленные модели через HTTP API
try {
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
const axios = require('axios');
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
// Проверяем, какие модели установлены в Ollama
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
const response = await axios.get(`${ollamaUrl}/api/tags`, {
timeout: 5000
});
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2) {
const modelName = parts[0];
const models = response.data.models || [];
for (const model of models) {
// Фильтруем embedding модели из Ollama
const modelName = model.name.toLowerCase();
const isEmbeddingModel = modelName.includes('embed') ||
modelName.includes('embedding') ||
modelName.includes('bge') ||
modelName.includes('nomic') ||
modelName.includes('mxbai') ||
modelName.includes('sentence');
if (!isEmbeddingModel) {
allModels.push({
id: modelName,
id: model.name,
provider: 'ollama'
});
}
@@ -189,27 +211,23 @@ async function getAllEmbeddingModels() {
}
}
// Для Ollama проверяем реально установленные embedding модели
// Для Ollama проверяем реально установленные embedding модели через HTTP API
try {
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
const axios = require('axios');
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
// Проверяем, какие embedding модели установлены в Ollama
const { stdout } = await execAsync('docker exec dapp-ollama ollama list');
const lines = stdout.trim().split('\n').slice(1); // Пропускаем заголовок
const response = await axios.get(`${ollamaUrl}/api/tags`, {
timeout: 5000
});
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'
});
}
const models = response.data.models || [];
for (const model of models) {
// Проверяем, что это embedding модель
if (model.name.includes('embed') || model.name.includes('bge') || model.name.includes('nomic')) {
allModels.push({
id: model.name,
provider: 'ollama'
});
}
}
} catch (ollamaError) {

View File

@@ -466,6 +466,191 @@ class AuthService {
}
}
/**
* Определяет уровень доступа пользователя на основе количества токенов
* @param {string} address - Адрес кошелька
* @returns {Promise<{level: string, tokenCount: number, hasAccess: boolean}>}
*/
async getUserAccessLevel(address) {
if (!address) {
return { level: 'user', tokenCount: 0, hasAccess: false };
}
logger.info(`Checking access level for address: ${address}`);
try {
// Получаем токены из базы данных напрямую (как в checkAdminRole)
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, readonly_threshold, editor_threshold, 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;
// Получаем RPC провайдеры
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;
}
// Получаем балансы токенов из блокчейна
const ERC20_ABI = ['function balanceOf(address owner) view returns (uint256)'];
const tokenBalances = [];
for (const token of tokens) {
const rpcUrl = rpcMap[token.network];
if (!rpcUrl) continue;
try {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
// Получаем баланс с таймаутом
const balancePromise = tokenContract.balanceOf(address);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 5000) // Увеличиваем таймаут до 5 секунд
);
const rawBalance = await Promise.race([balancePromise, timeoutPromise]);
const balance = ethers.formatUnits(rawBalance, 18);
tokenBalances.push({
network: token.network,
tokenAddress: token.address,
tokenName: token.name,
symbol: '',
balance,
minBalance: token.min_balance,
readonlyThreshold: token.readonly_threshold || 1,
editorThreshold: token.editor_threshold || 2,
});
logger.info(`[getUserAccessLevel] Token balance for ${token.name} (${token.address}): ${balance}`);
} catch (error) {
logger.error(`[getUserAccessLevel] Error getting balance for ${token.name} (${token.address}):`, error.message);
// Добавляем токен с нулевым балансом
tokenBalances.push({
network: token.network,
tokenAddress: token.address,
tokenName: token.name,
symbol: '',
balance: '0',
minBalance: token.min_balance,
readonlyThreshold: token.readonly_threshold || 1,
editorThreshold: token.editor_threshold || 2,
});
}
}
if (!tokenBalances || !Array.isArray(tokenBalances)) {
logger.warn(`No token balances found for address: ${address}`);
return { level: 'user', tokenCount: 0, hasAccess: false };
}
// Подсчитываем сумму токенов с достаточным балансом
let validTokenCount = 0;
const validTokens = [];
for (const token of tokenBalances) {
const balance = parseFloat(token.balance || '0');
const minBalance = parseFloat(token.minBalance || '0');
if (balance >= minBalance) {
validTokenCount += balance; // Суммируем баланс токенов, а не количество сетей
validTokens.push({
name: token.name,
network: token.network,
balance: balance,
minBalance: minBalance
});
}
}
logger.info(`Token validation for ${address}:`, {
totalTokens: tokenBalances.length,
validTokens: validTokenCount,
validTokenDetails: validTokens
});
// Определяем уровень доступа на основе настроек токенов
let accessLevel = 'user';
let hasAccess = false;
// Получаем настройки порогов из токенов (используем самые низкие требования для максимального доступа)
let readonlyThreshold = Infinity;
let editorThreshold = Infinity;
if (tokenBalances.length > 0) {
// Находим самые низкие пороги среди всех токенов
for (const token of tokenBalances) {
const tokenReadonlyThreshold = token.readonlyThreshold || 1;
const tokenEditorThreshold = token.editorThreshold || 2;
if (tokenReadonlyThreshold < readonlyThreshold) {
readonlyThreshold = tokenReadonlyThreshold;
}
if (tokenEditorThreshold < editorThreshold) {
editorThreshold = tokenEditorThreshold;
}
}
// Если не нашли токены с порогами, используем дефолтные значения
if (readonlyThreshold === Infinity) readonlyThreshold = 1;
if (editorThreshold === Infinity) editorThreshold = 2;
logger.info(`[AuthService] Определены пороги доступа: readonly=${readonlyThreshold}, editor=${editorThreshold} (из ${tokenBalances.length} токенов)`);
} else {
readonlyThreshold = 1;
editorThreshold = 2;
}
if (validTokenCount >= editorThreshold) {
// Достаточно токенов для полных прав редактора
accessLevel = 'editor';
hasAccess = true;
} else if (validTokenCount > 0) {
// Есть токены, но недостаточно для редактора - права только на чтение
accessLevel = 'readonly';
hasAccess = true;
} else {
// Нет токенов - обычный пользователь
accessLevel = 'user';
hasAccess = false;
}
logger.info(`Access level determined for ${address}: ${accessLevel} (${validTokenCount} tokens)`);
return {
level: accessLevel,
tokenCount: validTokenCount,
hasAccess: hasAccess,
validTokens: validTokens
};
} catch (error) {
logger.error(`Error in getUserAccessLevel: ${error.message}`);
return { level: 'user', tokenCount: 0, hasAccess: false };
}
}
// Добавляем псевдоним функции checkAdminRole для обратной совместимости
async checkAdminTokens(address) {
if (!address) return false;
@@ -473,9 +658,11 @@ class AuthService {
logger.info(`Checking admin tokens for address: ${address}`);
try {
const isAdmin = await checkAdminRole(address);
// Используем новую функцию для определения уровня доступа
const accessLevel = await this.getUserAccessLevel(address);
const isAdmin = accessLevel.hasAccess; // Любой доступ выше 'user' считается админским
// Обновляем роль пользователя в базе данных, если есть админские токены
// Обновляем роль пользователя в базе данных
if (isAdmin) {
try {
// Получаем ключ шифрования
@@ -503,16 +690,17 @@ class AuthService {
if (userResult.rows.length > 0) {
const userId = userResult.rows[0].id;
// Обновляем роль пользователя
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['admin', userId]);
logger.info(`Updated user ${userId} role to admin based on token holdings`);
// Обновляем роль пользователя с учетом уровня доступа
const role = accessLevel.level;
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [role, userId]);
logger.info(`Updated user ${userId} role to ${role} based on token holdings (${accessLevel.tokenCount} tokens)`);
}
} catch (error) {
logger.error('Error updating user role:', error);
// Продолжаем выполнение, даже если обновление роли не удалось
}
} else {
// Если пользователь не является администратором, сбрасываем роль на "user", если она была "admin"
// Если пользователь не имеет доступа, сбрасываем роль на "user"
try {
// Получаем ключ шифрования
const fs = require('fs');
@@ -536,10 +724,10 @@ class AuthService {
[address.toLowerCase(), encryptionKey]
);
if (userResult.rows.length > 0 && userResult.rows[0].role === 'admin') {
if (userResult.rows.length > 0 && userResult.rows[0].role !== 'user') {
const userId = userResult.rows[0].id;
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', ['user', userId]);
logger.info(`Reset user ${userId} role from admin to user (no tokens found)`);
logger.info(`Reset user ${userId} role to user (no valid tokens found)`);
}
} catch (error) {
logger.error('Error updating user role:', error);
@@ -594,20 +782,18 @@ class AuthService {
const address = user.address;
const currentRole = user.role;
logger.info(`Rechecking admin status for user ${user.id} with address ${address}`);
logger.info(`Rechecking access level for user ${user.id} with address ${address}`);
// Проверяем баланс токенов
const isAdmin = await checkAdminRole(address);
// Определяем новую роль
const newRole = isAdmin ? 'admin' : 'user';
// Получаем новый уровень доступа
const accessLevel = await this.getUserAccessLevel(address);
const newRole = accessLevel.hasAccess ? accessLevel.level : 'user';
// Обновляем роль только если она изменилась
if (currentRole !== newRole) {
await db.getQuery()('UPDATE users SET role = $1 WHERE id = $2', [newRole, user.id]);
logger.info(`Updated user ${user.id} role from ${currentRole} to ${newRole} (address: ${address})`);
logger.info(`Updated user ${user.id} role from ${currentRole} to ${newRole} (address: ${address}, tokens: ${accessLevel.tokenCount})`);
} else {
logger.info(`User ${user.id} role unchanged: ${currentRole} (address: ${address})`);
logger.info(`User ${user.id} role unchanged: ${currentRole} (address: ${address}, tokens: ${accessLevel.tokenCount})`);
}
} catch (userError) {

View File

@@ -27,13 +27,25 @@ async function saveAllAuthTokens(authTokens) {
name: token.name,
address: token.address,
network: token.network,
min_balance: token.minBalance == null ? 0 : Number(token.minBalance)
min_balance: token.minBalance == null ? 0 : Number(token.minBalance),
readonly_threshold: token.readonlyThreshold == null ? 1 : Number(token.readonlyThreshold),
editor_threshold: token.editorThreshold == null ? 2 : Number(token.editorThreshold)
});
}
}
async function upsertAuthToken(token) {
console.log('[AuthTokenService] Получены данные токена:', token);
console.log('[AuthTokenService] token.readonlyThreshold:', token.readonlyThreshold, 'тип:', typeof token.readonlyThreshold);
console.log('[AuthTokenService] token.editorThreshold:', token.editorThreshold, 'тип:', typeof token.editorThreshold);
const minBalance = token.minBalance == null ? 0 : Number(token.minBalance);
const readonlyThreshold = (token.readonlyThreshold === null || token.readonlyThreshold === undefined || token.readonlyThreshold === '') ? 1 : Number(token.readonlyThreshold);
const editorThreshold = (token.editorThreshold === null || token.editorThreshold === undefined || token.editorThreshold === '') ? 2 : Number(token.editorThreshold);
console.log('[AuthTokenService] Вычисленные значения:');
console.log('[AuthTokenService] readonlyThreshold:', readonlyThreshold);
console.log('[AuthTokenService] editorThreshold:', editorThreshold);
// Проверяем, существует ли токен
const existingTokens = await encryptedDb.getData('auth_tokens', {
@@ -45,7 +57,9 @@ async function upsertAuthToken(token) {
// Обновляем существующий токен
await encryptedDb.saveData('auth_tokens', {
name: token.name,
min_balance: minBalance
min_balance: minBalance,
readonly_threshold: readonlyThreshold,
editor_threshold: editorThreshold
}, {
address: token.address,
network: token.network
@@ -56,7 +70,9 @@ async function upsertAuthToken(token) {
name: token.name,
address: token.address,
network: token.network,
min_balance: minBalance
min_balance: minBalance,
readonly_threshold: readonlyThreshold,
editor_threshold: editorThreshold
});
}
}

View File

@@ -289,12 +289,23 @@ class EncryptedDataService {
.map((key, index) => `${quoteReservedWord(key)} = ${allData[key]}`)
.join(', ');
const whereClause = Object.keys(whereConditions)
.map((key, index) => `${quoteReservedWord(key)} = $${paramIndex + index}`)
.map((key, index) => {
// Для WHERE условий используем зашифрованные имена колонок
const encryptedColumn = columns.find(col => col.column_name === `${key}_encrypted`);
if (encryptedColumn) {
// Для зашифрованных колонок используем encrypt_text для сравнения
return `${quoteReservedWord(`${key}_encrypted`)} = encrypt_text($${paramIndex + index}, ${hasEncryptedFields ? '$1' : 'NULL'})`;
} else {
// Для незашифрованных колонок используем обычное сравнение
return `${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 {

View File

@@ -37,7 +37,7 @@ async function getUserTokenBalances(address) {
// Получаем токены и 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',
'SELECT id, min_balance, readonly_threshold, editor_threshold, 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;
@@ -57,18 +57,42 @@ async function getUserTokenBalances(address) {
for (const token of tokens) {
const rpcUrl = rpcMap[token.network];
if (!rpcUrl) continue;
const provider = new ethers.JsonRpcProvider(rpcUrl);
if (!rpcUrl) {
logger.warn(`[tokenBalanceService] RPC URL не найден для сети ${token.network}`);
continue;
}
// Создаем провайдер с таймаутом
const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {
polling: false,
staticNetwork: true
});
// Устанавливаем таймаут для запросов
provider._getConnection().timeout = 10000; // 10 секунд
const tokenContract = new ethers.Contract(token.address, ERC20_ABI, provider);
let balance = '0';
try {
const rawBalance = await tokenContract.balanceOf(address);
logger.info(`[tokenBalanceService] Получение баланса для ${token.name} (${token.address}) в сети ${token.network} для адреса ${address}`);
// Создаем промис с таймаутом
const balancePromise = tokenContract.balanceOf(address);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Request timeout')), 10000)
);
const rawBalance = await Promise.race([balancePromise, timeoutPromise]);
balance = ethers.formatUnits(rawBalance, 18);
if (!balance || isNaN(Number(balance))) balance = '0';
logger.info(`[tokenBalanceService] Баланс получен для ${token.name}: ${balance}`);
} catch (e) {
logger.error(
`[tokenBalanceService] Ошибка получения баланса для ${token.name} (${token.address}) в сети ${token.network}:`,
e
e.message || e
);
balance = '0';
}
@@ -79,6 +103,8 @@ async function getUserTokenBalances(address) {
symbol: token.symbol || '',
balance,
minBalance: token.min_balance,
readonlyThreshold: token.readonly_threshold || 1,
editorThreshold: token.editor_threshold || 2,
});
}