Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m35s
571 lines
23 KiB
JavaScript
571 lines
23 KiB
JavaScript
// Оптимизированная версия server.js с модульной структурой
|
|
require('dotenv').config();
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const path = require('path');
|
|
const compression = require('compression');
|
|
const Ajv = require('ajv');
|
|
const helmet = require('helmet');
|
|
const rateLimit = require('express-rate-limit');
|
|
const pino = require('pino');
|
|
const pinoHttp = require('pino-http');
|
|
const promClient = require('prom-client');
|
|
const crypto = require('crypto');
|
|
|
|
// Импорт модулей
|
|
const { sendError, sendOk, errorHandler } = require('./middleware/errorHandler');
|
|
const { getLockStatus, acquireLock, releaseLock } = require('./middleware/lockManager');
|
|
const { createTextDataRoutes } = require('./routes/textDataRoutes');
|
|
const { createJsonDataRoutes } = require('./routes/jsonDataRoutes');
|
|
const { splitWhitespace } = require('./utils/helpers');
|
|
const validators = require('./lib/validators');
|
|
const mikrotikValidator = require('./lib/mikrotik-validator');
|
|
|
|
// Роуты
|
|
const communitiesRoutes = require('./routes/communitiesRoutes');
|
|
const filtersRoutes = require('./routes/filtersRoutes');
|
|
const serverConfigsRoutes = require('./routes/serverConfigsRoutes');
|
|
const miscRoutes = require('./routes/miscRoutes');
|
|
const ipsecPasswordsRoutes = require('./routes/ipsecPasswordsRoutes');
|
|
const mikrotikConfigRoutes = require('./routes/mikrotikConfigRoutes');
|
|
const mikrotikBackupRoutes = require('./routes/mikrotikBackupRoutes');
|
|
const trafficRoutes = require('./routes/trafficRoutes');
|
|
const resourceStatsRoutes = require('./routes/resourceStatsRoutes');
|
|
const alertsRoutes = require('./routes/alertsRoutes');
|
|
const routeOptimizerRoutes = require('./routes/routeOptimizerRoutes');
|
|
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
|
|
const { initNetworkMapScheduler } = require('./services/networkMapScheduler');
|
|
const { initPingServicesScheduler } = require('./services/pingServicesScheduler');
|
|
const { initUptimeMonitorScheduler } = require('./services/uptimeMonitorScheduler');
|
|
const schedulerRoutes = require('./routes/schedulerRoutes');
|
|
|
|
const app = express();
|
|
const port = Number(process.env.PORT) || 3001;
|
|
|
|
// === Logger ===
|
|
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
|
app.use(pinoHttp({
|
|
logger,
|
|
genReqId: (req) => req.headers['x-request-id'] || crypto.randomBytes(8).toString('hex'),
|
|
serializers: {
|
|
req(req) { return { id: req.id, method: req.method, url: req.url }; },
|
|
res(res) { return { statusCode: res.statusCode }; },
|
|
},
|
|
}));
|
|
|
|
// === CORS ===
|
|
const allowed = (process.env.CORS_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
if (allowed.length > 0) {
|
|
app.use(cors({
|
|
origin: (origin, cb) => {
|
|
if (!origin || allowed.includes(origin)) return cb(null, true);
|
|
return cb(new Error('CORS blocked'));
|
|
},
|
|
credentials: true,
|
|
}));
|
|
} else {
|
|
app.use(cors({ origin: true, credentials: true }));
|
|
}
|
|
|
|
app.options('*', cors());
|
|
|
|
// === Security ===
|
|
app.use(helmet({
|
|
contentSecurityPolicy: false,
|
|
crossOriginEmbedderPolicy: false,
|
|
crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups' },
|
|
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
|
}));
|
|
app.set('trust proxy', 1);
|
|
app.disable('x-powered-by');
|
|
|
|
// === Rate Limiting ===
|
|
const createRateLimiter = (windowMs, max, message) => rateLimit({
|
|
windowMs,
|
|
max,
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
message: { code: 'E_RATE_LIMIT', message: message || 'Слишком много запросов' },
|
|
});
|
|
|
|
const generalLimiter = createRateLimiter(15 * 60 * 1000, 1000);
|
|
const writeLimiter = createRateLimiter(5 * 60 * 1000, 100, 'Слишком много операций записи');
|
|
const bgpUpdateLimiter = createRateLimiter(1 * 60 * 1000, 5, 'Слишком частые BGP обновления');
|
|
|
|
app.use(generalLimiter);
|
|
app.use(express.json({ limit: process.env.JSON_LIMIT || '1mb' }));
|
|
app.use(compression());
|
|
app.set('etag', false);
|
|
|
|
app.use((req, res, next) => {
|
|
res.setHeader('Access-Control-Expose-Headers', 'ETag, Last-Modified, Content-Length-Source');
|
|
// Запрещаем браузеру кешировать API ответы
|
|
if (req.path.startsWith('/api/')) {
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
}
|
|
next();
|
|
});
|
|
|
|
// === Metrics ===
|
|
promClient.collectDefaultMetrics();
|
|
const httpDuration = new promClient.Histogram({
|
|
name: 'http_request_duration_seconds',
|
|
help: 'HTTP request duration',
|
|
labelNames: ['method', 'route', 'code'],
|
|
buckets: [0.05,0.1,0.2,0.5,1,2,5]
|
|
});
|
|
const httpErrors = new promClient.Counter({
|
|
name: 'http_errors_total',
|
|
help: 'HTTP error count',
|
|
labelNames: ['route','code']
|
|
});
|
|
|
|
app.use((req, res, next) => {
|
|
const start = process.hrtime.bigint();
|
|
res.on('finish', () => {
|
|
try {
|
|
const dur = Number(process.hrtime.bigint() - start) / 1e9;
|
|
httpDuration.labels(req.method, req.route?.path || req.path, String(res.statusCode)).observe(dur);
|
|
if (res.statusCode >= 400) {
|
|
httpErrors.labels(req.route?.path || req.path, String(res.statusCode)).inc();
|
|
}
|
|
} catch {}
|
|
});
|
|
next();
|
|
});
|
|
|
|
// === Health & Metrics ===
|
|
app.get('/health', (req, res) => res.json({ ok: true }));
|
|
app.get('/ready', (req, res) => res.json({ ok: true }));
|
|
app.get('/metrics', async (req, res) => {
|
|
try {
|
|
res.set('Content-Type', promClient.register.contentType);
|
|
res.end(await promClient.register.metrics());
|
|
} catch (e) {
|
|
res.status(500).end(String(e?.message || e));
|
|
}
|
|
});
|
|
|
|
app.get('/api/version', (req, res) => {
|
|
res.json({
|
|
version: process.env.APP_VERSION || null,
|
|
gitSha: process.env.GIT_SHA || null,
|
|
buildAt: process.env.BUILD_AT || null
|
|
});
|
|
});
|
|
|
|
// === Cache TTL ===
|
|
const DEFAULT_CACHE_TTL = Math.max(0, Math.min(300, Number(process.env.CACHE_TTL_SECONDS) || 30));
|
|
app.use((req, res, next) => {
|
|
if (req.method === 'GET') {
|
|
res.set('Cache-Control', `private, max-age=${DEFAULT_CACHE_TTL}`);
|
|
}
|
|
next();
|
|
});
|
|
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// === AJV Schemas ===
|
|
const ajv = new Ajv({ allErrors: true, removeAdditional: 'failing' });
|
|
|
|
const schemaDomainsNew = {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
required: ['domain', 'community'],
|
|
additionalProperties: false,
|
|
properties: {
|
|
domain: { type: 'string' },
|
|
community: { type: 'string' }
|
|
}
|
|
}
|
|
};
|
|
|
|
const schemaAsns = {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
required: ['domain', 'type'],
|
|
additionalProperties: false,
|
|
properties: {
|
|
domain: { type: 'string' },
|
|
type: { type: 'string' }
|
|
}
|
|
}
|
|
};
|
|
|
|
const schemaIpRanges = {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
required: ['ipRange', 'community'],
|
|
additionalProperties: false,
|
|
properties: {
|
|
ipRange: { type: 'string' },
|
|
community: { type: 'string' }
|
|
}
|
|
}
|
|
};
|
|
|
|
const validateDomainsNew = ajv.compile(schemaDomainsNew);
|
|
const validateAsns = ajv.compile(schemaAsns);
|
|
const validateIpRanges = ajv.compile(schemaIpRanges);
|
|
|
|
// === Domains (старая версия) ===
|
|
const domainsOldRoutes = createTextDataRoutes({
|
|
s3Key: 'bgp_data/domains.txt',
|
|
mapLine: (line) => {
|
|
const parts = splitWhitespace(line);
|
|
return { domain: parts[0] || '', type: parts[1] || '' };
|
|
},
|
|
formatLine: (d) => `${String(d.domain || '').trim()} ${String(d.type || '').trim()}`.trim(),
|
|
validate: validateAsns,
|
|
cachePrefix: 'domains'
|
|
});
|
|
|
|
app.get('/api/domains', domainsOldRoutes.get);
|
|
app.post('/api/domains', domainsOldRoutes.post);
|
|
|
|
// === ASNs ===
|
|
const asnsRoutes = createTextDataRoutes({
|
|
s3Key: 'bgp_data/asns.txt',
|
|
mapLine: (line) => {
|
|
const parts = splitWhitespace(line);
|
|
return { domain: parts[0] || '', type: parts[1] || '' };
|
|
},
|
|
formatLine: (a) => `${String(a.domain || '').trim().toUpperCase()} ${String(a.type || '').trim()}`.trim(),
|
|
validate: validateAsns,
|
|
validateItem: (asn, i) => {
|
|
const errors = [];
|
|
if (!validators.isValidASN(asn.domain)) {
|
|
errors.push(`Элемент ${i}: неверный ASN "${asn.domain}"`);
|
|
}
|
|
if (!validators.isValidCommunity(asn.type)) {
|
|
errors.push(`Элемент ${i}: неверный community "${asn.type}"`);
|
|
}
|
|
return errors;
|
|
},
|
|
cachePrefix: 'asns'
|
|
});
|
|
|
|
app.get('/api/asns', asnsRoutes.get);
|
|
app.post('/api/asns', writeLimiter, asnsRoutes.post);
|
|
|
|
// === Domains New ===
|
|
const domainsNewRoutes = createTextDataRoutes({
|
|
s3Key: 'bgp_data/domains_community.txt',
|
|
mapLine: (line) => {
|
|
const parts = splitWhitespace(line);
|
|
return { domain: parts[0] || '', community: parts[1] || '' };
|
|
},
|
|
formatLine: (d) => `${String(d.domain || '').trim().toLowerCase()} ${String(d.community || '').trim()}`.trim(),
|
|
validate: validateDomainsNew,
|
|
validateItem: (d, i) => {
|
|
const errors = [];
|
|
if (!validators.isValidDomain(d.domain) && !validators.isValidWildcardDomain(d.domain)) {
|
|
errors.push(`Элемент ${i}: неверный домен "${d.domain}"`);
|
|
}
|
|
if (!validators.isValidCommunity(d.community)) {
|
|
errors.push(`Элемент ${i}: неверный community "${d.community}"`);
|
|
}
|
|
if (!validators.isSafeXSSString(d.domain)) {
|
|
errors.push(`Элемент ${i}: домен содержит потенциально опасные символы`);
|
|
}
|
|
return errors;
|
|
},
|
|
cachePrefix: 'domains-new'
|
|
});
|
|
|
|
app.get('/api/domains-new', domainsNewRoutes.get);
|
|
app.post('/api/domains-new', writeLimiter, domainsNewRoutes.post);
|
|
|
|
// === IP Ranges ===
|
|
const ipRangesRoutes = createTextDataRoutes({
|
|
s3Key: 'bgp_data/ips.txt',
|
|
mapLine: (line) => {
|
|
const parts = splitWhitespace(line);
|
|
return { ipRange: parts[0] || '', community: parts[1] || '' };
|
|
},
|
|
formatLine: (ip) => `${String(ip.ipRange || '').trim()} ${String(ip.community || '').trim()}`.trim(),
|
|
validate: validateIpRanges,
|
|
validateItem: (ip, i) => {
|
|
const errors = [];
|
|
const ipStr = String(ip.ipRange || '').trim();
|
|
const isValidCIDR = validators.isValidCIDRv4(ipStr) || validators.isValidCIDRv6(ipStr);
|
|
const isValidIP = validators.isValidIPv4(ipStr) || validators.isValidIPv6(ipStr);
|
|
|
|
if (!isValidCIDR && !isValidIP) {
|
|
errors.push(`Элемент ${i}: неверный IP/CIDR "${ipStr}"`);
|
|
}
|
|
if (!validators.isValidCommunity(ip.community)) {
|
|
errors.push(`Элемент ${i}: неверный community "${ip.community}"`);
|
|
}
|
|
return errors;
|
|
},
|
|
cachePrefix: 'ip-ranges'
|
|
});
|
|
|
|
app.get('/api/ip-ranges', ipRangesRoutes.get);
|
|
app.post('/api/ip-ranges', writeLimiter, ipRangesRoutes.post);
|
|
|
|
// === JSON DATA ROUTES (используют фабрики) ===
|
|
|
|
// Servers (кастомные роуты с поддержкой зашифрованных MikroTik учётных данных для jumphost)
|
|
const serversRoutes = require('./routes/serversRoutes');
|
|
app.get('/api/servers', serversRoutes.getServers);
|
|
app.post('/api/servers', serversRoutes.postServers);
|
|
|
|
// Server connections for topology graph
|
|
const serverConnectionsRoutes = createJsonDataRoutes('server-connections.json', (conn, i) => {
|
|
if (!conn.from || !conn.to || !conn.tunnelType) {
|
|
return `Connection at index ${i} is missing required fields: from, to, tunnelType`;
|
|
}
|
|
if (conn.from === conn.to) {
|
|
return `Connection at index ${i} has identical from/to values`;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
app.get('/api/server-connections', serverConnectionsRoutes.get);
|
|
app.post('/api/server-connections', serverConnectionsRoutes.post);
|
|
|
|
// Billing
|
|
const billingRoutes = createJsonDataRoutes('servers-billing.json', (item, i) => {
|
|
if (!item.hostName || !item.country || !item.provider) {
|
|
return `Billing item at index ${i} is missing required fields: hostName, country, provider`;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
app.get('/api/billing', billingRoutes.get);
|
|
app.post('/api/billing', billingRoutes.post);
|
|
|
|
// Filters
|
|
const filtersRoutes2 = createJsonDataRoutes('filters.json', (filter, i) => {
|
|
if (!filter.community || !filter.gateway) {
|
|
return `Filter at index ${i} is missing required fields`;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
app.get('/api/filters', filtersRoutes2.get);
|
|
app.post('/api/filters', filtersRoutes2.post);
|
|
|
|
// Simple Filters
|
|
const simpleFiltersRoutes = createJsonDataRoutes('filter-manager/simple-filters.json', (filter, i) => {
|
|
if (!filter.community || !filter.gateway) {
|
|
return `Filter at index ${i} is missing required fields`;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
app.get('/api/simple-filters', simpleFiltersRoutes.get);
|
|
app.post('/api/simple-filters', simpleFiltersRoutes.post);
|
|
|
|
// Network Config (справочник IP, интерфейсов и gateway)
|
|
const networkConfigRoutes = createJsonDataRoutes('network-config.json', (config) => {
|
|
// Валидация структуры конфига
|
|
if (typeof config !== 'object' || config === null) {
|
|
return 'Network config must be an object';
|
|
}
|
|
// Валидация gateways
|
|
if (config.gateways && !Array.isArray(config.gateways)) {
|
|
return 'gateways must be an array';
|
|
}
|
|
// Валидация tunnelInterfaces
|
|
if (config.tunnelInterfaces && !Array.isArray(config.tunnelInterfaces)) {
|
|
return 'tunnelInterfaces must be an array';
|
|
}
|
|
// Валидация ipPools
|
|
if (config.ipPools && !Array.isArray(config.ipPools)) {
|
|
return 'ipPools must be an array';
|
|
}
|
|
return null;
|
|
}, { singleObject: true });
|
|
|
|
app.get('/api/network-config', networkConfigRoutes.get);
|
|
app.post('/api/network-config', networkConfigRoutes.post);
|
|
|
|
// === COMMUNITIES ROUTES ===
|
|
app.get('/api/communities', communitiesRoutes.getCommunities);
|
|
app.post('/api/communities', writeLimiter, communitiesRoutes.postCommunities);
|
|
app.get('/api/communities/stats', communitiesRoutes.getCommunityStats);
|
|
|
|
// === FILTERS & MIKROTIK CONFIG ROUTES ===
|
|
app.get('/api/filters/generate-config', filtersRoutes.generateConfig);
|
|
app.post('/api/filters/export-config', filtersRoutes.exportConfig);
|
|
app.post('/api/server-filters/generate-config', filtersRoutes.generateServerFilterConfig);
|
|
|
|
// === SERVER CONFIGS ROUTES ===
|
|
app.get('/api/server-configs', serverConfigsRoutes.serverConfigsListRoutes.get);
|
|
app.post('/api/server-configs', serverConfigsRoutes.serverConfigsListRoutes.post);
|
|
app.get('/api/server-configs/:serverId', serverConfigsRoutes.getServerConfig);
|
|
app.post('/api/server-configs/:serverId', serverConfigsRoutes.postServerConfig);
|
|
app.delete('/api/server-configs/:serverId', serverConfigsRoutes.deleteServerConfig);
|
|
app.delete('/api/server-configs/:serverId/complete', serverConfigsRoutes.deleteServerComplete);
|
|
|
|
// Server Filters
|
|
app.get('/api/server-filters/:serverId', serverConfigsRoutes.getServerFilters);
|
|
app.post('/api/server-filters/:serverId', serverConfigsRoutes.postServerFilters);
|
|
|
|
// === LOCKS ===
|
|
app.get('/api/locks/:resource', (req, res) => {
|
|
const status = getLockStatus(req.params.resource);
|
|
res.json(status);
|
|
});
|
|
|
|
app.post('/api/locks/:resource', (req, res) => {
|
|
const { owner = 'anonymous', ttlSeconds = 120 } = req.body || {};
|
|
const result = acquireLock(req.params.resource, owner, ttlSeconds);
|
|
if (!result.success) {
|
|
return sendError(res, 423, 'Resource is locked by another user', 'E_RESOURCE_LOCKED', {
|
|
owner: result.owner,
|
|
expiresAt: result.expiresAt
|
|
});
|
|
}
|
|
res.json(result);
|
|
});
|
|
|
|
app.delete('/api/locks/:resource', (req, res) => {
|
|
const result = releaseLock(req.params.resource);
|
|
res.json(result);
|
|
});
|
|
|
|
// === HISTORY ===
|
|
app.get('/api/history/:resource', miscRoutes.getHistory);
|
|
app.post('/api/history/:resource/rollback', miscRoutes.postRollback);
|
|
|
|
// === MISC ROUTES ===
|
|
app.get('/api/s3/last-modified', miscRoutes.getS3LastModified);
|
|
app.get('/api/auto-urls', miscRoutes.getAutoUrls);
|
|
app.post('/api/auto-urls', miscRoutes.postAutoUrls);
|
|
app.post('/api/auto-urls/process', miscRoutes.processAutoUrls);
|
|
app.get('/api/servers/availability', miscRoutes.getServersAvailability);
|
|
app.post('/api/update-bgp/background', bgpUpdateLimiter, miscRoutes.updateBgpBackground);
|
|
app.get('/api/ws/url', miscRoutes.getWsUrl);
|
|
app.get('/api/ui-settings', miscRoutes.getUiSettings);
|
|
app.post('/api/ui-settings', miscRoutes.postUiSettings);
|
|
app.get('/api/ping-services-list', miscRoutes.getPingServicesList);
|
|
app.get('/api/ping-services', miscRoutes.getPingServices);
|
|
|
|
// === IPSEC PASSWORDS ===
|
|
app.get('/api/ipsec-passwords', ipsecPasswordsRoutes.getIpsecPasswords);
|
|
app.get('/api/ipsec-passwords/:id', ipsecPasswordsRoutes.getIpsecPassword);
|
|
app.post('/api/ipsec-passwords', writeLimiter, ipsecPasswordsRoutes.createIpsecPassword);
|
|
app.put('/api/ipsec-passwords/:id', writeLimiter, ipsecPasswordsRoutes.updateIpsecPassword);
|
|
app.delete('/api/ipsec-passwords/:id', writeLimiter, ipsecPasswordsRoutes.deleteIpsecPassword);
|
|
|
|
// === MIKROTIK CONFIG GENERATION (text + json) ===
|
|
app.post('/api/mikrotik/generate', mikrotikConfigRoutes.generateMikrotikConfig);
|
|
app.get('/api/mikrotik/generate-interfaces', mikrotikConfigRoutes.generateInterfaces);
|
|
app.get('/api/mikrotik/generate-recursive-routes', mikrotikConfigRoutes.generateRecursiveRoutes);
|
|
app.post('/api/mikrotik/test-connection', mikrotikConfigRoutes.testMikrotikConnection);
|
|
// Простой health-check (используется NetworkConfigManager при открытии модалки применения)
|
|
app.get('/api/mikrotik/ping', (req, res) => res.json({ ok: true, ts: Date.now() }));
|
|
// Реальный ping через RouterOS по интерфейсу/шлюзу
|
|
app.post('/api/mikrotik/ping', writeLimiter, mikrotikConfigRoutes.pingViaInterface);
|
|
// Traceroute через RouterOS с выбором сервера и шлюза
|
|
app.post('/api/mikrotik/traceroute', writeLimiter, mikrotikConfigRoutes.tracerouteViaGateway);
|
|
// Speed-test (TCP upload/download) между MikroTik по туннельному интерфейсу
|
|
app.post('/api/mikrotik/speed-test', writeLimiter, mikrotikConfigRoutes.speedTestViaTunnel);
|
|
app.post('/api/mikrotik/apply', mikrotikConfigRoutes.applyMikrotikConfig);
|
|
app.post('/api/mikrotik/run-script', mikrotikConfigRoutes.runScript);
|
|
app.get('/api/mikrotik/address-lists', mikrotikConfigRoutes.getAddressLists);
|
|
app.get('/api/mikrotik/ospf-interface-templates', mikrotikConfigRoutes.getOspfInterfaceTemplates);
|
|
app.post('/api/mikrotik/ospf-interface-templates/apply', writeLimiter, mikrotikConfigRoutes.applyOspfInterfaceTemplates);
|
|
app.post('/api/mikrotik/address-lists/apply-summary', writeLimiter, mikrotikConfigRoutes.applyAddressListSummary);
|
|
|
|
// === UPTIME MONITOR (проверка доступности: http / internal-ping / external-ping из настроек) ===
|
|
app.get('/api/uptime/cache', mikrotikConfigRoutes.getUptimeCache);
|
|
app.post('/api/uptime/check', writeLimiter, mikrotikConfigRoutes.uptimeCheck);
|
|
|
|
// === TRAFFIC STATS (MikroTik interfaces by jumphost) ===
|
|
app.get('/api/traffic/interface-stats', trafficRoutes.getInterfaceStats);
|
|
|
|
// === RESOURCE STATS (RAM/CPU/HDD по роутерам) ===
|
|
app.get('/api/resources/stats', resourceStatsRoutes.getResourceStats);
|
|
|
|
// === ALERTS (агрегация проблем: серверы, ресурсы роутеров) ===
|
|
app.get('/api/alerts', alertsRoutes.getAlerts);
|
|
|
|
// === MIKROTIK BACKUPS (S3) ===
|
|
app.post('/api/mikrotik/backups', writeLimiter, mikrotikBackupRoutes.createBackup);
|
|
app.get('/api/mikrotik/backups', mikrotikBackupRoutes.listBackups);
|
|
app.get('/api/mikrotik/backups/item', mikrotikBackupRoutes.getBackup);
|
|
app.post('/api/mikrotik/backups/diff', mikrotikBackupRoutes.diffBackups);
|
|
app.post('/api/mikrotik/backups/run', writeLimiter, mikrotikBackupRoutes.runBackupNow);
|
|
|
|
// === NETWORK MAP CACHE (для быстрой загрузки карты сети) ===
|
|
app.get('/api/network-map-cache', schedulerRoutes.getNetworkMapCache);
|
|
|
|
// === LOCAL AI ROUTE OPTIMIZER (rule-based) ===
|
|
app.get('/api/route-optimizer', routeOptimizerRoutes.getRouteOptimizer);
|
|
|
|
// === SCHEDULER (карта сети: пинг и скорость по расписанию) ===
|
|
app.get('/api/scheduler/network-map/settings', schedulerRoutes.getNetworkMapSchedulerSettings);
|
|
app.patch('/api/scheduler/network-map/settings', writeLimiter, schedulerRoutes.patchNetworkMapSchedulerSettings);
|
|
app.get('/api/scheduler/network-map/logs', schedulerRoutes.getNetworkMapSchedulerLogs);
|
|
app.post('/api/scheduler/network-map/run-now', writeLimiter, schedulerRoutes.runNetworkMapSchedulerNow);
|
|
|
|
// === SCHEDULER (пинг сервисов: обновление кеша по расписанию, по умолчанию каждые 2 мин) ===
|
|
app.get('/api/scheduler/ping-services/settings', schedulerRoutes.getPingServicesSchedulerSettings);
|
|
app.patch('/api/scheduler/ping-services/settings', writeLimiter, schedulerRoutes.patchPingServicesSchedulerSettings);
|
|
app.post('/api/scheduler/ping-services/run-now', writeLimiter, schedulerRoutes.runPingServicesSchedulerNow);
|
|
|
|
// === SCHEDULER (Uptime Monitor: проверки по расписанию, по умолчанию каждые 2 мин) ===
|
|
app.get('/api/scheduler/uptime-monitor/settings', schedulerRoutes.getUptimeMonitorSchedulerSettings);
|
|
app.patch('/api/scheduler/uptime-monitor/settings', writeLimiter, schedulerRoutes.patchUptimeMonitorSchedulerSettings);
|
|
app.post('/api/scheduler/uptime-monitor/run-now', writeLimiter, schedulerRoutes.runUptimeMonitorSchedulerNow);
|
|
|
|
// === MIKROTIK VALIDATION ===
|
|
app.post('/api/mikrotik/validate', async (req, res) => {
|
|
const { config } = req.body;
|
|
|
|
if (!config || typeof config !== 'string') {
|
|
return sendError(res, 400, 'Config is required', 'E_BAD_REQUEST');
|
|
}
|
|
|
|
try {
|
|
const validation = mikrotikValidator.validateMikrotikConfig(config);
|
|
return res.json(validation);
|
|
} catch (error) {
|
|
console.error('Error validating config:', error);
|
|
return sendError(res, 500, 'Error validating configuration', 'E_VALIDATION');
|
|
}
|
|
});
|
|
|
|
// === CATCHALL ===
|
|
app.get('*', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
});
|
|
|
|
// Error handler
|
|
app.use(errorHandler);
|
|
|
|
app.listen(port, () => {
|
|
console.log(`Server is running on http://localhost:${port}`);
|
|
try {
|
|
initMikrotikBackupScheduler(logger);
|
|
} catch (e) {
|
|
logger.error({ component: 'mikrotik-backup', err: e && e.message }, 'Failed to start backup scheduler');
|
|
}
|
|
try {
|
|
initNetworkMapScheduler(logger, port);
|
|
} catch (e) {
|
|
logger.error({ component: 'network-map-scheduler', err: e && e.message }, 'Failed to start network map scheduler');
|
|
}
|
|
try {
|
|
initPingServicesScheduler(logger);
|
|
} catch (e) {
|
|
logger.error({ component: 'ping-services-scheduler', err: e && e.message }, 'Failed to start ping services scheduler');
|
|
}
|
|
try {
|
|
initUptimeMonitorScheduler(logger, port);
|
|
} catch (e) {
|
|
logger.error({ component: 'uptime-monitor-scheduler', err: e && e.message }, 'Failed to start uptime monitor scheduler');
|
|
}
|
|
});
|
|
|