diff --git a/backend/server.js b/backend/server.js index 7823ad7..bbdbb8d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -648,6 +648,137 @@ app.delete('/api/server-configs/:serverId', async (req, res) => { } }); +// --- 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) => { diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index 5e37d43..57c9b7a 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -25,58 +25,51 @@ import { const API_URL = '/api'; function FilterManager() { - const [filters, setFilters] = useState([]); - const [newFilter, setNewFilter] = useState({ - community: '', - gateway: '', - description: '' - }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); - const [searchTerm, setSearchTerm] = useState(''); - const [editingFilter, setEditingFilter] = useState(null); - const [editingValues, setEditingValues] = useState({}); const [loading, setLoading] = useState(false); - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [filterToDelete, setFilterToDelete] = useState(null); - const [currentPage, setCurrentPage] = useState(1); - const [sortField, setSortField] = useState('community'); - const [sortOrder, setSortOrder] = useState('asc'); - const [filterGateway, setFilterGateway] = useState(''); - const editInputRef = useRef(null); - const pageSize = 10; - - const [editModalOpen, setEditModalOpen] = useState(false); - const [editModalFilter, setEditModalFilter] = useState(null); // Состояние для предварительного просмотра конфигурации const [previewModalOpen, setPreviewModalOpen] = useState(false); const [generatedConfig, setGeneratedConfig] = useState(''); - // Состояние для модального окна добавления фильтра - const [addFilterModalOpen, setAddFilterModalOpen] = useState(false); - - // Новые состояния для управления серверами + // Состояния для управления серверами const [servers, setServers] = useState([]); const [selectedServer, setSelectedServer] = useState(null); const [serverConfigModalOpen, setServerConfigModalOpen] = useState(false); const [newServer, setNewServer] = useState({ name: '', description: '', enabled: true }); - const [serverConfigs, setServerConfigs] = useState({}); + + // Состояния для управления фильтрами сервера + const [serverFilters, setServerFilters] = useState([]); + const [newFilter, setNewFilter] = useState({ community: '', gateway: '', description: '' }); + const [addFilterModalOpen, setAddFilterModalOpen] = useState(false); + const [editFilterModalOpen, setEditFilterModalOpen] = useState(false); + const [editingFilter, setEditingFilter] = useState(null); + const [deleteFilterModalOpen, setDeleteFilterModalOpen] = useState(false); + const [filterToDelete, setFilterToDelete] = useState(null); useEffect(() => { - fetchFilters(); fetchServers(); }, []); - const fetchFilters = async () => { + // Загружаем фильтры при выборе сервера + useEffect(() => { + if (selectedServer) { + fetchServerFilters(selectedServer.id); + } else { + setServerFilters([]); + } + }, [selectedServer]); + + const fetchServerFilters = async (serverId) => { setLoading(true); try { - const response = await axios.get(`${API_URL}/filters`); - setFilters(response.data); + const response = await axios.get(`${API_URL}/server-filters/${serverId}`); + setServerFilters(response.data); setError(''); } catch (error) { - console.error('Error fetching filters:', error); - setError('Не удалось загрузить фильтры. Проверьте, запущен ли бэкенд.'); + console.error('Error fetching server filters:', error); + setServerFilters([]); // Если файл не существует, создаем пустой массив } finally { setLoading(false); } @@ -301,139 +294,92 @@ function FilterManager() { } }; - const handleAddFilter = () => { - if (!newFilter.community.trim() || !newFilter.gateway.trim()) { - setError('Поля Community и Gateway должны быть заполнены.'); + const handleAddFilter = async () => { + if (!selectedServer) { + setError('Сначала выберите сервер.'); return; } - setError(''); - setFilters([...filters, { ...newFilter }]); - setNewFilter({ community: '', gateway: '', description: '' }); - }; - - // Функции для модального окна добавления фильтра - const handleOpenAddFilterModal = () => { - setAddFilterModalOpen(true); - }; - - const handleCloseAddFilterModal = () => { - setAddFilterModalOpen(false); - setNewFilter({ community: '', gateway: '', description: '' }); - setError(''); - }; - - const handleAddFilterFromModal = () => { if (!newFilter.community.trim() || !newFilter.gateway.trim()) { setError('Поля Community и Gateway должны быть заполнены.'); return; } setError(''); - setFilters([...filters, { ...newFilter }]); + const updatedFilters = [...serverFilters, { ...newFilter }]; + setServerFilters(updatedFilters); setNewFilter({ community: '', gateway: '', description: '' }); setAddFilterModalOpen(false); - setSuccess('Фильтр успешно добавлен!'); - setTimeout(() => setSuccess(''), 3000); - }; - - const handleEdit = (filter) => { - setEditModalFilter({ ...filter }); - setEditModalOpen(true); - }; - - const handleEditModalChange = (updatedFilter) => { - setEditModalFilter(updatedFilter); - }; - - const handleEditModalSave = (updatedFilter) => { - setFilters(filters.map(f => f.community === updatedFilter.community ? { ...updatedFilter } : f)); - setEditModalOpen(false); - setEditModalFilter(null); - }; - - const handleEditModalClose = () => { - setEditModalOpen(false); - setEditModalFilter(null); - }; - - const handleDelete = (filter) => { - setFilterToDelete(filter); - setShowDeleteModal(true); - }; - - const confirmDelete = () => { - if (filterToDelete) { - setFilters(filters.filter(f => f.community !== filterToDelete.community)); - setShowDeleteModal(false); - setFilterToDelete(null); - setSuccess('Фильтр удален!'); - setTimeout(() => setSuccess(''), 3000); - } - }; - - const handleSaveChanges = async () => { - setLoading(true); + + // Сохраняем фильтры сервера try { - await axios.post(`${API_URL}/filters`, { domains: filters }); - setSuccess('Изменения сохранены в S3!'); + await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters }); + setSuccess('Фильтр успешно добавлен!'); setTimeout(() => setSuccess(''), 3000); } catch (error) { - console.error('Error saving filters:', error); - setError('Не удалось сохранить изменения.'); - } finally { - setLoading(false); + console.error('Error saving server filters:', error); + setError('Не удалось сохранить фильтр.'); } }; - const handleSort = (field) => { - if (sortField === field) { - setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); - } else { - setSortField(field); - setSortOrder('asc'); - } + const handleEditFilter = (filter) => { + setEditingFilter({ ...filter }); + setEditFilterModalOpen(true); }; - const handleEditKeyDown = (e, filterCommunity) => { - if (e.key === 'Enter') { - handleSaveEdit(filterCommunity); - } else if (e.key === 'Escape') { - handleCancelEdit(filterCommunity); - } - }; - - const handleSaveEdit = (filterCommunity) => { - const filter = filters.find(f => f.community === filterCommunity); - if (filter && editingValues[filterCommunity]) { - setFilters(filters.map(f => - f.community === filterCommunity - ? { ...f, ...editingValues[filterCommunity] } - : f - )); - setEditingFilter(null); - setEditingValues({}); - } - }; - - const handleCancelEdit = (filterCommunity) => { + const handleSaveEditFilter = async (updatedFilter) => { + const updatedFilters = serverFilters.map(f => + f.community === updatedFilter.community ? { ...updatedFilter } : f + ); + setServerFilters(updatedFilters); + setEditFilterModalOpen(false); setEditingFilter(null); - setEditingValues({}); - }; - - const startEdit = (filter) => { - setEditingFilter(filter.community); - setEditingValues({ [filter.community]: { ...filter } }); - setTimeout(() => { - if (editInputRef.current) { - editInputRef.current.focus(); - } - }, 100); - }; - - // Функция для генерации конфигурации MikroTik - const generateMikrotikConfig = async () => { + + // Сохраняем фильтры сервера try { - const response = await axios.get(`${API_URL}/filters/generate-config`); + await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters }); + setSuccess('Фильтр успешно обновлен!'); + setTimeout(() => setSuccess(''), 3000); + } catch (error) { + console.error('Error saving server filters:', error); + setError('Не удалось сохранить фильтр.'); + } + }; + + const handleDeleteFilter = async (filter) => { + const updatedFilters = serverFilters.filter(f => f.community !== filter.community); + setServerFilters(updatedFilters); + setDeleteFilterModalOpen(false); + setFilterToDelete(null); + + // Сохраняем фильтры сервера + try { + await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters }); + setSuccess('Фильтр успешно удален!'); + setTimeout(() => setSuccess(''), 3000); + } catch (error) { + console.error('Error saving server filters:', error); + setError('Не удалось удалить фильтр.'); + } + }; + + // Удаляем старые функции редактирования + + // Удаляем старые функции, которые больше не нужны + + // Функция для генерации конфигурации MikroTik для выбранного сервера + const generateMikrotikConfig = async () => { + if (!selectedServer) { + return '// Сначала выберите сервер'; + } + + if (serverFilters.length === 0) { + return '// Нет фильтров для генерации конфигурации'; + } + + try { + const response = await axios.post(`${API_URL}/server-filters/generate-config`, { + filters: serverFilters + }); return response.data.config; } catch (error) { console.error('Error generating config:', error); @@ -496,26 +442,7 @@ function FilterManager() { } }; - // Фильтрация и сортировка - const filteredFilters = filters - .filter(filter => { - const matchesSearch = filter.community.toLowerCase().includes(searchTerm.toLowerCase()) || - filter.gateway.toLowerCase().includes(searchTerm.toLowerCase()) || - filter.description.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesGateway = !filterGateway || filter.gateway === filterGateway; - return matchesSearch && matchesGateway; - }) - .sort((a, b) => { - const aValue = a[sortField] || ''; - const bValue = b[sortField] || ''; - if (sortOrder === 'asc') { - return aValue.localeCompare(bValue); - } else { - return bValue.localeCompare(aValue); - } - }); - - const uniqueGateways = [...new Set(filters.map(f => f.gateway))].sort(); + // Удаляем старые переменные фильтрации return (
{server.description}
)}Нет фильтров для этого сервера
+ +| COMMUNITY | +GATEWAY | +ОПИСАНИЕ | +ДЕЙСТВИЯ | +
|---|---|---|---|
| + {filter.community} + | ++ {filter.gateway} + | +{filter.description || '-'} | +
+
+
+
+
+ |
+
| handleSort('community')}> - COMMUNITY - {sortField === 'community' && ( - - {sortOrder === 'asc' ? '▲' : '▼'} - - )} - | -handleSort('gateway')}> - GATEWAY - {sortField === 'gateway' && ( - - {sortOrder === 'asc' ? '▲' : '▼'} - - )} - | -ОПИСАНИЕ | -ДЕЙСТВИЯ | -
|---|---|---|---|
| - {filter.community} - | -- {filter.gateway} - | -{filter.description || '-'} | -
-
-
-
-
- |
-