pf-sdn/webui/src/pages/UnlockSite.js

403 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
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();
}
};