Files
router-lists-ui/backend/server.js
T
2025-07-14 11:13:35 +07:00

790 lines
25 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const AWS = require('aws-sdk');
const cors = require('cors');
const path = require('path');
const app = express();
const port = 3001;
app.use(cors());
app.use(express.json());
// Serve static files from the React app
app.use(express.static(path.join(__dirname, 'public')));
// Configure AWS S3
const s3 = new AWS.S3({
endpoint: 'https://storage.yandexcloud.net',
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
}
});
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
const FILE_KEY = 'domains.txt';
// Get domains from S3
app.get('/api/domains', async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Key: FILE_KEY,
};
try {
const data = await s3.getObject(params).promise();
const fileContent = data.Body.toString('utf-8');
const domains = fileContent.split('\n').filter(line => line).map(line => {
// Trim the line to remove any leading/trailing whitespace, including \r
// And split by any whitespace to be more robust
const parts = line.trim().split(/\s+/);
const domain = parts[0] || '';
const type = parts[1] || '';
return { domain, type };
});
res.json(domains);
} 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 domains in S3
app.post('/api/domains', async (req, res) => {
const { domains } = req.body;
const fileContent = domains.map(d => `${d.domain} ${d.type}`).join('\n');
const params = {
Bucket: BUCKET_NAME,
Key: FILE_KEY,
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');
}
});
// --- ASNs Routes ---
// Get ASNs from S3
app.get('/api/asns', async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Key: 'asns.txt',
};
try {
const data = await s3.getObject(params).promise();
const fileContent = data.Body.toString('utf-8');
const asns = fileContent.split('\n').filter(line => line).map(line => {
const parts = line.trim().split(/\s+/);
const domain = parts[0] || ''; // Keep name 'domain' for consistency in component
const type = parts[1] || '';
return { domain, type };
});
res.json(asns);
} catch (error) {
if (error.code === 'NoSuchKey') {
res.json([]);
} else {
console.error(error);
res.status(500).send('Error reading from S3');
}
}
});
// Update ASNs in S3
app.post('/api/asns', async (req, res) => {
const { domains: asns } = req.body; // Keep name 'domains' for consistency
const fileContent = asns.map(a => `${a.domain} ${a.type}`).join('\n');
const params = {
Bucket: BUCKET_NAME,
Key: 'asns.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');
}
});
// --- Domains New Routes ---
// Get domains-new from S3
app.get('/api/domains-new', async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Key: 'domains_comunity.txt',
};
try {
const data = await s3.getObject(params).promise();
const fileContent = data.Body.toString('utf-8');
const domains = fileContent.split('\n').filter(line => line).map(line => {
const parts = line.trim().split(/\s+/);
const domain = parts[0] || '';
const community = parts[1] || '';
return { domain, community };
});
res.json(domains);
} 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 domains-new in S3
app.post('/api/domains-new', async (req, res) => {
const { domains } = req.body;
const fileContent = domains.map(d => `${d.domain} ${d.community}`).join('\n');
const params = {
Bucket: BUCKET_NAME,
Key: 'domains_comunity.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) ---
// 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');
}
});
// --- Filters Routes (JSON format) ---
// Get filters from S3
app.get('/api/filters', async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Key: 'filters.json',
};
try {
const data = await s3.getObject(params).promise();
const fileContent = data.Body.toString('utf-8');
let filters = [];
try {
filters = JSON.parse(fileContent);
// Ensure it's an array
if (!Array.isArray(filters)) {
filters = [];
}
} catch (parseError) {
console.error('Error parsing filters.json:', parseError);
filters = [];
}
res.json(filters);
} 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 filters in S3
app.post('/api/filters', async (req, res) => {
const { domains: filters } = req.body; // Keep name 'domains' for consistency
// Validate filters structure
if (!Array.isArray(filters)) {
return res.status(400).send('Filters must be an array');
}
// Validate each filter has required fields
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
if (!filter.community || !filter.gateway) {
return res.status(400).send(`Filter at index ${i} is missing required fields`);
}
}
const params = {
Bucket: BUCKET_NAME,
Key: 'filters.json',
Body: JSON.stringify(filters, 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, domainsNewHead, asnsHead, serversHead, filtersHead] = await Promise.all([
s3.headObject({ Bucket: BUCKET_NAME, Key: 'domains.txt' }).promise(),
s3.headObject({ Bucket: BUCKET_NAME, Key: 'domains_comunity.txt' }).promise(),
s3.headObject({ Bucket: BUCKET_NAME, Key: 'asns.txt' }).promise(),
s3.headObject({ Bucket: BUCKET_NAME, Key: 'servers.json' }).promise(),
s3.headObject({ Bucket: BUCKET_NAME, Key: 'filters.json' }).promise()
]);
res.json({
domainsLastModified: domainsHead.LastModified ? domainsHead.LastModified.toISOString() : null,
domainsNewLastModified: domainsNewHead.LastModified ? domainsNewHead.LastModified.toISOString() : null,
asnsLastModified: asnsHead.LastModified ? asnsHead.LastModified.toISOString() : null,
serversLastModified: serversHead.LastModified ? serversHead.LastModified.toISOString() : null,
filtersLastModified: filtersHead.LastModified ? filtersHead.LastModified.toISOString() : null
});
} catch (error) {
console.error(error);
res.status(500).send('Error fetching last modified dates from S3');
}
});
// Generate MikroTik configuration from filters
app.get('/api/filters/generate-config', async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Key: 'filters.json',
};
try {
const data = await s3.getObject(params).promise();
const fileContent = data.Body.toString('utf-8');
let filters = [];
try {
filters = JSON.parse(fileContent);
if (!Array.isArray(filters)) {
filters = [];
}
} catch (parseError) {
console.error('Error parsing filters.json:', parseError);
filters = [];
}
if (filters.length === 0) {
return res.json({ config: '// No filters to generate configuration' });
}
// Group filters by gateway
const gatewayGroups = {};
filters.forEach(filter => {
if (!gatewayGroups[filter.gateway]) {
gatewayGroups[filter.gateway] = [];
}
gatewayGroups[filter.gateway].push(filter.community);
});
let config = '// Frouting filter configuration for MikroTik 7.14+\n';
config += '// Generated automatically\n';
config += `// Date: ${new Date().toISOString()}\n\n`;
// Если есть только один gateway, создаем простую конфигурацию
if (Object.keys(gatewayGroups).length === 1) {
const [gateway, communities] = Object.entries(gatewayGroups)[0];
config += `if (\n`;
communities.forEach((community, index) => {
config += `(bgp-communities includes ${community})`;
if (index < communities.length - 1) {
config += ` \nor `;
}
});
config += `\n)\n{\n set gw ${gateway}; accept;\n}\nelse\n{\n reject;\n}\n`;
} else {
// Если несколько gateway, создаем каскадную структуру
const gatewayEntries = Object.entries(gatewayGroups);
gatewayEntries.forEach(([gateway, communities], gatewayIndex) => {
if (gatewayIndex === 0) {
config += `if (\n`;
} else {
config += `else if (\n`;
}
communities.forEach((community, index) => {
config += `(bgp-communities includes ${community})`;
if (index < communities.length - 1) {
config += ` \nor `;
}
});
config += `\n)\n{\n set gw ${gateway}; accept;\n}`;
if (gatewayIndex === gatewayEntries.length - 1) {
config += `\nelse\n{\n reject;\n}\n`;
} else {
config += `\n`;
}
});
}
res.json({ config });
} catch (error) {
if (error.code === 'NoSuchKey') {
res.json({ config: '// filters.json file not found' });
} else {
console.error(error);
res.status(500).send('Error generating configuration');
}
}
});
// Export MikroTik configuration to S3
app.post('/api/filters/export-config', async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Key: 'filters.json',
};
try {
const data = await s3.getObject(params).promise();
const fileContent = data.Body.toString('utf-8');
let filters = [];
try {
filters = JSON.parse(fileContent);
if (!Array.isArray(filters)) {
filters = [];
}
} catch (parseError) {
console.error('Error parsing filters.json:', parseError);
filters = [];
}
if (filters.length === 0) {
return res.json({ success: false, message: 'Нет фильтров для экспорта' });
}
// Group filters by gateway
const gatewayGroups = {};
filters.forEach(filter => {
if (!gatewayGroups[filter.gateway]) {
gatewayGroups[filter.gateway] = [];
}
gatewayGroups[filter.gateway].push(filter.community);
});
let config = '// Frouting filter configuration for MikroTik 7.14+\n';
config += '// Generated automatically\n';
config += `// Date: ${new Date().toISOString()}\n\n`;
// Если есть только один gateway, создаем простую конфигурацию
if (Object.keys(gatewayGroups).length === 1) {
const [gateway, communities] = Object.entries(gatewayGroups)[0];
config += `if (\n`;
communities.forEach((community, index) => {
config += `(bgp-communities includes ${community})`;
if (index < communities.length - 1) {
config += ` \nor `;
}
});
config += `\n)\n{\n set gw ${gateway}; accept;\n}\nelse\n{\n reject;\n}\n`;
} else {
// Если несколько gateway, создаем каскадную структуру
const gatewayEntries = Object.entries(gatewayGroups);
gatewayEntries.forEach(([gateway, communities], gatewayIndex) => {
if (gatewayIndex === 0) {
config += `if (\n`;
} else {
config += `else if (\n`;
}
communities.forEach((community, index) => {
config += `(bgp-communities includes ${community})`;
if (index < communities.length - 1) {
config += ` \nor `;
}
});
config += `\n)\n{\n set gw ${gateway}; accept;\n}`;
if (gatewayIndex === gatewayEntries.length - 1) {
config += `\nelse\n{\n reject;\n}\n`;
} else {
config += `\n`;
}
});
}
// Save configuration to S3
const exportParams = {
Bucket: BUCKET_NAME,
Key: 'mikrotik-frouting-config.txt',
Body: config,
ContentType: 'text/plain',
};
await s3.putObject(exportParams).promise();
res.json({ success: true, message: 'Конфигурация экспортирована в S3' });
} catch (error) {
console.error(error);
res.status(500).send('Error exporting configuration');
}
});
// --- Server Configs Routes ---
// Get server configs list
app.get('/api/server-configs', async (req, res) => {
const params = {
Bucket: BUCKET_NAME,
Key: 'server-configs.json',
};
try {
const data = await s3.getObject(params).promise();
const fileContent = data.Body.toString('utf-8');
let servers = [];
try {
servers = JSON.parse(fileContent);
if (!Array.isArray(servers)) {
servers = [];
}
} catch (parseError) {
console.error('Error parsing server-configs.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 server configs from S3');
}
}
});
// Update server configs list
app.post('/api/server-configs', async (req, res) => {
const { servers } = req.body;
// 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.id || !server.name) {
return res.status(400).send(`Server at index ${i} is missing required fields`);
}
}
const params = {
Bucket: BUCKET_NAME,
Key: 'server-configs.json',
Body: JSON.stringify(servers, null, 2), // Pretty print JSON
ContentType: 'application/json',
};
try {
await s3.putObject(params).promise();
res.send('Server configs updated successfully');
} catch (error) {
console.error(error);
res.status(500).send('Error writing server configs to S3');
}
});
// Get specific server config
app.get('/api/server-configs/:serverId', async (req, res) => {
const { serverId } = req.params;
const params = {
Bucket: BUCKET_NAME,
Key: `config-${serverId}.txt`,
};
try {
const data = await s3.getObject(params).promise();
const config = data.Body.toString('utf-8');
res.json({ config });
} catch (error) {
if (error.code === 'NoSuchKey') {
res.json({ config: '// Конфигурация не найдена' });
} else {
console.error(error);
res.status(500).send('Error reading server config from S3');
}
}
});
// Save specific server config
app.post('/api/server-configs/:serverId', async (req, res) => {
const { serverId } = req.params;
const { config } = req.body;
if (!config) {
return res.status(400).send('Config is required');
}
const params = {
Bucket: BUCKET_NAME,
Key: `config-${serverId}.txt`,
Body: config,
ContentType: 'text/plain',
};
try {
await s3.putObject(params).promise();
res.send('Server config saved successfully');
} catch (error) {
console.error(error);
res.status(500).send('Error writing server config to S3');
}
});
// Delete specific server config
app.delete('/api/server-configs/:serverId', async (req, res) => {
const { serverId } = req.params;
const params = {
Bucket: BUCKET_NAME,
Key: `config-${serverId}.txt`,
};
try {
await s3.deleteObject(params).promise();
res.send('Server config deleted successfully');
} catch (error) {
console.error(error);
res.status(500).send('Error deleting server config from S3');
}
});
// --- Server Filters Routes ---
// Get server filters
app.get('/api/server-filters/:serverId', async (req, res) => {
const { serverId } = req.params;
const params = {
Bucket: BUCKET_NAME,
Key: `server-filters-${serverId}.json`,
};
try {
const data = await s3.getObject(params).promise();
const fileContent = data.Body.toString('utf-8');
let filters = [];
try {
filters = JSON.parse(fileContent);
if (!Array.isArray(filters)) {
filters = [];
}
} catch (parseError) {
console.error('Error parsing server filters:', parseError);
filters = [];
}
res.json(filters);
} 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 server filters from S3');
}
}
});
// Update server filters
app.post('/api/server-filters/:serverId', async (req, res) => {
const { serverId } = req.params;
const { filters } = req.body;
// Validate filters structure
if (!Array.isArray(filters)) {
return res.status(400).send('Filters must be an array');
}
// Validate each filter has required fields
for (let i = 0; i < filters.length; i++) {
const filter = filters[i];
if (!filter.community || !filter.gateway) {
return res.status(400).send(`Filter at index ${i} is missing required fields`);
}
}
const params = {
Bucket: BUCKET_NAME,
Key: `server-filters-${serverId}.json`,
Body: JSON.stringify(filters, null, 2), // Pretty print JSON
ContentType: 'application/json',
};
try {
await s3.putObject(params).promise();
res.send('Server filters updated successfully');
} catch (error) {
console.error(error);
res.status(500).send('Error writing server filters to S3');
}
});
// Generate MikroTik configuration from server filters
app.post('/api/server-filters/generate-config', async (req, res) => {
const { filters } = req.body;
if (!Array.isArray(filters) || filters.length === 0) {
return res.json({ config: '// No filters to generate configuration' });
}
// Group filters by gateway
const gatewayGroups = {};
filters.forEach(filter => {
if (!gatewayGroups[filter.gateway]) {
gatewayGroups[filter.gateway] = [];
}
gatewayGroups[filter.gateway].push(filter.community);
});
let config = '// Frouting filter configuration for MikroTik 7.14+\n';
config += '// Generated automatically\n';
config += `// Date: ${new Date().toISOString()}\n\n`;
// Если есть только один gateway, создаем простую конфигурацию
if (Object.keys(gatewayGroups).length === 1) {
const [gateway, communities] = Object.entries(gatewayGroups)[0];
config += `if (\n`;
communities.forEach((community, index) => {
config += `(bgp-communities includes ${community})`;
if (index < communities.length - 1) {
config += ` \nor `;
}
});
config += `\n)\n{\n set gw ${gateway}; accept;\n}\nelse\n{\n reject;\n}\n`;
} else {
// Если несколько gateway, создаем каскадную структуру
const gatewayEntries = Object.entries(gatewayGroups);
gatewayEntries.forEach(([gateway, communities], gatewayIndex) => {
if (gatewayIndex === 0) {
config += `if (\n`;
} else {
config += `else if (\n`;
}
communities.forEach((community, index) => {
config += `(bgp-communities includes ${community})`;
if (index < communities.length - 1) {
config += ` \nor `;
}
});
config += `\n)\n{\n set gw ${gateway}; accept;\n}`;
if (gatewayIndex === gatewayEntries.length - 1) {
config += `\nelse\n{\n reject;\n}\n`;
} else {
config += `\n`;
}
});
}
res.json({ config });
});
// The "catchall" handler: for any request that doesn't
// match one above, send back React's index.html file.
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});