403 lines
16 KiB
JavaScript
403 lines
16 KiB
JavaScript
import $ from 'jquery';
|
||
import { JSONRPC } from '@/json-rpc.js';
|
||
|
||
const FIELD_IDS = {
|
||
container: 'unlock-site-container',
|
||
form: 'unlock-site-form',
|
||
domainInput: 'unlock-site-domain',
|
||
serverInput: 'unlock-site-server',
|
||
searchButton: 'unlock-site-search',
|
||
results: 'unlock-site-results',
|
||
status: 'unlock-site-status',
|
||
addressesCheckbox: 'unlock-site-addresses-checkbox',
|
||
subnetsCheckbox: 'unlock-site-subnets-checkbox',
|
||
asnCheckbox: 'unlock-site-asn-checkbox',
|
||
nameInput: 'unlock-site-name',
|
||
storageSelect: 'unlock-site-storage',
|
||
addButton: 'unlock-site-add-button',
|
||
nameError: 'unlock-site-name-error',
|
||
domainError: 'unlock-site-domain-error',
|
||
serverError: 'unlock-site-server-error'
|
||
};
|
||
|
||
const SELECTORS = Object.entries(FIELD_IDS).reduce((acc, [key, value]) => {
|
||
acc[key] = `#${value}`;
|
||
return acc;
|
||
}, {});
|
||
|
||
const DEFAULT_DOMAIN = 'google.com';
|
||
const DEFAULT_SERVER = '8.8.8.8';
|
||
|
||
let currentResults = { addresses: [], subnets: [], ASN: [] };
|
||
let availableStorages = [];
|
||
let currentStorage = '';
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function normalizeListing(values) {
|
||
if (!Array.isArray(values)) {
|
||
return [];
|
||
}
|
||
return values
|
||
.map(item => (item === null || item === undefined) ? '' : String(item))
|
||
.filter(item => item.length > 0);
|
||
}
|
||
|
||
async function loadLocalRepositories() {
|
||
try {
|
||
const repositories = await JSONRPC.SubscriptionManager.getLocalRepositories();
|
||
return normalizeListing(repositories);
|
||
} catch (error) {
|
||
console.error('Ошибка получения списка хранилищ:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function setStatus(message, type) {
|
||
const $status = $(SELECTORS.status);
|
||
if (!$status.length) {
|
||
return;
|
||
}
|
||
if (!message) {
|
||
$status.hide().text('').removeClass('success-message error-message');
|
||
return;
|
||
}
|
||
$status
|
||
.removeClass('success-message error-message')
|
||
.addClass(type === 'success' ? 'success-message' : 'error-message')
|
||
.text(message)
|
||
.show();
|
||
}
|
||
|
||
function showFieldError(selector, message) {
|
||
const $error = $(selector);
|
||
if (!$error.length) {
|
||
return;
|
||
}
|
||
$error.text(message || '').toggle(!!message);
|
||
}
|
||
|
||
function clearFieldError(selector) {
|
||
showFieldError(selector, '');
|
||
}
|
||
|
||
function validateDomain(value) {
|
||
const trimmed = value.trim();
|
||
if (!trimmed) {
|
||
return { valid: false, message: 'Укажите домен.' };
|
||
}
|
||
const domainPattern = /^(?=.{1,253}$)([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,}$/;
|
||
if (!domainPattern.test(trimmed)) {
|
||
return { valid: false, message: 'Некорректный домен.' };
|
||
}
|
||
return { valid: true, message: '' };
|
||
}
|
||
|
||
function validateServer(value) {
|
||
const trimmed = value.trim();
|
||
if (!trimmed) {
|
||
return { valid: false, message: 'Укажите DNS сервер.' };
|
||
}
|
||
const ipv4Pattern = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
|
||
const domainPattern = /^(?=.{1,253}$)([A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,}$/;
|
||
if (!ipv4Pattern.test(trimmed) && !domainPattern.test(trimmed)) {
|
||
return { valid: false, message: 'Введите IP или домен DNS сервера.' };
|
||
}
|
||
return { valid: true, message: '' };
|
||
}
|
||
|
||
function validateListName(value) {
|
||
const trimmed = value.trim();
|
||
if (!trimmed) {
|
||
return { valid: false, message: 'Имя списка не может быть пустым.' };
|
||
}
|
||
const namePattern = /^[A-Za-z0-9.]+$/;
|
||
if (!namePattern.test(trimmed)) {
|
||
return { valid: false, message: 'Допустимы только символы A-Z, a-z, 0-9 и точка.' };
|
||
}
|
||
return { valid: true, message: '' };
|
||
}
|
||
|
||
function buildListItems(items) {
|
||
if (!items.length) {
|
||
return '<div class="hint-text" style="margin-top: 12px;">Список пуст.</div>';
|
||
}
|
||
return `<div style="margin-top: 12px; display: flex; flex-direction: column; gap: 6px;">${items.map(item => `<div>${escapeHtml(item)}</div>`).join('')}</div>`;
|
||
}
|
||
|
||
function buildAsnItems(items) {
|
||
if (!items.length) {
|
||
return '<div class="hint-text" style="margin-top: 12px;">Список пуст.</div>';
|
||
}
|
||
return `<div style="margin-top: 12px;">${items.map(item => escapeHtml(item)).join(', ')}</div>`;
|
||
}
|
||
|
||
function renderResults(domainValue, storages) {
|
||
const addresses = normalizeListing(currentResults.addresses);
|
||
const subnets = normalizeListing(currentResults.subnets);
|
||
const ASN = normalizeListing(currentResults.ASN);
|
||
const storageOptions = Array.isArray(storages) ? storages : [];
|
||
|
||
if (!storageOptions.includes(currentStorage)) {
|
||
currentStorage = storageOptions.length ? storageOptions[0] : '';
|
||
}
|
||
|
||
const storageSelectDisabled = storageOptions.length === 0;
|
||
const storageOptionsHtml = storageOptions.length
|
||
? storageOptions.map(storage => `<option value="${escapeHtml(storage)}"${storage === currentStorage ? ' selected' : ''}>${escapeHtml(storage)}</option>`).join('')
|
||
: '<option value="">Нет доступных хранилищ</option>';
|
||
const storageHint = storageOptions.length ? '' : '<div class="hint-text" style="margin-top: 8px;">Не найдено доступных хранилищ.</div>';
|
||
|
||
const addressesDisabled = addresses.length === 0;
|
||
const subnetsDisabled = subnets.length === 0;
|
||
const asnDisabled = ASN.length === 0;
|
||
|
||
const html = `
|
||
<div style="margin-top: 24px; display: flex; flex-direction: column; gap: 16px;">
|
||
<div class="card" style="padding: 20px;">
|
||
<label style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="checkbox" id="${FIELD_IDS.addressesCheckbox}" ${addressesDisabled ? 'disabled' : ''}>
|
||
<span>Добавить обнаруженные адреса</span>
|
||
</label>
|
||
${buildListItems(addresses)}
|
||
</div>
|
||
<div class="card" style="padding: 20px;">
|
||
<label style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="checkbox" id="${FIELD_IDS.subnetsCheckbox}" ${subnetsDisabled ? 'disabled' : ''}>
|
||
<span>Добавить подсети, содержащие обнаруженные адреса</span>
|
||
</label>
|
||
${buildListItems(subnets)}
|
||
</div>
|
||
<div class="card" style="padding: 20px;">
|
||
<label style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="checkbox" id="${FIELD_IDS.asnCheckbox}" ${asnDisabled ? 'disabled' : ''}>
|
||
<span>Добавить ASN, содержащие обнаруженные подсети</span>
|
||
</label>
|
||
${buildAsnItems(ASN)}
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="${FIELD_IDS.storageSelect}">Хранилище</label>
|
||
<select id="${FIELD_IDS.storageSelect}" class="form-control" ${storageSelectDisabled ? 'disabled' : ''}>
|
||
${storageOptionsHtml}
|
||
</select>
|
||
${storageHint}
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="${FIELD_IDS.nameInput}">Имя списка</label>
|
||
<input type="text" id="${FIELD_IDS.nameInput}" class="form-control" value="${escapeHtml(domainValue)}">
|
||
<div id="${FIELD_IDS.nameError}" class="error-message" style="display: none;"></div>
|
||
</div>
|
||
<div>
|
||
<button type="button" id="${FIELD_IDS.addButton}" class="btn-primary" disabled>Добавить</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const $results = $(SELECTORS.results);
|
||
$results.html(html).show();
|
||
}
|
||
|
||
function updateAddButtonState() {
|
||
const $addButton = $(SELECTORS.addButton);
|
||
if (!$addButton.length) {
|
||
return;
|
||
}
|
||
const $nameInput = $(SELECTORS.nameInput);
|
||
const $storageSelect = $(SELECTORS.storageSelect);
|
||
const { valid } = validateListName($nameInput.val() || '');
|
||
const hasSelection = [
|
||
$(SELECTORS.addressesCheckbox),
|
||
$(SELECTORS.subnetsCheckbox),
|
||
$(SELECTORS.asnCheckbox)
|
||
].some($checkbox => $checkbox.length && !$checkbox.prop('disabled') && $checkbox.prop('checked'));
|
||
const storageValue = $storageSelect.length && !$storageSelect.prop('disabled') ? ($storageSelect.val() || '') : '';
|
||
$addButton.prop('disabled', !(hasSelection && valid && storageValue.length > 0));
|
||
}
|
||
|
||
async function handleSearch(event) {
|
||
event.preventDefault();
|
||
setStatus('', '');
|
||
const $domainInput = $(SELECTORS.domainInput);
|
||
const $serverInput = $(SELECTORS.serverInput);
|
||
|
||
const domainValidation = validateDomain($domainInput.val() || '');
|
||
const serverValidation = validateServer($serverInput.val() || '');
|
||
|
||
if (!domainValidation.valid) {
|
||
showFieldError(SELECTORS.domainError, domainValidation.message);
|
||
} else {
|
||
clearFieldError(SELECTORS.domainError);
|
||
}
|
||
|
||
if (!serverValidation.valid) {
|
||
showFieldError(SELECTORS.serverError, serverValidation.message);
|
||
} else {
|
||
clearFieldError(SELECTORS.serverError);
|
||
}
|
||
|
||
if (!domainValidation.valid || !serverValidation.valid) {
|
||
return;
|
||
}
|
||
|
||
const $button = $(SELECTORS.searchButton);
|
||
$button.prop('disabled', true).text('Поиск...');
|
||
$(SELECTORS.results).hide().empty();
|
||
|
||
try {
|
||
const result = await JSONRPC.NetworkManager.discoverBlockedDomainResources(
|
||
$domainInput.val().trim(),
|
||
$serverInput.val().trim()
|
||
);
|
||
currentResults = {
|
||
addresses: normalizeListing(result?.addresses),
|
||
subnets: normalizeListing(result?.subnets),
|
||
ASN: normalizeListing(result?.ASN)
|
||
};
|
||
availableStorages = await loadLocalRepositories();
|
||
renderResults($domainInput.val().trim(), availableStorages);
|
||
updateAddButtonState();
|
||
clearFieldError(SELECTORS.nameError);
|
||
} catch (error) {
|
||
console.error('Ошибка поиска ресурсов домена:', error);
|
||
currentResults = { addresses: [], subnets: [], ASN: [] };
|
||
setStatus('Не удалось получить данные. Попробуйте позже.', 'error');
|
||
} finally {
|
||
$button.prop('disabled', false).text('Найти и разблокировать');
|
||
}
|
||
}
|
||
|
||
function handleAdd() {
|
||
const $nameInput = $(SELECTORS.nameInput);
|
||
const validation = validateListName($nameInput.val() || '');
|
||
if (!validation.valid) {
|
||
showFieldError(SELECTORS.nameError, validation.message);
|
||
updateAddButtonState();
|
||
return;
|
||
}
|
||
showFieldError(SELECTORS.nameError, '');
|
||
|
||
const $addButton = $(SELECTORS.addButton);
|
||
$addButton.prop('disabled', true).text('Добавление...');
|
||
setStatus('', '');
|
||
|
||
const domain = ($(SELECTORS.domainInput).val() || '').trim();
|
||
const selectedAddresses = $(SELECTORS.addressesCheckbox).prop('checked') ? currentResults.addresses : [];
|
||
const selectedSubnets = $(SELECTORS.subnetsCheckbox).prop('checked') ? currentResults.subnets : [];
|
||
const selectedAsn = $(SELECTORS.asnCheckbox).prop('checked') ? currentResults.ASN : [];
|
||
const storage = $(SELECTORS.storageSelect).val() || '';
|
||
|
||
if (!storage) {
|
||
setStatus('Выберите хранилище.', 'error');
|
||
$addButton.prop('disabled', false).text('Добавить');
|
||
updateAddButtonState();
|
||
return;
|
||
}
|
||
|
||
JSONRPC.SubscriptionManager.writeLocalResourceFile(
|
||
$nameInput.val().trim(),
|
||
"Unlocked resource: " + $nameInput.val().trim() + " (" + domain + ")",
|
||
[domain],
|
||
selectedAsn,
|
||
selectedSubnets,
|
||
selectedAddresses,
|
||
storage,
|
||
true
|
||
).then(() => {
|
||
setStatus('Ресурсы успешно добавлены.', 'success');
|
||
}).catch(error => {
|
||
console.error('Ошибка добавления ресурсов:', error);
|
||
setStatus('Не удалось добавить ресурсы.', 'error');
|
||
}).finally(() => {
|
||
$addButton.prop('disabled', false).text('Добавить');
|
||
updateAddButtonState();
|
||
});
|
||
}
|
||
|
||
function handleCheckboxChange() {
|
||
updateAddButtonState();
|
||
}
|
||
|
||
function handleNameInputChange() {
|
||
const $nameInput = $(SELECTORS.nameInput);
|
||
const validation = validateListName($nameInput.val() || '');
|
||
if (!validation.valid) {
|
||
showFieldError(SELECTORS.nameError, validation.message);
|
||
} else {
|
||
clearFieldError(SELECTORS.nameError);
|
||
}
|
||
updateAddButtonState();
|
||
}
|
||
|
||
function handleStorageChange() {
|
||
currentStorage = $(SELECTORS.storageSelect).val() || '';
|
||
updateAddButtonState();
|
||
}
|
||
|
||
function attachEventHandlers() {
|
||
$(SELECTORS.form).on('submit', handleSearch);
|
||
$(SELECTORS.domainInput).on('input', () => clearFieldError(SELECTORS.domainError));
|
||
$(SELECTORS.serverInput).on('input', () => clearFieldError(SELECTORS.serverError));
|
||
$(SELECTORS.results).on('change', `input[type="checkbox"]`, handleCheckboxChange);
|
||
$(SELECTORS.results).on('change', SELECTORS.storageSelect, handleStorageChange);
|
||
$(SELECTORS.results).on('input', SELECTORS.nameInput, handleNameInputChange);
|
||
$(SELECTORS.results).on('click', SELECTORS.addButton, handleAdd);
|
||
}
|
||
|
||
function detachEventHandlers() {
|
||
$(SELECTORS.form).off('submit', handleSearch);
|
||
$(SELECTORS.domainInput).off('input');
|
||
$(SELECTORS.serverInput).off('input');
|
||
$(SELECTORS.results).off('change');
|
||
$(SELECTORS.results).off('input');
|
||
$(SELECTORS.results).off('click');
|
||
}
|
||
|
||
function resetState() {
|
||
currentResults = { addresses: [], subnets: [], ASN: [] };
|
||
currentStorage = '';
|
||
availableStorages = [];
|
||
setStatus('', '');
|
||
}
|
||
|
||
export const UnlockSitePage = {
|
||
render: () => `
|
||
<div>
|
||
<h1 class="page-title">Разблокировка сайта</h1>
|
||
<div id="${FIELD_IDS.container}" class="card" style="padding: 24px; max-width: 640px; margin: 0 auto;">
|
||
<form id="${FIELD_IDS.form}" style="display: flex; flex-direction: column; gap: 16px;">
|
||
<div class="form-group">
|
||
<label for="${FIELD_IDS.domainInput}">Домен</label>
|
||
<input type="text" id="${FIELD_IDS.domainInput}" class="form-control" value="${DEFAULT_DOMAIN}">
|
||
<div id="${FIELD_IDS.domainError}" class="error-message" style="display: none;"></div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="${FIELD_IDS.serverInput}">Искать через DNS сервер</label>
|
||
<input type="text" id="${FIELD_IDS.serverInput}" class="form-control" value="${DEFAULT_SERVER}">
|
||
<div id="${FIELD_IDS.serverError}" class="error-message" style="display: none;"></div>
|
||
</div>
|
||
<div>
|
||
<button type="submit" id="${FIELD_IDS.searchButton}" class="btn-primary">Найти и разблокировать</button>
|
||
</div>
|
||
</form>
|
||
<div id="${FIELD_IDS.results}" style="display: none;"></div>
|
||
<div id="${FIELD_IDS.status}" class="error-message" style="display: none; margin-top: 20px;"></div>
|
||
</div>
|
||
</div>
|
||
`,
|
||
mount: () => {
|
||
attachEventHandlers();
|
||
},
|
||
unmount: () => {
|
||
detachEventHandlers();
|
||
resetState();
|
||
$(SELECTORS.results).empty().hide();
|
||
}
|
||
};
|