/** * Роуты для работы с communities (справочник BGP Community) */ const { s3, BUCKET_NAME, writeS3JsonObject, invalidateCacheForKey } = require('../services/s3Service'); const { sendError, sendOk } = require('../middleware/errorHandler'); const { GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3'); const { streamToString } = require('../services/s3Service'); const validators = require('../lib/validators'); const { getS3JsonWithHeaders } = require('../utils/s3Helpers'); const S3_KEY = 'bgp_data/communities.json'; // GET /api/communities async function getCommunities(req, res) { await getS3JsonWithHeaders(S3_KEY, req, res, { transform: (communities) => { // Нормализация return communities .filter((c) => c && typeof c.value === 'string' && c.value.trim().length > 0) .map((c) => ({ value: String(c.value).trim(), name: c.name ? String(c.name) : '', description: c.description ? String(c.description) : '', tags: Array.isArray(c.tags) ? c.tags.map(String) : [], gatewayDefault: c.gatewayDefault ? String(c.gatewayDefault) : '', color: c.color ? String(c.color) : '' })); }, defaultValue: [] }); } // POST /api/communities async function postCommunities(req, res) { const { communities } = req.body; if (!Array.isArray(communities)) { return sendError(res, 400, 'communities must be an array', 'E_BAD_REQUEST'); } const seen = new Set(); const normalized = []; const validationErrors = []; for (let i = 0; i < communities.length; i++) { const entry = communities[i] || {}; const value = typeof entry.value === 'string' ? entry.value.trim() : ''; if (!value) { validationErrors.push(`Community at index ${i} is missing required field: value`); continue; } if (!validators.isValidCommunity(value)) { validationErrors.push(`Community at index ${i} has invalid value: ${value}`); continue; } if (seen.has(value)) { validationErrors.push(`Duplicate community value at index ${i}: ${value}`); continue; } seen.add(value); normalized.push({ value, name: entry.name ? String(entry.name) : '', description: entry.description ? String(entry.description) : '', tags: Array.isArray(entry.tags) ? entry.tags.map(String) : [], gatewayDefault: entry.gatewayDefault ? String(entry.gatewayDefault) : '', color: entry.color ? String(entry.color) : '', category: entry.category ? String(entry.category) : '', priority: typeof entry.priority === 'number' ? entry.priority : 0, enabled: typeof entry.enabled === 'boolean' ? entry.enabled : true, }); } if (validationErrors.length > 0) { return sendError(res, 400, 'Validation errors', 'E_VALIDATION', { errors: validationErrors }); } try { await s3.send(new PutObjectCommand({ Bucket: BUCKET_NAME, Key: S3_KEY, Body: JSON.stringify(normalized, null, 2), ContentType: 'application/json', })); invalidateCacheForKey(S3_KEY); const { headMeta } = require('../services/s3Service'); const meta = await headMeta(S3_KEY); return sendOk(res, meta); } catch (error) { console.error('Error writing communities to S3:', error); return sendError(res, 500, 'Error writing communities to S3', 'E_S3'); } } // GET /api/communities/stats async function getCommunityStats(req, res) { try { const [domainsRes, ipRangesRes, asnsRes, filtersRes] = await Promise.allSettled([ s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/domains_community.txt' })), s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/ips.txt' })), s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'bgp_data/asns.txt' })), s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: 'filter-manager/simple-filters.json' })), ]); const communityUsage = new Map(); // Подсчет использования в доменах if (domainsRes.status === 'fulfilled') { const text = await streamToString(domainsRes.value.Body); text.split('\n').filter(Boolean).forEach(line => { const parts = line.trim().split(/\s+/); if (parts[1]) { communityUsage.set(parts[1], (communityUsage.get(parts[1]) || 0) + 1); } }); } // Подсчет в IP ranges if (ipRangesRes.status === 'fulfilled') { const text = await streamToString(ipRangesRes.value.Body); text.split('\n').filter(Boolean).forEach(line => { const parts = line.trim().split(/\s+/); if (parts[1]) { communityUsage.set(parts[1], (communityUsage.get(parts[1]) || 0) + 1); } }); } // Подсчет в ASNs if (asnsRes.status === 'fulfilled') { const text = await streamToString(asnsRes.value.Body); text.split('\n').filter(Boolean).forEach(line => { const parts = line.trim().split(/\s+/); if (parts[1]) { communityUsage.set(parts[1], (communityUsage.get(parts[1]) || 0) + 1); } }); } // Подсчет в фильтрах if (filtersRes.status === 'fulfilled') { try { const text = await streamToString(filtersRes.value.Body); const filters = JSON.parse(text); if (Array.isArray(filters)) { filters.forEach(f => { if (f.community) { communityUsage.set(f.community, (communityUsage.get(f.community) || 0) + 1); } }); } } catch {} } const stats = Array.from(communityUsage.entries()).map(([community, count]) => ({ community, count, })).sort((a, b) => b.count - a.count); res.json({ stats, total: stats.reduce((sum, s) => sum + s.count, 0) }); } catch (error) { console.error('Error getting community stats:', error); return sendError(res, 500, 'Error getting community stats', 'E_S3'); } } module.exports = { getCommunities, postCommunities, getCommunityStats, };