feat: Удаление устаревших файлов документации и оптимизация структуры проекта. Упрощение кода и улучшение читаемости за счет удаления ненужных компонентов и отчетов, что способствует более эффективному управлению проектом.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s
This commit is contained in:
@@ -1,331 +0,0 @@
|
||||
// Оптимизированная версия 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, resourceToKey, toIso, buildNestedGatewayBlocks } = require('./utils/helpers');
|
||||
const validators = require('./lib/validators');
|
||||
const mikrotikValidator = require('./lib/mikrotik-validator');
|
||||
const s3Service = require('./services/s3Service');
|
||||
|
||||
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');
|
||||
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);
|
||||
|
||||
// === Остальные роуты из старого server.js ===
|
||||
// Для экономии места и времени, остальные роуты могут быть вынесены позже
|
||||
// Но основная логика уже оптимизирована
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Fallback для остальных роутов - временно используем старую логику из server.js
|
||||
// В будущем можно вынести и оптимизировать остальные эндпоинты аналогично
|
||||
|
||||
// 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}`);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user