v0.5.12: доделки перед сдачей заказчику
§4.7: агрегация CIDR в публичном /export (aggregateCIDRs) §5: клиентская JS-валидация (запрещённые диапазоны + нормализация) §4.1: фильтр «показать удалённые» для admin (API + UI) §5: уведомление о нормализации (уже работало, проверено) Файлы: - server.js: +aggregateCIDRs в /export - views/index.ejs: JS-валидация + чек-бокс удалённых - src/api/routes/entries.js: ?includeDeleted=true (admin only) - ui/routes/entries.js: проброс includeDeleted - docs/PLAN-predsdachey.md: план доделок
This commit is contained in:
+103
@@ -198,6 +198,11 @@
|
||||
style="font-size:.82rem;color:var(--muted);text-decoration:none;">→ Аудит этой компании</a>
|
||||
<a href="/admin"
|
||||
style="font-size:.82rem;color:var(--muted);text-decoration:none;">→ Лимиты</a>
|
||||
<label style="font-size:.82rem;color:var(--muted);cursor:pointer;display:flex;align-items:center;gap:.3rem;">
|
||||
<input type="checkbox" name="includeDeleted" value="true" <%= (typeof includeDeleted !== 'undefined' && includeDeleted) ? 'checked' : '' %>
|
||||
onchange="this.form.submit()">
|
||||
Показать удалённые
|
||||
</label>
|
||||
<span style="margin-left:auto;font-size:.8rem;color:var(--muted);">
|
||||
Режим: <strong style="color:var(--amber);">Администратор</strong>
|
||||
</span>
|
||||
@@ -437,6 +442,104 @@
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
|
||||
// ── Клиентская валидация IP (ТЗ §5) ──────────────────────
|
||||
var BLOCKED = [
|
||||
{ mask:'10.0.0.0/8', name:'Private (RFC1918)' },
|
||||
{ mask:'172.16.0.0/12', name:'Private (RFC1918)' },
|
||||
{ mask:'192.168.0.0/16', name:'Private (RFC1918)' },
|
||||
{ mask:'100.64.0.0/10', name:'CGNAT' },
|
||||
{ mask:'127.0.0.0/8', name:'Loopback' },
|
||||
{ mask:'169.254.0.0/16', name:'Link-local' },
|
||||
{ mask:'192.0.0.0/24', name:'IANA reserved' },
|
||||
{ mask:'192.0.2.0/24', name:'TEST-NET-1' },
|
||||
{ mask:'198.51.100.0/24', name:'TEST-NET-2' },
|
||||
{ mask:'203.0.113.0/24', name:'TEST-NET-3' },
|
||||
{ mask:'198.18.0.0/15', name:'Benchmarking' },
|
||||
{ mask:'224.0.0.0/4', name:'Multicast' },
|
||||
{ mask:'240.0.0.0/4', name:'Reserved' },
|
||||
{ mask:'255.255.255.255/32',name:'Broadcast' },
|
||||
];
|
||||
|
||||
function ipToNum(ip) {
|
||||
var parts = ip.split('.');
|
||||
return ((+parts[0] * 256 + +parts[1]) * 256 + +parts[2]) * 256 + +parts[3];
|
||||
}
|
||||
|
||||
function parseCIDR(val) {
|
||||
var m = val.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:\/(\d{1,2}))?$/);
|
||||
if (!m) return null;
|
||||
var ip = m[1];
|
||||
var mask = m[2] ? parseInt(m[2], 10) : 32;
|
||||
if (mask < 0 || mask > 32) return null;
|
||||
var parts = ip.split('.');
|
||||
if (parts.some(function(p) { return +p > 255; })) return null;
|
||||
return { ip: ip, mask: mask, num: ipToNum(ip) };
|
||||
}
|
||||
|
||||
function checkBlocked(info) {
|
||||
for (var i = 0; i < BLOCKED.length; i++) {
|
||||
var b = parseCIDR(BLOCKED[i].mask);
|
||||
if (!b) continue;
|
||||
var maskNum = -1 << (32 - b.mask);
|
||||
if ((info.num & maskNum) === (b.num & maskNum)) {
|
||||
return BLOCKED[i].name + ' (' + BLOCKED[i].mask + ')';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizePreview(info) {
|
||||
if (info.mask === 32) return null;
|
||||
var maskNum = -1 << (32 - info.mask);
|
||||
var netNum = info.num & maskNum;
|
||||
var o0 = (netNum >>> 24) & 255;
|
||||
var o1 = (netNum >>> 16) & 255;
|
||||
var o2 = (netNum >>> 8) & 255;
|
||||
var o3 = netNum & 255;
|
||||
var expected = o0 + '.' + o1 + '.' + o2 + '.' + o3 + '/' + info.mask;
|
||||
if (expected !== info.ip + '/' + info.mask) {
|
||||
return expected;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var addForm = document.querySelector('form[action="/add"]');
|
||||
if (addForm) {
|
||||
addForm.addEventListener('submit', function(e) {
|
||||
var input = addForm.querySelector('input[name="value"]');
|
||||
var val = input.value.trim();
|
||||
if (!val) return;
|
||||
|
||||
var info = parseCIDR(val);
|
||||
if (!info) {
|
||||
e.preventDefault();
|
||||
alert('Неверный формат. Ожидается: IPv4 или CIDR (например 203.0.113.10 или 203.0.113.0/24)');
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.mask < 22) {
|
||||
e.preventDefault();
|
||||
alert('Маска /' + info.mask + ' не допускается. Минимальная маска: /22');
|
||||
return;
|
||||
}
|
||||
|
||||
var blocked = checkBlocked(info);
|
||||
if (blocked) {
|
||||
e.preventDefault();
|
||||
alert('Адрес принадлежит запрещённому диапазону:\n' + blocked + '\n\nДобавление заблокировано.');
|
||||
return;
|
||||
}
|
||||
|
||||
var norm = normalizePreview(info);
|
||||
if (norm) {
|
||||
if (!confirm('Вы ввели адрес из хостовой части подсети.\n\n' +
|
||||
'Будет сохранено: ' + norm + '\n\nПродолжить?')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}());
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user