diff --git a/README.md b/README.md
index 4ff4ae6..fe9f5ab 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,15 @@
# 📂 S3 Lists Manager
-Веб-интерфейс для удобного управления текстовыми файлами (`domains.txt`, `asns.txt`) в S3-совместимом хранилище Yandex Cloud. Приложение позволяет в реальном времени просматривать, добавлять, редактировать, удалять и массово изменять записи в этих файлах.
+Веб-интерфейс для удобного управления файлами в S3-совместимом хранилище Yandex Cloud. Приложение позволяет в реальном времени просматривать, добавлять, редактировать, удалять и массово изменять записи в следующих файлах:
+
+- **domains.txt** - список доменов и их шлюзов
+- **asns.txt** - список AS номеров и их шлюзов
+- **servers.json** - список серверов с расширенной информацией (IP, DNS, страна, провайдер, тип туннеля)
## ✨ Возможности
-- **Два режима работы:** Управление списками доменов и AS-номеров через вкладки.
+- **Три режима работы:** Управление списками доменов, AS-номеров и серверов через вкладки.
- **CRUD операции:** Полный набор действий: создание, чтение, обновление и удаление записей.
- **Поиск в реальном времени:** Мгновенная фильтрация списков по мере ввода.
- **Массовая замена:** Быстрое обновление шлюзов для сотен записей в один клик.
@@ -44,6 +48,7 @@ graph TD
C{S3 Bucket}
D1[domains.txt]
D2[asns.txt]
+ D3[servers.json]
end
A -- HTTP Requests --> B
@@ -197,4 +202,6 @@ docker-compose up -d
| `GET` | `/api/domains`| Получить список всех доменов. |
| `POST` | `/api/domains`| Сохранить изменения в `domains.txt`. |
| `GET` | `/api/asns` | Получить список всех AS. |
-| `POST` | `/api/asns` | Сохранить изменения в `asns.txt`. |
\ No newline at end of file
+| `POST` | `/api/asns` | Сохранить изменения в `asns.txt`. |
+| `GET` | `/api/servers`| Получить список всех серверов. |
+| `POST` | `/api/servers`| Сохранить изменения в `servers.json`. |
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
index f1cbfda..12d2f07 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -126,16 +126,87 @@ app.post('/api/asns', async (req, res) => {
}
});
+// --- Servers Routes (JSON format) ---
+
+// Get servers from S3
+app.get('/api/servers', async (req, res) => {
+ const params = {
+ Bucket: BUCKET_NAME,
+ Key: 'servers.json',
+ };
+
+ try {
+ const data = await s3.getObject(params).promise();
+ const fileContent = data.Body.toString('utf-8');
+ let servers = [];
+
+ try {
+ servers = JSON.parse(fileContent);
+ // Ensure it's an array
+ if (!Array.isArray(servers)) {
+ servers = [];
+ }
+ } catch (parseError) {
+ console.error('Error parsing servers.json:', parseError);
+ servers = [];
+ }
+
+ res.json(servers);
+ } 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 servers in S3
+app.post('/api/servers', async (req, res) => {
+ const { domains: servers } = req.body; // Keep name 'domains' for consistency
+
+ // Validate servers structure
+ if (!Array.isArray(servers)) {
+ return res.status(400).send('Servers must be an array');
+ }
+
+ // Validate each server has required fields
+ for (let i = 0; i < servers.length; i++) {
+ const server = servers[i];
+ if (!server.ip || !server.dns || !server.country || !server.provider || !server.tunnel) {
+ return res.status(400).send(`Server at index ${i} is missing required fields`);
+ }
+ }
+
+ const params = {
+ Bucket: BUCKET_NAME,
+ Key: 'servers.json',
+ Body: JSON.stringify(servers, null, 2), // Pretty print JSON
+ ContentType: 'application/json',
+ };
+
+ try {
+ await s3.putObject(params).promise();
+ res.send('File updated successfully');
+ } catch (error) {
+ console.error(error);
+ res.status(500).send('Error writing to S3');
+ }
+});
+
// Новый эндпоинт для получения дат последнего изменения файлов S3
app.get('/api/s3/last-modified', async (req, res) => {
try {
- const [domainsHead, asnsHead] = await Promise.all([
+ const [domainsHead, asnsHead, serversHead] = await Promise.all([
s3.headObject({ Bucket: BUCKET_NAME, Key: 'domains.txt' }).promise(),
- s3.headObject({ Bucket: BUCKET_NAME, Key: 'asns.txt' }).promise()
+ s3.headObject({ Bucket: BUCKET_NAME, Key: 'asns.txt' }).promise(),
+ s3.headObject({ Bucket: BUCKET_NAME, Key: 'servers.json' }).promise()
]);
res.json({
domainsLastModified: domainsHead.LastModified ? domainsHead.LastModified.toISOString() : null,
- asnsLastModified: asnsHead.LastModified ? asnsHead.LastModified.toISOString() : null
+ asnsLastModified: asnsHead.LastModified ? asnsHead.LastModified.toISOString() : null,
+ serversLastModified: serversHead.LastModified ? serversHead.LastModified.toISOString() : null
});
} catch (error) {
console.error(error);
diff --git a/example-servers.json b/example-servers.json
new file mode 100644
index 0000000..57dd170
--- /dev/null
+++ b/example-servers.json
@@ -0,0 +1,37 @@
+[
+ {
+ "ip": "192.168.1.1",
+ "dns": "srv1.shx.su",
+ "country": "RU",
+ "provider": "Yandex",
+ "tunnel": "GRE"
+ },
+ {
+ "ip": "10.0.0.5",
+ "dns": "srv2.shx.su",
+ "country": "US",
+ "provider": "AWS",
+ "tunnel": "IPSec"
+ },
+ {
+ "ip": "172.16.0.10",
+ "dns": "srv3.shx.su",
+ "country": "DE",
+ "provider": "Hetzner",
+ "tunnel": "GRE"
+ },
+ {
+ "ip": "8.8.8.8",
+ "dns": "dns.google",
+ "country": "US",
+ "provider": "Google",
+ "tunnel": "IPSec"
+ },
+ {
+ "ip": "1.1.1.1",
+ "dns": "cloudflare-dns.com",
+ "country": "US",
+ "provider": "Cloudflare",
+ "tunnel": "GRE"
+ }
+]
\ No newline at end of file
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 86c8979..894b3a2 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -42,6 +42,20 @@
box-shadow: none !important;
}
+/* Spinning animation for loading icons */
+.spin {
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
/* Custom responsive adjustments */
@media (max-width: 992px) {
.page-wrapper {
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 088af32..7d77bdb 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -15,9 +15,11 @@ import {
IconUser,
IconDatabase,
IconAlertTriangle,
- IconAlertCircle
+ IconAlertCircle,
+ IconServer
} from '@tabler/icons-react';
import DataManager from './DataManager';
+import ServerManager from './ServerManager';
import './App.css';
import axios from 'axios';
@@ -32,6 +34,7 @@ function App() {
{ id: 'dashboard', title: 'Dashboard', icon: IconHome },
{ id: 'domains', title: 'Домены', icon: IconWorld },
{ id: 'asns', title: 'AS', icon: IconNetwork },
+ { id: 'servers', title: 'Серверы', icon: IconServer },
{ id: 'files', title: 'Файлы', icon: IconFileText },
{ id: 'cloud', title: 'Облако', icon: IconCloud },
{ id: 'security', title: 'Безопасность', icon: IconShield },
@@ -59,6 +62,8 @@ function App() {
placeholder="AS12345"
/>
);
+ case 'servers':
+ return
+ Все изменения сохраняются в файл servers.json в S3 хранилище. +
+| handleSort('ip')}> + IP адрес + {sortField === 'ip' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + | +handleSort('dns')}> + DNS имя + {sortField === 'dns' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + | +handleSort('country')}> + Страна + {sortField === 'country' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + | +handleSort('provider')}> + Провайдер + {sortField === 'provider' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + | +handleSort('tunnel')}> + Туннель + {sortField === 'tunnel' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + | +Действия | +
|---|---|---|---|---|---|
| {server.ip} | +{server.dns} | +{server.country} | +{server.provider} | +{server.tunnel} | ++ {editingServer === server.ip ? ( + <> + + + > + ) : ( + <> + + + > + )} + | +
+ Показано {(currentPage - 1) * pageSize + 1} до {Math.min(currentPage * pageSize, filteredServers.length)} из {filteredServers.length} серверов +
+Вы уверены, что хотите удалить сервер {serverToDelete?.ip}?
+Это действие нельзя отменить.
+