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

This commit is contained in:
2025-08-07 20:27:24 +03:00
parent cde35ac576
commit 0a72902c37
44 changed files with 3594 additions and 1447 deletions

View File

@@ -1,5 +1,21 @@
# Финальная безопасная конфигурация nginx
# Включаем WAF конфигурацию
# include /etc/nginx/conf.d/waf.conf;
# Блокировка всех подозрительных поддоменов
server {
listen 80;
server_name _;
# Возвращаем 444 (Connection Closed Without Response) для всех неизвестных доменов
return 444;
# Логируем попытки доступа к подозрительным доменам
access_log /var/log/nginx/suspicious_domains.log;
}
# Основной сервер только для легитимных доменов
server {
listen 80;
server_name hb3-accelerator.com www.hb3-accelerator.com localhost 127.0.0.1;
@@ -7,22 +23,40 @@ server {
root /usr/share/nginx/html;
index index.html;
# Блокировка по WAF правилам
# if ($bad_ip = 1) {
# return 403;
# }
# if ($bad_bot = 1) {
# return 403;
# }
# if ($bad_request = 1) {
# return 404;
# }
# if ($bad_domain = 1) {
# return 404;
# }
# Блокировка агрессивных сканеров
if ($http_user_agent ~* (sqlmap|nikto|dirb|gobuster|wfuzz|burp|zap|nessus|openvas)) {
return 403;
}
# Блокировка старых браузеров и подозрительных User-Agent
if ($http_user_agent ~* "Chrome/[1-7][0-9]\.") {
# Блокировка только очень старых браузеров (до Chrome 50)
if ($http_user_agent ~* "Chrome/[1-4][0-9]\.") {
return 403;
}
if ($http_user_agent ~* "Safari/[1-5][0-9][0-9]\.") {
# Блокировка только очень старых Safari (до версии 500)
if ($http_user_agent ~* "Safari/[1-4][0-9][0-9]\.") {
return 403;
}
# Блокировка подозрительных поддоменов
if ($host !~* "^(hb3-accelerator\.com|www\.hb3-accelerator\.com|localhost|127\.0\.0\.1)$") {
# Дополнительная проверка подозрительных поддоменов
if ($host ~* "^(test|dev|staging|admin|beta|demo|old|new|backup|www2|www3|www4|www5|www6|www7|www8|www9|www10)\.hb3-accelerator\.com$") {
return 404;
}
@@ -51,11 +85,6 @@ server {
return 403;
}
# Блокировка HEAD запросов к подозрительным файлам
if ($request_method = "HEAD" && $request_uri ~* "(backup|backups|bak|old|restore|\.tar|\.gz|\.sql|config\.js|sftp-config\.json)") {
return 404;
}
# Блокировка всех запросов к конфигурационным файлам
if ($request_uri ~* "(config\.js|sftp-config\.json|\.config\.|\.conf\.|\.ini\.|\.env\.|\.json\.)") {
return 404;

View File

@@ -41,6 +41,7 @@ map $http_user_agent $bad_bot {
geo $bad_ip {
default 0;
198.55.98.76 1;
# Дополнительные IP будут добавляться автоматически мониторингом
}
# Блокировка подозрительных запросов
@@ -56,4 +57,17 @@ map $request_uri $bad_request {
~*(config|setup|install|upgrade|backup|restore) 1;
~*\.(env|config|ini|conf|cfg|yml|yaml|json|xml|sql|db|bak|backup|old|tmp|temp|log)$ 1;
~*(\.\.|\.\./|\.\.\\|\.\.%2f|\.\.%5c) 1;
}
# Блокировка подозрительных доменов
map $host $bad_domain {
default 0;
~*^(test|dev|staging|admin|beta|demo|old|new|backup|www2|www3|www4|www5|www6|www7|www8|www9|www10)\.hb3-accelerator\.com$ 1;
~*akamai-inputs- 1;
~*gosipgambar 1;
~*gitlab\.cloud 1;
~*autodiscover\.home 1;
~*akamai-san 1;
~*bestcupcakerecipes 1;
~*usmc1 1;
}

View File

@@ -1,3 +1,4 @@
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html/
COPY nginx-tunnel.conf /etc/nginx/conf.d/default.conf
COPY nginx-tunnel.conf /etc/nginx/conf.d/default.conf
# COPY nginx-waf.conf /etc/nginx/conf.d/waf.conf

View File

@@ -35,6 +35,7 @@
import { useAuth, provideAuth } from './composables/useAuth';
import { fetchTokenBalances } from './services/tokens';
import eventBus from './utils/eventBus';
import wsClient from './utils/websocket';
// Импорт стилей
import './assets/styles/variables.css';
@@ -163,6 +164,28 @@
}
});
// Подписываемся на WebSocket события для токенов
wsClient.onAuthTokenAdded(() => {
console.log('[App] WebSocket: токен добавлен, обновляем балансы');
if (auth.isAuthenticated.value) {
refreshTokenBalances();
}
});
wsClient.onAuthTokenDeleted(() => {
console.log('[App] WebSocket: токен удален, обновляем балансы');
if (auth.isAuthenticated.value) {
refreshTokenBalances();
}
});
wsClient.onAuthTokenUpdated(() => {
console.log('[App] WebSocket: токен обновлен, обновляем балансы');
if (auth.isAuthenticated.value) {
refreshTokenBalances();
}
});
// Отписываемся при размонтировании компонента
onUnmounted(() => {
if (unsubscribe) {

View File

@@ -205,51 +205,63 @@ export async function executeProposal(dleAddress, proposalId) {
}
/**
* Добавить модуль
* Создать предложение о добавлении модуля
* @param {string} dleAddress - Адрес DLE контракта
* @param {string} description - Описание предложения
* @param {number} duration - Длительность голосования в секундах
* @param {string} moduleId - ID модуля
* @param {string} moduleAddress - Адрес модуля
* @returns {Promise<Object>} - Результат добавления
* @param {number} chainId - ID цепочки для голосования
* @returns {Promise<Object>} - Результат создания предложения
*/
export async function addModule(dleAddress, moduleId, moduleAddress) {
export async function createAddModuleProposal(dleAddress, description, duration, moduleId, moduleAddress, chainId) {
try {
const response = await axios.post('/blockchain/add-module', {
const response = await axios.post('/blockchain/create-add-module-proposal', {
dleAddress: dleAddress,
description: description,
duration: duration,
moduleId: moduleId,
moduleAddress: moduleAddress
moduleAddress: moduleAddress,
chainId: chainId
});
if (response.data.success) {
return response.data.data;
} else {
throw new Error(response.data.message || 'Не удалось добавить модуль');
throw new Error(response.data.message || 'Не удалось создать предложение о добавлении модуля');
}
} catch (error) {
console.error('Ошибка добавления модуля:', error);
console.error('Ошибка создания предложения о добавлении модуля:', error);
throw error;
}
}
/**
* Удалить модуль
* Создать предложение об удалении модуля
* @param {string} dleAddress - Адрес DLE контракта
* @param {string} description - Описание предложения
* @param {number} duration - Длительность голосования в секундах
* @param {string} moduleId - ID модуля
* @returns {Promise<Object>} - Результат удаления
* @param {number} chainId - ID цепочки для голосования
* @returns {Promise<Object>} - Результат создания предложения
*/
export async function removeModule(dleAddress, moduleId) {
export async function createRemoveModuleProposal(dleAddress, description, duration, moduleId, chainId) {
try {
const response = await axios.post('/blockchain/remove-module', {
const response = await axios.post('/blockchain/create-remove-module-proposal', {
dleAddress: dleAddress,
moduleId: moduleId
description: description,
duration: duration,
moduleId: moduleId,
chainId: chainId
});
if (response.data.success) {
return response.data.data;
} else {
throw new Error(response.data.message || 'Не удалось удалить модуль');
throw new Error(response.data.message || 'Не удалось создать предложение об удалении модуля');
}
} catch (error) {
console.error('Ошибка удаления модуля:', error);
console.error('Ошибка создания предложения об удалении модуля:', error);
throw error;
}
}
@@ -464,4 +476,239 @@ export async function getSupportedChains(dleAddress) {
// Возвращаем пустой массив если API недоступен
return [];
}
}
/**
* Деактивировать DLE (только при достижении кворума)
* @param {string} dleAddress - Адрес DLE контракта
* @param {string} userAddress - Адрес пользователя
* @returns {Promise<Object>} - Результат деактивации
*/
export async function deactivateDLE(dleAddress, userAddress) {
try {
// Проверяем наличие браузерного кошелька
if (!window.ethereum) {
throw new Error('Браузерный кошелек не установлен');
}
// Запрашиваем подключение к кошельку
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// Проверяем, что подключенный адрес совпадает с userAddress
const connectedAddress = await signer.getAddress();
if (connectedAddress.toLowerCase() !== userAddress.toLowerCase()) {
throw new Error('Подключенный кошелек не совпадает с адресом пользователя');
}
// ABI для деактивации DLE
const dleAbi = [
"function deactivate() external",
"function balanceOf(address) external view returns (uint256)",
"function totalSupply() external view returns (uint256)",
"function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)",
"function voteDeactivation(uint256 _proposalId, bool _support) external",
"function checkDeactivationProposalResult(uint256 _proposalId) public view returns (bool passed, bool quorumReached)",
"function executeDeactivationProposal(uint256 _proposalId) external"
];
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
// Проверяем, что пользователь имеет токены
const balance = await dle.balanceOf(userAddress);
if (balance <= 0) {
throw new Error('Для деактивации DLE необходимо иметь токены');
}
// Проверяем, что DLE не пустой (есть токены)
const totalSupply = await dle.totalSupply();
if (totalSupply <= 0) {
throw new Error('DLE не имеет токенов');
}
// Выполняем деактивацию (функция проверит наличие валидного предложения с кворумом)
const tx = await dle.deactivate();
const receipt = await tx.wait();
console.log('DLE деактивирован, tx hash:', tx.hash);
return {
success: true,
txHash: tx.hash,
blockNumber: receipt.blockNumber,
message: 'DLE успешно деактивирован'
};
} catch (error) {
console.error('Ошибка деактивации DLE:', error);
throw error;
}
}
/**
* Создать предложение о деактивации DLE
* @param {string} dleAddress - Адрес DLE контракта
* @param {string} description - Описание предложения
* @param {number} duration - Длительность голосования в секундах
* @param {number} chainId - ID цепочки для деактивации
* @returns {Promise<Object>} - Результат создания предложения
*/
export async function createDeactivationProposal(dleAddress, description, duration, chainId) {
try {
// Проверяем наличие браузерного кошелька
if (!window.ethereum) {
throw new Error('Браузерный кошелек не установлен');
}
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const dleAbi = [
"function createDeactivationProposal(string memory _description, uint256 _duration, uint256 _chainId) external returns (uint256)"
];
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
const tx = await dle.createDeactivationProposal(description, duration, chainId);
const receipt = await tx.wait();
console.log('Предложение о деактивации создано, tx hash:', tx.hash);
return {
success: true,
txHash: tx.hash,
blockNumber: receipt.blockNumber,
message: 'Предложение о деактивации создано'
};
} catch (error) {
console.error('Ошибка создания предложения о деактивации:', error);
throw error;
}
}
/**
* Голосовать за предложение деактивации
* @param {string} dleAddress - Адрес DLE контракта
* @param {number} proposalId - ID предложения
* @param {boolean} support - Поддержка предложения
* @returns {Promise<Object>} - Результат голосования
*/
export async function voteDeactivationProposal(dleAddress, proposalId, support) {
try {
if (!window.ethereum) {
throw new Error('Браузерный кошелек не установлен');
}
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const dleAbi = [
"function voteDeactivation(uint256 _proposalId, bool _support) external"
];
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
const tx = await dle.voteDeactivation(proposalId, support);
const receipt = await tx.wait();
console.log('Голосование за предложение деактивации, tx hash:', tx.hash);
return {
success: true,
txHash: tx.hash,
blockNumber: receipt.blockNumber,
message: `Голосование ${support ? 'за' : 'против'} предложения деактивации`
};
} catch (error) {
console.error('Ошибка голосования за предложение деактивации:', error);
throw error;
}
}
/**
* Проверить результат предложения деактивации
* @param {string} dleAddress - Адрес DLE контракта
* @param {number} proposalId - ID предложения
* @returns {Promise<Object>} - Результат проверки
*/
export async function checkDeactivationProposalResult(dleAddress, proposalId) {
try {
const response = await axios.post('http://localhost:8000/api/blockchain/check-deactivation-proposal-result', {
dleAddress: dleAddress,
proposalId: proposalId
});
if (response.data.success) {
return response.data.data;
} else {
throw new Error(response.data.message || 'Не удалось проверить результат предложения деактивации');
}
} catch (error) {
console.error('Ошибка проверки результата предложения деактивации:', error);
throw error;
}
}
/**
* Исполнить предложение деактивации
* @param {string} dleAddress - Адрес DLE контракта
* @param {number} proposalId - ID предложения
* @returns {Promise<Object>} - Результат исполнения
*/
export async function executeDeactivationProposal(dleAddress, proposalId) {
try {
if (!window.ethereum) {
throw new Error('Браузерный кошелек не установлен');
}
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const dleAbi = [
"function executeDeactivationProposal(uint256 _proposalId) external"
];
const dle = new ethers.Contract(dleAddress, dleAbi, signer);
const tx = await dle.executeDeactivationProposal(proposalId);
const receipt = await tx.wait();
console.log('Предложение деактивации исполнено, tx hash:', tx.hash);
return {
success: true,
txHash: tx.hash,
blockNumber: receipt.blockNumber,
message: 'Предложение деактивации успешно исполнено'
};
} catch (error) {
console.error('Ошибка исполнения предложения деактивации:', error);
throw error;
}
}
/**
* Загрузить предложения деактивации
* @param {string} dleAddress - Адрес DLE контракта
* @returns {Promise<Array>} - Список предложений деактивации
*/
export async function loadDeactivationProposals(dleAddress) {
try {
const response = await axios.post('http://localhost:8000/api/blockchain/load-deactivation-proposals', {
dleAddress: dleAddress
});
if (response.data.success) {
return response.data.data.proposals;
} else {
throw new Error(response.data.message || 'Не удалось загрузить предложения деактивации');
}
} catch (error) {
console.error('Ошибка загрузки предложений деактивации:', error);
return [];
}
}

View File

@@ -124,6 +124,19 @@ class WebSocketClient {
dleAddress: dleAddress
});
}
// Обработчики для токенов аутентификации
onAuthTokenAdded(callback) {
this.on('auth_token_added', callback);
}
onAuthTokenDeleted(callback) {
this.on('auth_token_deleted', callback);
}
onAuthTokenUpdated(callback) {
this.on('auth_token_updated', callback);
}
}
// Создаем глобальный экземпляр WebSocket клиента

View File

@@ -90,6 +90,7 @@ import { reactive } from 'vue';
import useBlockchainNetworks from '@/composables/useBlockchainNetworks';
import api from '@/api/axios';
import { useAuthContext } from '@/composables/useAuth';
import eventBus from '@/utils/eventBus';
const props = defineProps({
authTokens: { type: Array, required: true }
});
@@ -97,7 +98,7 @@ const emit = defineEmits(['update']);
const newToken = reactive({ name: '', address: '', network: '', minBalance: 0 });
const { networkGroups, networks } = useBlockchainNetworks();
const { isAdmin } = useAuthContext();
const { isAdmin, checkTokenBalances, address, checkAuth } = useAuthContext();
async function addToken() {
if (!newToken.name || !newToken.address || !newToken.network) {
@@ -109,7 +110,30 @@ async function addToken() {
...newToken,
minBalance: Number(newToken.minBalance) || 0
});
emit('update');
// После добавления токена перепроверяем баланс пользователя и обновляем состояние аутентификации
try {
if (address.value) {
await checkTokenBalances(address.value);
console.log('[AuthTokensSettings] Баланс токенов перепроверен после добавления');
}
// Обновляем состояние аутентификации чтобы отразить изменения роли
await checkAuth();
console.log('[AuthTokensSettings] Состояние аутентификации обновлено после добавления токена');
// Уведомляем App.vue об изменении настроек аутентификации
eventBus.emit('auth-settings-saved');
console.log('[AuthTokensSettings] Событие auth-settings-saved отправлено');
} catch (balanceError) {
console.error('[AuthTokensSettings] Ошибка при перепроверке баланса:', balanceError);
}
// Небольшая задержка для синхронизации с backend
setTimeout(() => {
emit('update');
}, 100);
newToken.name = '';
newToken.address = '';
newToken.network = '';
@@ -130,7 +154,29 @@ async function removeToken(index) {
try {
const response = await api.delete(`/settings/auth-token/${token.address}/${token.network}`);
console.log('[AuthTokensSettings] Успешное удаление:', response.data);
emit('update');
// После удаления токена перепроверяем баланс пользователя и обновляем состояние аутентификации
try {
if (address.value) {
await checkTokenBalances(address.value);
console.log('[AuthTokensSettings] Баланс токенов перепроверен после удаления');
}
// Обновляем состояние аутентификации чтобы отразить изменения роли
await checkAuth();
console.log('[AuthTokensSettings] Состояние аутентификации обновлено после удаления токена');
// Уведомляем App.vue об изменении настроек аутентификации
eventBus.emit('auth-settings-saved');
console.log('[AuthTokensSettings] Событие auth-settings-saved отправлено');
} catch (balanceError) {
console.error('[AuthTokensSettings] Ошибка при перепроверке баланса:', balanceError);
}
// Небольшая задержка для синхронизации с backend
setTimeout(() => {
emit('update');
}, 100);
} catch (e) {
console.error('[AuthTokensSettings] Ошибка при удалении токена:', e);
console.error('[AuthTokensSettings] Response:', e.response);

View File

@@ -77,6 +77,7 @@ import AuthTokensSettings from './AuthTokensSettings.vue';
import { useRouter } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import NoAccessModal from '@/components/NoAccessModal.vue';
import wsClient from '@/utils/websocket';
// Состояние для отображения/скрытия дополнительных настроек
const showRpcSettings = ref(false);
@@ -234,6 +235,22 @@ const saveSecuritySettings = async () => {
// Загрузка настроек при монтировании компонента
onMounted(() => {
loadSettings();
// Подписываемся на WebSocket события для обновления списка токенов
wsClient.onAuthTokenAdded(() => {
console.log('[SecuritySettingsView] WebSocket: токен добавлен, обновляем список');
loadSettings();
});
wsClient.onAuthTokenDeleted(() => {
console.log('[SecuritySettingsView] WebSocket: токен удален, обновляем список');
loadSettings();
});
wsClient.onAuthTokenUpdated(() => {
console.log('[SecuritySettingsView] WebSocket: токен обновлен, обновляем список');
loadSettings();
});
});
// --- Методы для RPC ---

View File

@@ -19,46 +19,42 @@
@auth-action-completed="$emit('auth-action-completed')"
>
<div class="dle-proposals-management">
<div class="proposals-header">
<div class="header-info">
<h3>🗳 Управление предложениями</h3>
<div v-if="selectedDle" class="dle-info">
<span class="dle-name">{{ selectedDle.name }} ({{ selectedDle.symbol }})</span>
<span class="dle-address">{{ shortenAddress(selectedDle.dleAddress) }}</span>
<!-- Заголовок в стиле настроек -->
<div class="page-header">
<div class="header-content">
<h1>Предложения DLE</h1>
<p v-if="selectedDle">{{ selectedDle.name }} ({{ selectedDle.symbol }}) - {{ selectedDle.dleAddress }}</p>
<p v-else-if="isLoadingDle">Загрузка...</p>
<p v-else>DLE не выбран</p>
</div>
<div v-else-if="isLoadingDle" class="loading-info">
<span>Загрузка данных DLE...</span>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<div v-else class="no-dle-info">
<span>DLE не выбран</span>
<!-- Фильтры и управление -->
<div class="controls-section">
<div class="controls-header">
<h3>Фильтры</h3>
</div>
<div class="controls-content">
<div class="filters-row">
<select v-model="statusFilter" class="form-control">
<option value="">Все статусы</option>
<option value="active">Активные</option>
<option value="pending">Ожидающие</option>
<option value="succeeded">Принятые</option>
<option value="defeated">Отклоненные</option>
<option value="executed">Выполненные</option>
</select>
<button
class="btn btn-sm btn-outline-secondary"
@click="loadDleData"
:disabled="isLoadingDle"
>
<i class="fas fa-sync-alt"></i> Обновить
</button>
</div>
</div>
</div>
</div>
<!-- Список предложений -->
<div class="proposals-list">
<div class="list-header">
<h4>📋 Список предложений</h4>
<div class="list-filters">
<select v-model="statusFilter" class="form-control">
<option value="">Все статусы</option>
<option value="active">Активные</option>
<option value="pending">Ожидающие</option>
<option value="succeeded">Принятые</option>
<option value="defeated">Отклоненные</option>
<option value="executed">Выполненные</option>
</select>
<button
class="btn btn-sm btn-outline-secondary"
@click="loadDleData"
:disabled="isLoadingDle"
>
<i class="fas fa-sync-alt"></i> Обновить
</button>
</div>
</div>
<div v-if="filteredProposals.length === 0" class="no-proposals">
<p>Предложений пока нет</p>
@@ -133,7 +129,7 @@
:disabled="hasSigned(proposal.id)"
>
<i class="fas fa-signature"></i> Подписать
</button>
</button>
<button
v-if="canVoteAgainst(proposal) && props.isAuthenticated && hasAdminRights()"
class="btn btn-sm btn-warning"
@@ -162,7 +158,7 @@
<i class="fas fa-lock"></i>
Для участия в голосовании необходимы права администратора
</small>
</div>
</div>
</div>
</div>
@@ -255,6 +251,9 @@
<option value="transfer">Передача токенов</option>
<option value="mint">Минтинг токенов</option>
<option value="burn">Сжигание токенов</option>
<option value="updateDLEInfo">Обновить данные DLE</option>
<option value="updateQuorum">Изменить кворум</option>
<option value="updateChain">Изменить текущую цепочку</option>
<option value="custom">Пользовательская операция</option>
</select>
</div>
@@ -351,6 +350,111 @@
></textarea>
</div>
</div>
<!-- Параметры для обновления данных DLE -->
<div v-if="newProposal.operationType === 'updateDLEInfo'" class="operation-params">
<div class="form-group">
<label for="dleName">Новое название DLE:</label>
<input
type="text"
id="dleName"
v-model="newProposal.operationParams.name"
class="form-control"
placeholder="Новое название"
>
</div>
<div class="form-group">
<label for="dleSymbol">Новый символ токена:</label>
<input
type="text"
id="dleSymbol"
v-model="newProposal.operationParams.symbol"
class="form-control"
placeholder="Новый символ"
>
</div>
<div class="form-group">
<label for="dleLocation">Новое местонахождение:</label>
<input
type="text"
id="dleLocation"
v-model="newProposal.operationParams.location"
class="form-control"
placeholder="Новое местонахождение"
>
</div>
<div class="form-group">
<label for="dleCoordinates">Новые координаты:</label>
<input
type="text"
id="dleCoordinates"
v-model="newProposal.operationParams.coordinates"
class="form-control"
placeholder="44.0422736,43.062124"
>
</div>
<div class="form-group">
<label for="dleJurisdiction">Новая юрисдикция:</label>
<input
type="number"
id="dleJurisdiction"
v-model.number="newProposal.operationParams.jurisdiction"
class="form-control"
placeholder="643"
>
</div>
<div class="form-group">
<label for="dleOktmo">Новый ОКТМО:</label>
<input
type="number"
id="dleOktmo"
v-model.number="newProposal.operationParams.oktmo"
class="form-control"
placeholder="45000000000"
>
</div>
<div class="form-group">
<label for="dleKpp">Новый КПП:</label>
<input
type="number"
id="dleKpp"
v-model.number="newProposal.operationParams.kpp"
class="form-control"
placeholder="770101001"
>
</div>
</div>
<!-- Параметры для изменения кворума -->
<div v-if="newProposal.operationType === 'updateQuorum'" class="operation-params">
<div class="form-group">
<label for="newQuorum">Новый процент кворума:</label>
<input
type="number"
id="newQuorum"
v-model.number="newProposal.operationParams.quorumPercentage"
class="form-control"
min="1"
max="100"
placeholder="51"
>
<small class="form-text text-muted">Процент от общего количества токенов (1-100%)</small>
</div>
</div>
<!-- Параметры для изменения текущей цепочки -->
<div v-if="newProposal.operationType === 'updateChain'" class="operation-params">
<div class="form-group">
<label for="newChainId">Новая текущая цепочка:</label>
<select id="newChainId" v-model="newProposal.operationParams.chainId" class="form-control">
<option value="">-- Выберите цепочку --</option>
<option v-for="chain in availableChains" :key="chain.chainId" :value="chain.chainId">
{{ chain.name }} ({{ chain.chainId }})
</option>
</select>
<small class="form-text text-muted">Выберите новую цепочку для управления DLE</small>
</div>
</div>
</div>
</div>
@@ -393,7 +497,6 @@
</div>
</div>
</div> <!-- Закрываем div для авторизованных пользователей -->
</div>
</div>
</BaseLayout>
</template>
@@ -405,6 +508,7 @@ import { useAuthContext } from '@/composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, loadProposals, createProposal as createProposalAPI, voteForProposal as voteForProposalAPI, executeProposal as executeProposalAPI, getSupportedChains } from '../../utils/dle-contract.js';
import wsClient from '../../utils/websocket.js';
import { ethers } from 'ethers';
const props = defineProps({
dleAddress: { type: String, required: false, default: null },
@@ -453,7 +557,15 @@ const newProposal = ref({
to: '',
from: '',
amount: 0,
customData: ''
customData: '',
name: '',
symbol: '',
location: '',
coordinates: '',
jurisdiction: 0,
oktmo: 0,
kpp: 0,
chainId: ''
}
});
@@ -546,6 +658,12 @@ function validateOperationParams() {
return validateAddress(params.from) && params.amount > 0;
case 'custom':
return params.customData && params.customData.startsWith('0x') && params.customData.length >= 10;
case 'updateDLEInfo':
return params.name && params.symbol && params.location && params.coordinates && params.jurisdiction && params.oktmo && params.kpp;
case 'updateQuorum':
return params.quorumPercentage >= 1 && params.quorumPercentage <= 100;
case 'updateChain':
return params.chainId && params.chainId !== '';
default:
return false;
}
@@ -580,7 +698,10 @@ function getOperationTypeName(type) {
'transfer': 'Передача токенов',
'mint': 'Минтинг токенов',
'burn': 'Сжигание токенов',
'custom': 'Пользовательская операция'
'custom': 'Пользовательская операция',
'updateDLEInfo': 'Обновить данные DLE',
'updateQuorum': 'Изменить кворум',
'updateChain': 'Изменить текущую цепочку'
};
return types[type] || 'Неизвестный тип';
}
@@ -597,6 +718,12 @@ function getOperationParamsPreview() {
return `От: ${shortenAddress(params.from)}, Количество: ${params.amount}`;
case 'custom':
return `Данные: ${params.customData.substring(0, 20)}...`;
case 'updateDLEInfo':
return `Название: ${params.name}, Символ: ${params.symbol}, Местонахождение: ${params.location}, Координаты: ${params.coordinates}, Юрисдикция: ${params.jurisdiction}, ОКТМО: ${params.oktmo}, КПП: ${params.kpp}`;
case 'updateQuorum':
return `Процент кворума: ${params.quorumPercentage}%`;
case 'updateChain':
return `Новая цепочка: ${getChainName(params.chainId) || 'Не выбрана'}`;
default:
return 'Не указаны';
}
@@ -620,8 +747,30 @@ function getProposalStatus(proposal) {
return 'executed';
}
// Проверяем, достигнут ли кворум
const quorumPercentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum();
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
// Добавляем отладочную информацию
console.log('[getProposalStatus] Проверка предложения:', {
proposalId: proposal.id,
now,
deadline,
deadlinePassed: deadline > 0 && now >= deadline,
quorumPercentage,
requiredQuorum,
hasReachedQuorum
});
// Если кворум достигнут, предложение можно выполнить
if (hasReachedQuorum) {
return 'succeeded';
}
// Если дедлайн истек, но кворум не достигнут
if (deadline > 0 && now >= deadline) {
return proposal.isPassed ? 'succeeded' : 'defeated';
return 'defeated';
}
return 'active';
@@ -754,7 +903,31 @@ function canSign(proposal) {
}
function canExecute(proposal) {
return proposal.status === 'succeeded' && !proposal.executed;
const now = Math.floor(Date.now() / 1000);
const deadline = proposal.deadline || 0;
// Предложение можно выполнить только если:
// 1. Дедлайн истек
// 2. Кворум достигнут
// 3. Предложение еще не выполнено
const quorumPercentage = getQuorumPercentage(proposal);
const requiredQuorum = getRequiredQuorum();
const hasReachedQuorum = quorumPercentage >= requiredQuorum;
const deadlinePassed = deadline > 0 && now >= deadline;
// Добавляем отладочную информацию
console.log('[canExecute] Проверка предложения:', {
proposalId: proposal.id,
quorumPercentage,
requiredQuorum,
hasReachedQuorum,
deadline,
now,
deadlinePassed,
executed: proposal.executed
});
return deadlinePassed && hasReachedQuorum && !proposal.executed;
}
function hasSigned(proposalId) {
@@ -859,6 +1032,12 @@ function encodeOperation() {
return encodeBurnOperation(params.from, params.amount);
case 'custom':
return params.customData;
case 'updateDLEInfo':
return encodeUpdateDLEInfoOperation(params.name, params.symbol, params.location, params.coordinates, params.jurisdiction, params.oktmo, params.kpp);
case 'updateQuorum':
return encodeUpdateQuorumOperation(params.quorumPercentage);
case 'updateChain':
return encodeUpdateChainOperation(params.chainId);
default:
throw new Error('Неизвестный тип операции');
}
@@ -888,6 +1067,42 @@ function encodeBurnOperation(from, amount) {
return selector + paddedAddress + paddedAmount;
}
function encodeUpdateDLEInfoOperation(name, symbol, location, coordinates, jurisdiction, oktmo, kpp) {
// Селектор для updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateDLEInfo(string,string,string,string,uint256,uint256,string[],uint256)')).slice(0, 10);
// Кодируем параметры
const abiCoder = new ethers.AbiCoder();
const encodedData = abiCoder.encode(
['string', 'string', 'string', 'string', 'uint256', 'uint256', 'string[]', 'uint256'],
[name, symbol, location, coordinates, jurisdiction, oktmo, [], kpp] // okvedCodes пока пустой массив
);
return selector + encodedData.slice(2);
}
function encodeUpdateQuorumOperation(quorumPercentage) {
// Селектор для updateQuorumPercentage(uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateQuorumPercentage(uint256)')).slice(0, 10);
// Кодируем параметр
const abiCoder = new ethers.AbiCoder();
const encodedData = abiCoder.encode(['uint256'], [quorumPercentage]);
return selector + encodedData.slice(2);
}
function encodeUpdateChainOperation(chainId) {
// Селектор для updateCurrentChainId(uint256)
const selector = '0x' + ethers.keccak256(ethers.toUtf8Bytes('updateCurrentChainId(uint256)')).slice(0, 10);
// Кодируем параметр
const abiCoder = new ethers.AbiCoder();
const encodedData = abiCoder.encode(['uint256'], [chainId]);
return selector + encodedData.slice(2);
}
// Подпись предложения
async function signProposalLocal(proposalId) {
// Проверка прав админа для голосования
@@ -994,7 +1209,15 @@ function resetForm() {
to: '',
from: '',
amount: 0,
customData: ''
customData: '',
name: '',
symbol: '',
location: '',
coordinates: '',
jurisdiction: 0,
oktmo: 0,
kpp: 0,
chainId: ''
}
};
}
@@ -1094,9 +1317,128 @@ onUnmounted(() => {
<style scoped>
.dle-proposals-management {
padding: 1rem;
padding: 20px;
background-color: var(--color-white);
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
margin-bottom: 20px;
}
/* Заголовок в стиле настроек */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.header-content {
flex-grow: 1;
}
.page-header h1 {
color: var(--color-primary);
font-size: 2rem;
margin: 0 0 5px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s;
flex-shrink: 0;
}
.close-btn:hover {
background: #f0f0f0;
color: #333;
}
/* Секция управления */
.controls-section {
background: white;
border: 1px solid #e9ecef;
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 20px;
}
.controls-header {
background: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
}
.controls-header h3 {
color: var(--color-primary);
margin: 0;
font-size: 1.2rem;
}
.controls-content {
padding: 20px;
}
.filters-row {
display: flex;
gap: 15px;
align-items: center;
}
.form-control {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 0.9rem;
min-width: 150px;
}
.btn {
border: none;
padding: 8px 16px;
border-radius: var(--radius-sm);
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.8rem;
}
.btn-outline-secondary {
background: transparent;
color: var(--color-secondary);
border: 1px solid var(--color-secondary);
}
.btn-outline-secondary:hover {
background: var(--color-secondary);
color: white;
}
/* Устаревшие стили для совместимости */
.proposals-header {
display: flex;
justify-content: space-between;

View File

@@ -22,291 +22,48 @@
<!-- Заголовок -->
<div class="page-header">
<div class="header-content">
<h1>Настройки</h1>
<p>Параметры DLE и конфигурация</p>
<h1>Настройки DLE</h1>
<p v-if="dleInfo">{{ dleInfo.name }} ({{ dleInfo.symbol }}) - {{ dleInfo.address }}</p>
<p v-else-if="address">Загрузка...</p>
<p v-else>DLE не выбран</p>
</div>
<button class="close-btn" @click="router.push('/management')">×</button>
</div>
<!-- Основные настройки -->
<div class="main-settings-section">
<h2>Основные настройки</h2>
<form @submit.prevent="saveMainSettings" class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="dleName">Название DLE:</label>
<input
id="dleName"
v-model="mainSettings.name"
type="text"
placeholder="Введите название DLE"
required
>
</div>
<div class="form-group">
<label for="dleSymbol">Символ токена:</label>
<input
id="dleSymbol"
v-model="mainSettings.symbol"
type="text"
placeholder="MDLE"
maxlength="10"
required
>
</div>
</div>
<div class="form-group">
<label for="dleDescription">Описание:</label>
<textarea
id="dleDescription"
v-model="mainSettings.description"
placeholder="Опишите назначение и деятельность DLE..."
rows="4"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="dleLocation">Местонахождение:</label>
<input
id="dleLocation"
v-model="mainSettings.location"
type="text"
placeholder="Страна, город"
required
>
</div>
<div class="form-group">
<label for="dleWebsite">Веб-сайт:</label>
<input
id="dleWebsite"
v-model="mainSettings.website"
type="url"
placeholder="https://example.com"
>
</div>
</div>
<button type="submit" class="btn-primary" :disabled="isSaving">
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки' }}
</button>
</form>
</div>
<!-- Настройки безопасности -->
<div class="security-settings-section">
<h2>Настройки безопасности</h2>
<form @submit.prevent="saveSecuritySettings" class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="minQuorum">Минимальный кворум (%):</label>
<input
id="minQuorum"
v-model="securitySettings.minQuorum"
type="number"
min="1"
max="100"
placeholder="51"
required
>
<span class="input-hint">Минимальный процент токенов для принятия решений</span>
</div>
<div class="form-group">
<label for="maxProposalDuration">Максимальная длительность предложения (дни):</label>
<input
id="maxProposalDuration"
v-model="securitySettings.maxProposalDuration"
type="number"
min="1"
max="365"
placeholder="7"
required
>
<span class="input-hint">Максимальное время жизни предложения</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="emergencyThreshold">Порог экстренных действий (%):</label>
<input
id="emergencyThreshold"
v-model="securitySettings.emergencyThreshold"
type="number"
min="1"
max="100"
placeholder="75"
required
>
<span class="input-hint">Процент для экстренных действий</span>
</div>
<div class="form-group">
<label for="timelockDelay">Задержка таймлока (часы):</label>
<input
id="timelockDelay"
v-model="securitySettings.timelockDelay"
type="number"
min="1"
max="168"
placeholder="24"
required
>
<span class="input-hint">Минимальная задержка перед выполнением</span>
</div>
</div>
<div class="form-group">
<label>Дополнительные настройки:</label>
<div class="checkbox-group">
<label class="checkbox-item">
<input
type="checkbox"
v-model="securitySettings.allowDelegation"
>
Разрешить делегирование голосов
</label>
<label class="checkbox-item">
<input
type="checkbox"
v-model="securitySettings.requireKYC"
>
Требовать KYC для участия
</label>
<label class="checkbox-item">
<input
type="checkbox"
v-model="securitySettings.autoExecute"
>
Автоматическое выполнение предложений
</label>
</div>
</div>
<button type="submit" class="btn-primary" :disabled="isSaving">
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки безопасности' }}
</button>
</form>
</div>
<!-- Настройки сети -->
<div class="network-settings-section">
<h2>Настройки сети</h2>
<form @submit.prevent="saveNetworkSettings" class="settings-form">
<div class="form-group">
<label>Поддерживаемые сети:</label>
<div class="networks-grid">
<label
v-for="network in availableNetworks"
:key="network.id"
class="network-checkbox"
>
<input
type="checkbox"
:value="network.id"
v-model="networkSettings.enabledNetworks"
>
<div class="network-info">
<span class="network-name">{{ network.name }}</span>
<span class="network-chain-id">Chain ID: {{ network.chainId }}</span>
</div>
</label>
</div>
</div>
<div class="form-group">
<label for="defaultNetwork">Сеть по умолчанию:</label>
<select id="defaultNetwork" v-model="networkSettings.defaultNetwork" required>
<option value="">Выберите сеть</option>
<option
v-for="network in availableNetworks"
:key="network.id"
:value="network.id"
>
{{ network.name }}
</option>
</select>
</div>
<div class="form-group">
<label for="rpcEndpoint">RPC Endpoint:</label>
<input
id="rpcEndpoint"
v-model="networkSettings.rpcEndpoint"
type="url"
placeholder="https://mainnet.infura.io/v3/YOUR_PROJECT_ID"
>
<span class="input-hint">RPC endpoint для подключения к блокчейну</span>
</div>
<button type="submit" class="btn-primary" :disabled="isSaving">
{{ isSaving ? 'Сохранение...' : 'Сохранить настройки сети' }}
</button>
</form>
</div>
<!-- Резервное копирование -->
<div class="backup-section">
<h2>Резервное копирование</h2>
<div class="backup-actions">
<div class="backup-card">
<h3>Экспорт настроек</h3>
<p>Скачайте файл с настройками DLE для резервного копирования</p>
<button @click="exportSettings" class="btn-secondary">
Экспортировать
</button>
</div>
<div class="backup-card">
<h3>Импорт настроек</h3>
<p>Загрузите файл с настройками для восстановления</p>
<input
ref="importFile"
type="file"
accept=".json"
@change="importSettings"
style="display: none"
>
<button @click="$refs.importFile.click()" class="btn-secondary">
Импортировать
</button>
</div>
</div>
</div>
<!-- Опасная зона -->
<div class="danger-zone-section">
<h2>Опасная зона</h2>
<div class="danger-actions">
<div class="danger-card">
<h3>Сброс настроек</h3>
<p>Вернуть все настройки к значениям по умолчанию</p>
<button @click="resetSettings" class="btn-danger">
Сбросить настройки
</button>
</div>
<div class="danger-card">
<!-- Основной контент -->
<div v-if="dleInfo" class="main-content">
<!-- Удаление DLE -->
<div class="danger-card">
<div class="danger-header">
<h3>Удаление DLE</h3>
<p>Полное удаление DLE и всех связанных данных</p>
<button @click="deleteDLE" class="btn-danger">
Удалить DLE
</div>
<div class="danger-content">
<p>Полное удаление DLE и всех связанных данных. Это действие необратимо.</p>
<button @click="deleteDLE" class="btn-danger" :disabled="isLoading">
{{ isLoading ? 'Загрузка...' : 'Удалить DLE' }}
</button>
</div>
</div>
</div>
<!-- Сообщение если DLE не выбран -->
<div v-if="!address" class="no-dle-card">
<h3>DLE не выбран</h3>
<p>Для управления настройками необходимо выбрать DLE</p>
<button @click="router.push('/management')" class="btn-primary">
Вернуться к списку DLE
</button>
</div>
</div>
</BaseLayout>
</template>
<script setup>
import { ref, defineProps, defineEmits } from 'vue';
import { useRouter } from 'vue-router';
import { ref, defineProps, defineEmits, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAuthContext } from '@/composables/useAuth';
import BaseLayout from '../../components/BaseLayout.vue';
import { getDLEInfo, deactivateDLE } from '../../utils/dle-contract.js';
// Определяем props
const props = defineProps({
@@ -320,196 +77,117 @@ const props = defineProps({
const emit = defineEmits(['auth-action-completed']);
const router = useRouter();
const route = useRoute();
// Состояние
const isSaving = ref(false);
const dleAddress = ref('');
const dleInfo = ref(null);
const isLoading = ref(false);
// Основные настройки
const mainSettings = ref({
name: 'Мое DLE',
symbol: 'MDLE',
description: 'Цифровое юридическое лицо для управления активами и принятия решений',
location: 'Россия, Москва',
website: 'https://example.com'
});
// Получаем адрес DLE из URL параметров
const address = route.query.address || props.dleAddress;
// Настройки безопасности
const securitySettings = ref({
minQuorum: 51,
maxProposalDuration: 7,
emergencyThreshold: 75,
timelockDelay: 24,
allowDelegation: true,
requireKYC: false,
autoExecute: false
});
// Получаем адрес пользователя из контекста аутентификации
const { address: userAddress } = useAuthContext();
// Настройки сети
const networkSettings = ref({
enabledNetworks: [1, 137],
defaultNetwork: 1,
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
});
// Загружаем информацию о DLE
const loadDLEInfo = async () => {
if (!address) {
console.error('Адрес DLE не указан');
return;
}
// Доступные сети (загружаются из конфигурации)
const availableNetworks = ref([]);
try {
isLoading.value = true;
console.log('Загружаем информацию о DLE:', address);
// Загружаем данные DLE из блокчейна через API
const dleData = await getDLEInfo(address);
console.log('Загружены данные DLE из блокчейна:', dleData);
dleInfo.value = {
name: dleData.name, // Название DLE из блокчейна
symbol: dleData.symbol, // Символ DLE из блокчейна
address: dleData.dleAddress || address // Адрес из API или из URL
};
} catch (error) {
console.error('Ошибка при загрузке информации о DLE:', error);
// В случае ошибки показываем базовую информацию
dleInfo.value = {
name: 'DLE ' + address.slice(0, 8) + '...',
symbol: 'DLE',
address: address
};
} finally {
isLoading.value = false;
}
};
// Методы
const saveMainSettings = async () => {
if (isSaving.value) return;
const deleteDLE = async () => {
if (!address) {
alert('Адрес DLE не найден');
return;
}
// Проверяем аутентификацию
if (!props.isAuthenticated || !userAddress.value) {
alert('❌ Для удаления DLE необходимо авторизоваться в приложении');
return;
}
if (!confirm(`ВНИМАНИЕ! Это действие необратимо. Вы уверены, что хотите деактивировать DLE ${dleInfo.value?.name || address}?`)) {
return;
}
if (!confirm('Это действие нельзя отменить. Подтвердите деактивацию еще раз.')) {
return;
}
try {
isSaving.value = true;
console.log('Деактивация DLE:', address);
// Здесь будет логика сохранения основных настроек
// console.log('Сохранение основных настроек:', mainSettings.value);
// Выполняем деактивацию DLE
const result = await deactivateDLE(address, userAddress.value);
// Временная логика
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Результат деактивации:', result);
alert('Основные настройки успешно сохранены!');
alert(`✅ DLE ${dleInfo.value?.name || address} успешно деактивирован!\n\nТранзакция: ${result.txHash}`);
// Перенаправляем на главную страницу управления
router.push('/management');
} catch (error) {
// console.error('Ошибка сохранения основных настроек:', error);
alert('Ошибка при сохранении настроек');
} finally {
isSaving.value = false;
}
};
const saveSecuritySettings = async () => {
if (isSaving.value) return;
try {
isSaving.value = true;
console.error('Ошибка при деактивации DLE:', error);
// Здесь будет логика сохранения настроек безопасности
// console.log('Сохранение настроек безопасности:', securitySettings.value);
let errorMessage = 'Ошибка при деактивации DLE';
// Временная логика
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Настройки безопасности успешно сохранены!');
} catch (error) {
// console.error('Ошибка сохранения настроек безопасности:', error);
alert('Ошибка при сохранении настроек безопасности');
} finally {
isSaving.value = false;
}
};
const saveNetworkSettings = async () => {
if (isSaving.value) return;
try {
isSaving.value = true;
// Здесь будет логика сохранения настроек сети
// console.log('Сохранение настроек сети:', networkSettings.value);
// Временная логика
await new Promise(resolve => setTimeout(resolve, 1000));
alert('Настройки сети успешно сохранены!');
} catch (error) {
// console.error('Ошибка сохранения настроек сети:', error);
alert('Ошибка при сохранении настроек сети');
} finally {
isSaving.value = false;
}
};
const exportSettings = () => {
const settings = {
main: mainSettings.value,
security: securitySettings.value,
network: networkSettings.value,
exportDate: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `dle-settings-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
alert('Настройки успешно экспортированы!');
};
const importSettings = (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const settings = JSON.parse(e.target.result);
if (settings.main) mainSettings.value = settings.main;
if (settings.security) securitySettings.value = settings.security;
if (settings.network) networkSettings.value = settings.network;
alert('Настройки успешно импортированы!');
} catch (error) {
// console.error('Ошибка импорта настроек:', error);
alert('Ошибка при импорте настроек');
if (error.message.includes('владелец')) {
errorMessage = '❌ Только владелец DLE может его деактивировать';
} else if (error.message.includes('кошелек')) {
errorMessage = '❌ Необходимо подключить кошелек';
} else if (error.message.includes('деактивирован')) {
errorMessage = '❌ DLE уже деактивирован';
} else {
errorMessage = `❌ Ошибка: ${error.message}`;
}
};
reader.readAsText(file);
alert(errorMessage);
} finally {
isSaving.value = false;
}
};
const resetSettings = () => {
if (!confirm('Вы уверены, что хотите сбросить все настройки к значениям по умолчанию?')) {
return;
// Загружаем данные при монтировании компонента
onMounted(() => {
if (address) {
dleAddress.value = address;
loadDLEInfo();
}
// Сброс к значениям по умолчанию
mainSettings.value = {
name: 'Мое DLE',
symbol: 'MDLE',
description: 'Цифровое юридическое лицо для управления активами и принятия решений',
location: 'Россия, Москва',
website: 'https://example.com'
};
securitySettings.value = {
minQuorum: 51,
maxProposalDuration: 7,
emergencyThreshold: 75,
timelockDelay: 24,
allowDelegation: true,
requireKYC: false,
autoExecute: false
};
networkSettings.value = {
enabledNetworks: [1, 137],
defaultNetwork: 1,
rpcEndpoint: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
};
alert('Настройки сброшены к значениям по умолчанию!');
};
const deleteDLE = () => {
if (!confirm('ВНИМАНИЕ! Это действие необратимо. Вы уверены, что хотите удалить DLE и все связанные данные?')) {
return;
}
if (!confirm('Это действие нельзя отменить. Подтвердите удаление еще раз.')) {
return;
}
// Здесь будет логика удаления DLE
// console.log('Удаление DLE...');
alert('DLE будет удален. Это действие может занять некоторое время.');
};
});
</script>
<style scoped>
@@ -525,10 +203,10 @@ const deleteDLE = () => {
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #f0f0f0;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.header-content {
@@ -537,13 +215,13 @@ const deleteDLE = () => {
.page-header h1 {
color: var(--color-primary);
font-size: 2.5rem;
margin: 0 0 10px 0;
font-size: 2rem;
margin: 0 0 5px 0;
}
.page-header p {
color: var(--color-grey-dark);
font-size: 1.1rem;
font-size: 1rem;
margin: 0;
}
@@ -569,261 +247,92 @@ const deleteDLE = () => {
color: #333;
}
/* Секции */
.main-settings-section,
.security-settings-section,
.network-settings-section,
.backup-section,
.danger-zone-section {
margin-bottom: 40px;
}
.main-settings-section h2,
.security-settings-section h2,
.network-settings-section h2,
.backup-section h2,
.danger-zone-section h2 {
color: var(--color-primary);
margin-bottom: 20px;
font-size: 1.8rem;
}
/* Формы настроек */
.settings-form {
background: #f8f9fa;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
}
.form-row {
/* Основной контент */
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.form-group label {
font-weight: 600;
color: var(--color-grey-dark);
}
.form-group input,
.form-group select,
.form-group textarea {
padding: 12px;
border: 1px solid #ddd;
border-radius: var(--radius-sm);
font-size: 1rem;
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.input-hint {
font-size: 0.8rem;
color: var(--color-grey-dark);
margin-top: 4px;
}
/* Чекбоксы */
.checkbox-group {
display: grid;
gap: 15px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
padding: 10px;
border-radius: var(--radius-sm);
transition: background 0.2s;
}
.checkbox-item:hover {
background: #e9ecef;
}
.checkbox-item input[type="checkbox"] {
width: auto;
}
/* Сети */
.networks-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.network-checkbox {
display: flex;
align-items: center;
gap: 15px;
cursor: pointer;
padding: 15px;
background: white;
border-radius: var(--radius-sm);
border: 1px solid #e9ecef;
transition: all 0.3s ease;
}
.network-checkbox:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.network-checkbox input[type="checkbox"] {
width: auto;
}
.network-info {
display: flex;
flex-direction: column;
gap: 5px;
}
.network-name {
font-weight: 600;
color: var(--color-primary);
}
.network-chain-id {
font-size: 0.8rem;
color: var(--color-grey-dark);
}
/* Резервное копирование */
.backup-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.backup-card {
background: white;
padding: 25px;
border-radius: var(--radius-lg);
border: 1px solid #e9ecef;
text-align: center;
}
.backup-card h3 {
color: var(--color-primary);
margin-bottom: 15px;
font-size: 1.2rem;
}
.backup-card p {
color: var(--color-grey-dark);
margin-bottom: 20px;
line-height: 1.5;
}
/* Опасная зона */
.danger-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
/* Карточки */
.danger-card {
background: #fff5f5;
padding: 25px;
background: white;
border: 1px solid #e9ecef;
border-radius: var(--radius-lg);
border: 1px solid #fed7d7;
text-align: center;
overflow: hidden;
}
.danger-card h3 {
.danger-header {
background: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
}
.danger-header h3 {
color: #c53030;
margin-bottom: 15px;
margin: 0;
font-size: 1.2rem;
}
.danger-card p {
color: var(--color-grey-dark);
margin-bottom: 20px;
line-height: 1.5;
.danger-content {
padding: 20px;
}
/* Кнопки */
.btn-primary,
.btn-danger {
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--color-primary);
color: white;
border: none;
padding: 12px 24px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
.btn-primary:hover {
background: var(--color-primary-dark);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: var(--color-secondary);
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: var(--color-secondary-dark);
transform: translateY(-1px);
}
.btn-danger {
background: #dc3545;
background: #e53e3e;
color: white;
border: none;
padding: 10px 20px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-danger:hover {
background: #c82333;
background: #c53030;
transform: translateY(-1px);
}
/* Адаптивность */
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.networks-grid {
grid-template-columns: 1fr;
}
.backup-actions,
.danger-actions {
grid-template-columns: 1fr;
}
.btn-primary:active,
.btn-danger:active {
transform: translateY(0);
}
/* Сообщение если DLE не выбран */
.no-dle-card {
background: #fff5f5;
border: 2px solid #fed7d7;
border-radius: var(--radius-lg);
padding: 30px;
text-align: center;
}
.no-dle-card h3 {
color: #c53030;
margin-bottom: 10px;
font-size: 1.2rem;
}
.no-dle-card p {
color: #4a5568;
margin-bottom: 15px;
line-height: 1.5;
font-size: 0.9rem;
}
</style>