feat: Add IP ranges management API endpoints and integrate with frontend for viewing and updating IP ranges
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m20s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m20s
This commit is contained in:
@@ -176,6 +176,56 @@ app.post('/api/domains-new', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- IP Ranges Routes ---
|
||||||
|
|
||||||
|
// Get IP ranges from S3
|
||||||
|
app.get('/api/ip-ranges', async (req, res) => {
|
||||||
|
const params = {
|
||||||
|
Bucket: BUCKET_NAME,
|
||||||
|
Key: 'domains/ips.txt',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await s3.getObject(params).promise();
|
||||||
|
const fileContent = data.Body.toString('utf-8');
|
||||||
|
const ipRanges = fileContent.split('\n').filter(line => line).map(line => {
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
const ipRange = parts[0] || '';
|
||||||
|
const community = parts[1] || '';
|
||||||
|
return { ipRange, community };
|
||||||
|
});
|
||||||
|
res.json(ipRanges);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'NoSuchKey') {
|
||||||
|
res.json([]); // Return empty array if file does not exist
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Error reading from S3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update IP ranges in S3
|
||||||
|
app.post('/api/ip-ranges', async (req, res) => {
|
||||||
|
const { ipRanges } = req.body;
|
||||||
|
const fileContent = ipRanges.map(ip => `${ip.ipRange} ${ip.community}`).join('\n');
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
Bucket: BUCKET_NAME,
|
||||||
|
Key: 'domains/ips.txt',
|
||||||
|
Body: fileContent,
|
||||||
|
ContentType: 'text/plain',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await s3.putObject(params).promise();
|
||||||
|
res.send('File updated successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).send('Error writing to S3');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- Servers Routes (JSON format) ---
|
// --- Servers Routes (JSON format) ---
|
||||||
|
|
||||||
// Get servers from S3
|
// Get servers from S3
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Примеры файлов S3 для Router Lists UI
|
||||||
|
|
||||||
|
Эта папка содержит примеры файлов, которые хранятся в S3 для работы с FilterManager.
|
||||||
|
|
||||||
|
## Структура папок
|
||||||
|
|
||||||
|
```
|
||||||
|
s3/
|
||||||
|
├── domains/ # Файлы доменов и IP
|
||||||
|
│ ├── domains.txt # Основные домены
|
||||||
|
│ ├── domains_comunity.txt # Домены с community
|
||||||
|
│ ├── asns.txt # ASN списки
|
||||||
|
│ └── ips.txt # IP-диапазоны с community
|
||||||
|
├── filter-manager/ # Файлы FilterManager
|
||||||
|
│ ├── simple-filters.json # Фильтры упрощённого режима
|
||||||
|
│ ├── server-filters-*.json # Фильтры для каждого сервера
|
||||||
|
│ └── config-*.txt # Сгенерированные конфигурации MikroTik
|
||||||
|
└── server-configs.json # Список серверов (в корне S3)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Описание файлов
|
||||||
|
|
||||||
|
### `server-configs.json`
|
||||||
|
Список всех серверов в расширенном режиме.
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "server-001",
|
||||||
|
"name": "SWE-HIPHOST",
|
||||||
|
"description": "Шведский сервер для стриминга и игр",
|
||||||
|
"enabled": true,
|
||||||
|
"createdAt": "2024-01-15T10:30:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `filter-manager/simple-filters.json`
|
||||||
|
Фильтры для упрощённого режима (применяются ко всем серверам).
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"community": "65001:100",
|
||||||
|
"gateway": "SWE-HIPHOST",
|
||||||
|
"description": "Шведский сервер для стриминга"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `filter-manager/server-filters-{serverId}.json`
|
||||||
|
Фильтры для конкретного сервера в расширенном режиме.
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"community": "65001:100",
|
||||||
|
"gateway": "SWE-HIPHOST",
|
||||||
|
"description": "Стриминг сервисы"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `domains/ips.txt`
|
||||||
|
IP-диапазоны с community для маршрутизации.
|
||||||
|
```
|
||||||
|
192.168.1.0/24 65001:100
|
||||||
|
10.0.0.0/8 65001:200
|
||||||
|
172.16.0.0/12 65001:300
|
||||||
|
8.8.8.0/24 65001:400
|
||||||
|
```
|
||||||
|
|
||||||
|
### `filter-manager/config-{serverId}.txt`
|
||||||
|
Сгенерированная конфигурация MikroTik для конкретного сервера.
|
||||||
|
```
|
||||||
|
// Frouting filter configuration for MikroTik 7.14+
|
||||||
|
// Generated automatically
|
||||||
|
// Date: 2024-01-20T12:00:00.000Z
|
||||||
|
|
||||||
|
if (
|
||||||
|
(bgp-communities includes 65001:100)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:101)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
set gw SWE-HIPHOST; accept;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reject;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Примеры серверов
|
||||||
|
|
||||||
|
1. **SWE-HIPHOST** (server-001) - Шведский сервер для стриминга
|
||||||
|
2. **NL-AMSTERDAM** (server-002) - Голландский сервер с низкой задержкой
|
||||||
|
3. **DE-FRANKFURT** (server-003) - Немецкий сервер для торрентов (отключен)
|
||||||
|
4. **US-NEWYORK** (server-004) - Американский сервер для Netflix (с каскадной структурой)
|
||||||
|
5. **JP-TOKYO** (server-005) - Японский сервер для аниме и игр
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
- **Каскадная структура**: Сервер US-NEWYORK демонстрирует конфигурацию с несколькими gateway
|
||||||
|
- **Отключённые серверы**: DE-FRANKFURT показывает неактивный сервер
|
||||||
|
- **Разные типы трафика**: Каждый сервер специализируется на определённом типе контента
|
||||||
|
- **Реалистичные community**: Используются AS:community в формате 65001:XXX
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
Эти файлы можно использовать для:
|
||||||
|
- Тестирования FilterManager
|
||||||
|
- Демонстрации функциональности
|
||||||
|
- Обучения работе с системой
|
||||||
|
- Восстановления данных после сбоя
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
192.168.1.0/24 65001:100
|
||||||
|
10.0.0.0/8 65001:200
|
||||||
|
172.16.0.0/12 65001:300
|
||||||
|
8.8.8.0/24 65001:400
|
||||||
|
1.1.1.0/24 65001:500
|
||||||
|
208.67.222.0/24 65001:600
|
||||||
|
9.9.9.0/24 65001:700
|
||||||
|
149.112.112.0/24 65001:800
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Frouting filter configuration for MikroTik 7.14+
|
||||||
|
// Generated automatically
|
||||||
|
// Date: 2024-01-20T12:00:00.000Z
|
||||||
|
|
||||||
|
if (
|
||||||
|
(bgp-communities includes 65001:100)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:101)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:102)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
set gw SWE-HIPHOST; accept;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reject;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Frouting filter configuration for MikroTik 7.14+
|
||||||
|
// Generated automatically
|
||||||
|
// Date: 2024-01-20T12:00:00.000Z
|
||||||
|
|
||||||
|
if (
|
||||||
|
(bgp-communities includes 65001:200)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:201)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:202)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:203)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
set gw NL-AMSTERDAM; accept;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reject;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Frouting filter configuration for MikroTik 7.14+
|
||||||
|
// Generated automatically
|
||||||
|
// Date: 2024-01-20T12:00:00.000Z
|
||||||
|
|
||||||
|
if (
|
||||||
|
(bgp-communities includes 65001:400)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:401)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
set gw US-NEWYORK; accept;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
(bgp-communities includes 65001:402)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:403)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
set gw US-LOSANGELES; accept;
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
(bgp-communities includes 65001:404)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
set gw US-CHICAGO; accept;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reject;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Frouting filter configuration for MikroTik 7.14+
|
||||||
|
// Generated automatically
|
||||||
|
// Date: 2024-01-20T12:00:00.000Z
|
||||||
|
|
||||||
|
if (
|
||||||
|
(bgp-communities includes 65001:500)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:501)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:502)
|
||||||
|
or
|
||||||
|
(bgp-communities includes 65001:503)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
set gw JP-TOKYO; accept;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reject;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"community": "65001:100",
|
||||||
|
"gateway": "SWE-HIPHOST",
|
||||||
|
"description": "Стриминг сервисы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:101",
|
||||||
|
"gateway": "SWE-HIPHOST",
|
||||||
|
"description": "Игровые серверы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:102",
|
||||||
|
"gateway": "SWE-HIPHOST",
|
||||||
|
"description": "Социальные сети"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"community": "65001:200",
|
||||||
|
"gateway": "NL-AMSTERDAM",
|
||||||
|
"description": "Игровые платформы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:201",
|
||||||
|
"gateway": "NL-AMSTERDAM",
|
||||||
|
"description": "VoIP сервисы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:202",
|
||||||
|
"gateway": "NL-AMSTERDAM",
|
||||||
|
"description": "Облачные сервисы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:203",
|
||||||
|
"gateway": "NL-AMSTERDAM",
|
||||||
|
"description": "CDN сети"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"community": "65001:300",
|
||||||
|
"gateway": "DE-FRANKFURT",
|
||||||
|
"description": "Торрент трекеры"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:301",
|
||||||
|
"gateway": "DE-FRANKFURT",
|
||||||
|
"description": "Файлообменники"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:302",
|
||||||
|
"gateway": "DE-FRANKFURT",
|
||||||
|
"description": "P2P сети"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"community": "65001:400",
|
||||||
|
"gateway": "US-NEWYORK",
|
||||||
|
"description": "Netflix US"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:401",
|
||||||
|
"gateway": "US-NEWYORK",
|
||||||
|
"description": "Hulu US"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:402",
|
||||||
|
"gateway": "US-LOSANGELES",
|
||||||
|
"description": "Disney+ US"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:403",
|
||||||
|
"gateway": "US-LOSANGELES",
|
||||||
|
"description": "HBO Max US"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:404",
|
||||||
|
"gateway": "US-CHICAGO",
|
||||||
|
"description": "Amazon Prime US"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"community": "65001:500",
|
||||||
|
"gateway": "JP-TOKYO",
|
||||||
|
"description": "Crunchyroll"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:501",
|
||||||
|
"gateway": "JP-TOKYO",
|
||||||
|
"description": "Funimation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:502",
|
||||||
|
"gateway": "JP-TOKYO",
|
||||||
|
"description": "Steam JP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:503",
|
||||||
|
"gateway": "JP-TOKYO",
|
||||||
|
"description": "Nintendo Online"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"community": "65001:100",
|
||||||
|
"gateway": "SWE-HIPHOST",
|
||||||
|
"description": "Шведский сервер для стриминга"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:200",
|
||||||
|
"gateway": "NL-AMSTERDAM",
|
||||||
|
"description": "Голландский сервер для игр"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:300",
|
||||||
|
"gateway": "DE-FRANKFURT",
|
||||||
|
"description": "Немецкий сервер для торрентов"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:400",
|
||||||
|
"gateway": "US-NEWYORK",
|
||||||
|
"description": "Американский сервер для Netflix"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"community": "65001:500",
|
||||||
|
"gateway": "JP-TOKYO",
|
||||||
|
"description": "Японский сервер для аниме"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "server-001",
|
||||||
|
"name": "SWE-HIPHOST",
|
||||||
|
"description": "Шведский сервер для стриминга и игр",
|
||||||
|
"enabled": true,
|
||||||
|
"createdAt": "2024-01-15T10:30:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-002",
|
||||||
|
"name": "NL-AMSTERDAM",
|
||||||
|
"description": "Голландский сервер с низкой задержкой",
|
||||||
|
"enabled": true,
|
||||||
|
"createdAt": "2024-01-16T14:20:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-003",
|
||||||
|
"name": "DE-FRANKFURT",
|
||||||
|
"description": "Немецкий сервер для торрентов",
|
||||||
|
"enabled": false,
|
||||||
|
"createdAt": "2024-01-17T09:15:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-004",
|
||||||
|
"name": "US-NEWYORK",
|
||||||
|
"description": "Американский сервер для Netflix",
|
||||||
|
"enabled": true,
|
||||||
|
"createdAt": "2024-01-18T16:45:00.000Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-005",
|
||||||
|
"name": "JP-TOKYO",
|
||||||
|
"description": "Японский сервер для аниме и игр",
|
||||||
|
"enabled": true,
|
||||||
|
"createdAt": "2024-01-19T11:30:00.000Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -23,6 +23,7 @@ import DataManager from './DataManager';
|
|||||||
import ServerManager from './ServerManager';
|
import ServerManager from './ServerManager';
|
||||||
import FilterManager from './FilterManager';
|
import FilterManager from './FilterManager';
|
||||||
import DomainsNewManager from './DomainsNewManager';
|
import DomainsNewManager from './DomainsNewManager';
|
||||||
|
import IPRangesManager from './IPRangesManager';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
@@ -52,6 +53,7 @@ function MainLayout() {
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ id: 'domains', title: 'Домены', icon: IconWorld, path: '/domains' },
|
{ id: 'domains', title: 'Домены', icon: IconWorld, path: '/domains' },
|
||||||
{ id: 'domains-new', title: 'Домены New', icon: IconWorld, path: '/domains-new' },
|
{ id: 'domains-new', title: 'Домены New', icon: IconWorld, path: '/domains-new' },
|
||||||
|
{ id: 'ip-ranges', title: 'IP-диапазоны', icon: IconNetwork, path: '/ip-ranges' },
|
||||||
{ id: 'asns', title: 'AS', icon: IconNetwork, path: '/asns' },
|
{ id: 'asns', title: 'AS', icon: IconNetwork, path: '/asns' },
|
||||||
{ id: 'servers', title: 'Серверы', icon: IconServer, path: '/servers' },
|
{ id: 'servers', title: 'Серверы', icon: IconServer, path: '/servers' },
|
||||||
{ id: 'filters', title: 'Фильтры', icon: IconFilter, path: '/filters' },
|
{ id: 'filters', title: 'Фильтры', icon: IconFilter, path: '/filters' },
|
||||||
@@ -98,6 +100,7 @@ function MainLayout() {
|
|||||||
/>
|
/>
|
||||||
} />
|
} />
|
||||||
<Route path="/domains-new" element={<DomainsNewManager />} />
|
<Route path="/domains-new" element={<DomainsNewManager />} />
|
||||||
|
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
||||||
<Route path="/asns" element={
|
<Route path="/asns" element={
|
||||||
<DataManager
|
<DataManager
|
||||||
entityName="AS"
|
entityName="AS"
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import {
|
||||||
|
IconPlus,
|
||||||
|
IconSearch,
|
||||||
|
IconEdit,
|
||||||
|
IconTrash,
|
||||||
|
IconCheck,
|
||||||
|
IconX,
|
||||||
|
IconDatabase,
|
||||||
|
IconRefresh,
|
||||||
|
IconDownload,
|
||||||
|
IconUpload,
|
||||||
|
IconAlertTriangle,
|
||||||
|
IconFilter,
|
||||||
|
IconChevronDown,
|
||||||
|
IconLink,
|
||||||
|
IconCopy,
|
||||||
|
IconEye,
|
||||||
|
IconServer,
|
||||||
|
IconSettings,
|
||||||
|
IconDeviceFloppy,
|
||||||
|
IconHash,
|
||||||
|
IconRocket,
|
||||||
|
IconWorld,
|
||||||
|
IconNetwork
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
|
const API_URL = '/api';
|
||||||
|
|
||||||
|
function IPRangesManager() {
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [success, setSuccess] = useState('');
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [editingIndex, setEditingIndex] = useState(null);
|
||||||
|
const [editingItem, setEditingItem] = useState({ ipRange: '', community: '' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchItems();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchItems = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/ip-ranges`);
|
||||||
|
setItems(response.data);
|
||||||
|
setError('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching ip-ranges:', error);
|
||||||
|
setItems([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setEditingIndex(null);
|
||||||
|
setEditingItem({ ipRange: '', community: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (index) => {
|
||||||
|
setEditingIndex(index);
|
||||||
|
setEditingItem({ ...items[index] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!editingItem.ipRange.trim() || !editingItem.community.trim()) {
|
||||||
|
setError('IP-диапазон и Community обязательны для заполнения.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedItems = [...items];
|
||||||
|
if (editingIndex !== null) {
|
||||||
|
updatedItems[editingIndex] = { ...editingItem };
|
||||||
|
} else {
|
||||||
|
updatedItems.push({ ...editingItem });
|
||||||
|
}
|
||||||
|
|
||||||
|
setItems(updatedItems);
|
||||||
|
setEditingIndex(null);
|
||||||
|
setEditingItem({ ipRange: '', community: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditingIndex(null);
|
||||||
|
setEditingItem({ ipRange: '', community: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (index) => {
|
||||||
|
const updatedItems = items.filter((_, i) => i !== index);
|
||||||
|
setItems(updatedItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveToS3 = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await axios.post(`${API_URL}/ip-ranges`, { ipRanges: items });
|
||||||
|
setSuccess('IP-диапазоны успешно сохранены!');
|
||||||
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving ip-ranges:', error);
|
||||||
|
setError('Не удалось сохранить IP-диапазоны.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target.result;
|
||||||
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
|
const parsedItems = lines.map(line => {
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
return {
|
||||||
|
ipRange: parts[0] || '',
|
||||||
|
community: parts[1] || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setItems(parsedItems);
|
||||||
|
setSuccess(`Загружено ${parsedItems.length} IP-диапазонов!`);
|
||||||
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
const content = items.map(item => `${item.ipRange} ${item.community}`).join('\n');
|
||||||
|
const blob = new Blob([content], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'ips.txt';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredItems = items.filter(item =>
|
||||||
|
item.ipRange.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
item.community.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container-xl">
|
||||||
|
<div className="page-header d-print-none mb-4">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
<h2 className="page-title">Управление IP-диапазонами</h2>
|
||||||
|
<div className="page-pretitle">Главная / IP-диапазоны</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-auto ms-auto d-print-none">
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={items.length === 0}
|
||||||
|
>
|
||||||
|
<IconDownload className="me-2" />
|
||||||
|
Скачать
|
||||||
|
</button>
|
||||||
|
<label className="btn btn-outline-primary">
|
||||||
|
<IconUpload className="me-2" />
|
||||||
|
Загрузить
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".txt"
|
||||||
|
onChange={handleUpload}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
<IconPlus className="me-2" />
|
||||||
|
Добавить IP-диапазон
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Уведомления */}
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<IconAlertTriangle className="me-2" />
|
||||||
|
{error}
|
||||||
|
<button type="button" className="btn-close" onClick={() => setError('')}></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<div className="alert alert-success alert-dismissible" role="alert">
|
||||||
|
<IconCheck className="me-2" />
|
||||||
|
{success}
|
||||||
|
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Панель поиска и сохранения */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row g-3 align-items-center">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="input-group">
|
||||||
|
<span className="input-group-text">
|
||||||
|
<IconSearch />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder="Поиск по IP-диапазону или Community..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="d-flex gap-2 justify-content-end">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onClick={fetchItems}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IconRefresh className="me-2" />
|
||||||
|
Обновить
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-success"
|
||||||
|
onClick={handleSaveToS3}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IconDeviceFloppy className="me-2" />
|
||||||
|
Сохранить в S3
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Таблица IP-диапазонов */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title">
|
||||||
|
<IconNetwork className="me-2" />
|
||||||
|
IP-диапазоны ({filteredItems.length} из {items.length})
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center text-muted py-4">
|
||||||
|
<div className="spinner-border spinner-border-sm me-2" role="status">
|
||||||
|
<span className="visually-hidden">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
Загрузка IP-диапазонов...
|
||||||
|
</div>
|
||||||
|
) : filteredItems.length === 0 ? (
|
||||||
|
<div className="text-center text-muted py-4">
|
||||||
|
<IconNetwork className="icon-lg mb-2" />
|
||||||
|
<p>Нет IP-диапазонов</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
<IconPlus className="me-2" />
|
||||||
|
Добавить первый IP-диапазон
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: '40%' }}>IP-ДИАПАЗОН</th>
|
||||||
|
<th style={{ width: '40%' }}>COMMUNITY</th>
|
||||||
|
<th style={{ width: '20%' }} className="text-end">ДЕЙСТВИЯ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredItems.map((item, index) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>
|
||||||
|
{editingIndex === index ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={editingItem.ipRange}
|
||||||
|
onChange={(e) => setEditingItem({ ...editingItem, ipRange: e.target.value })}
|
||||||
|
placeholder="192.168.1.0/24"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<span className="badge bg-blue-lt text-blue me-2">
|
||||||
|
<IconNetwork size={12} />
|
||||||
|
</span>
|
||||||
|
<code className="text-blue">{item.ipRange}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{editingIndex === index ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={editingItem.community}
|
||||||
|
onChange={(e) => setEditingItem({ ...editingItem, community: e.target.value })}
|
||||||
|
placeholder="65001:100"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<span className="badge bg-green-lt text-green me-2">
|
||||||
|
<IconHash size={12} />
|
||||||
|
</span>
|
||||||
|
<code className="text-green">{item.community}</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="text-end">
|
||||||
|
{editingIndex === index ? (
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-success btn-icon btn-sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
title="Сохранить"
|
||||||
|
>
|
||||||
|
<IconCheck size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary btn-icon btn-sm"
|
||||||
|
onClick={handleCancel}
|
||||||
|
title="Отмена"
|
||||||
|
>
|
||||||
|
<IconX size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary btn-icon btn-sm"
|
||||||
|
onClick={() => handleEdit(index)}
|
||||||
|
title="Редактировать"
|
||||||
|
>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-danger btn-icon btn-sm"
|
||||||
|
onClick={() => handleDelete(index)}
|
||||||
|
title="Удалить"
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IPRangesManager;
|
||||||
Reference in New Issue
Block a user