ваше сообщение коммита
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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 клиента
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user