feat: Enhance server security and performance by adding helmet for security headers, implementing rate limiting, and centralizing error handling; update frontend API calls to use a dedicated api module for consistency
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 4m37s

This commit is contained in:
2025-08-11 19:11:30 +07:00
parent 087e0eb491
commit 6afd43f9e8
13 changed files with 98 additions and 358 deletions
+31 -3
View File
@@ -8,11 +8,27 @@ const compression = require('compression');
const Ajv = require('ajv');
const net = require('net');
const crypto = require('crypto');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express();
const port = 3001;
const port = Number(process.env.PORT) || 3001;
// CORS: при необходимости сузьте до списка доменов (пример ниже)
// const allowed = (process.env.CORS_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean);
// app.use(cors({ origin: (origin, cb) => { if (!origin || allowed.length===0 || allowed.includes(origin)) return cb(null, true); cb(new Error('CORS blocked')); }, credentials: true }));
app.use(cors());
app.use(helmet({
crossOriginResourcePolicy: { policy: 'cross-origin' },
}));
app.disable('x-powered-by');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 1000,
standardHeaders: true,
legacyHeaders: false,
});
app.use(limiter);
app.use(express.json());
app.use(compression());
// Disable Express auto-ETag to avoid weak ETags on JSON bodies
@@ -24,6 +40,8 @@ app.use((req, res, next) => {
next();
});
// Централизованный обработчик ошибок (должен быть подключён ПОСЛЕ роутов — см. ниже второе use)
// Helpers: meta and responses
function toIso(x) {
try { return new Date(x).toISOString(); } catch { return null; }
@@ -1739,6 +1757,16 @@ app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Централизованный error handler (последний middleware)
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
const status = typeof err?.status === 'number' ? err.status : 500;
const code = err?.code || 'E_INTERNAL';
const message = status === 500 && process.env.NODE_ENV === 'production' ? 'Internal Server Error' : (err?.message || 'Error');
try { console.error('error:', err); } catch {}
res.status(status).json({ message, code });
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
console.log(`Server is running on http://localhost:${port}`);
});
+3 -297
View File
@@ -12,9 +12,7 @@
"@tabler/icons-react": "^3.34.0",
"@xyflow/react": "^12.3.4",
"axios": "^1.10.0",
"bootstrap": "^5.3.7",
"react": "^19.1.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.0",
"react-router-dom": "^6.30.1",
"react-sigma": "^1.2.35"
@@ -248,15 +246,6 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"dev": true,
@@ -554,21 +543,6 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.9",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz",
"integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
@@ -578,60 +552,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@restart/hooks": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz",
"integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@popperjs/core": "^2.11.8",
"@react-aria/ssr": "^3.5.0",
"@restart/hooks": "^0.5.0",
"@types/warning": "^3.0.3",
"dequal": "^2.0.3",
"dom-helpers": "^5.2.0",
"uncontrollable": "^8.0.4",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/@restart/ui/node_modules/@restart/hooks": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz",
"integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui/node_modules/uncontrollable": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
"integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.14.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.19",
"dev": true,
@@ -650,15 +570,6 @@
"linux"
]
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@tabler/core": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.3.2.tgz",
@@ -817,14 +728,9 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.8",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -838,21 +744,6 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/warning": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.6.0",
"dev": true,
@@ -975,25 +866,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/bootstrap": {
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
"integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"dev": true,
@@ -1093,12 +965,6 @@
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
"license": "MIT"
},
"node_modules/color-convert": {
"version": "2.0.1",
"dev": true,
@@ -1150,6 +1016,7 @@
},
"node_modules/csstype": {
"version": "3.1.3",
"devOptional": true,
"license": "MIT"
},
"node_modules/d3-color": {
@@ -1285,25 +1152,6 @@
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"license": "MIT",
@@ -1844,15 +1692,6 @@
"node": ">=0.8.19"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"dev": true,
@@ -1879,6 +1718,7 @@
},
"node_modules/js-tokens": {
"version": "4.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -1968,18 +1808,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
@@ -2055,15 +1883,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"dev": true,
@@ -2186,30 +2005,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types-extra": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
"license": "MIT",
"dependencies": {
"react-is": "^16.3.2",
"warning": "^4.0.0"
},
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"license": "MIT"
@@ -2229,37 +2024,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-bootstrap": {
"version": "2.10.10",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz",
"integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.7",
"@restart/hooks": "^0.4.9",
"@restart/ui": "^1.9.4",
"@types/prop-types": "^15.7.12",
"@types/react-transition-group": "^4.4.6",
"classnames": "^2.3.2",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.8.1",
"prop-types-extra": "^1.1.0",
"react-transition-group": "^4.4.5",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"@types/react": ">=16.14.8",
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"license": "MIT",
@@ -2270,18 +2034,6 @@
"react": "^19.1.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.17.0",
"dev": true,
@@ -2335,22 +2087,6 @@
"react-dom": ">=15.3"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"dev": true,
@@ -2435,12 +2171,6 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"dev": true,
@@ -2452,21 +2182,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": ">=15.0.0"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"dev": true,
@@ -2899,15 +2614,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"dev": true,
-2
View File
@@ -13,9 +13,7 @@
"@tabler/core": "^1.3.2",
"@tabler/icons-react": "^3.34.0",
"axios": "^1.10.0",
"bootstrap": "^5.3.7",
"react": "^19.1.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.0",
"react-router-dom": "^6.30.1",
"react-sigma": "^1.2.35",
+1 -2
View File
@@ -1,5 +1,4 @@
import { useState, useEffect, createContext, useContext } from 'react';
import { Container } from 'react-bootstrap';
import {
IconBrandTabler,
IconWorld,
@@ -23,7 +22,7 @@ import CommunitiesManager from './CommunitiesManager';
import Dashboard from './Dashboard';
import './App.css';
import { NotifyProvider } from './components/NotifyProvider.jsx';
import axios from 'axios';
// axios не используется напрямую; сетевые вызовы через src/lib/api.js
// --- Simple i18n (RU/EN) ---
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
+4 -4
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import axios from 'axios';
import api from './lib/api.js';
import {
IconPlus,
IconTrash,
@@ -30,7 +30,7 @@ function AutoUrlManager() {
const fetchUrls = async () => {
try {
setLoading(true);
const response = await axios.get('/api/auto-urls');
const response = await api.get('/auto-urls');
setUrls(response.data);
} catch (error) {
console.error('Error fetching URLs:', error);
@@ -91,7 +91,7 @@ function AutoUrlManager() {
return;
}
await axios.post('/api/auto-urls', { urls: validUrls });
await api.post('/auto-urls', { urls: validUrls });
setUrls(validUrls);
setMessage('URL-адреса сохранены успешно');
setMessageType('success');
@@ -109,7 +109,7 @@ function AutoUrlManager() {
setProcessing(true);
setMessage('');
const response = await axios.post('/api/auto-urls/process');
const response = await api.post('/auto-urls/process');
setMessage(response.data.message);
setMessageType('success');
} catch (error) {
+3 -3
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import api from './lib/api.js';
import {
IconPlus,
IconEdit,
@@ -114,7 +114,7 @@ function BillingManager() {
const fetchBillingData = async () => {
setLoading(true);
try {
const response = await axios.get(`${API_URL}/billing`);
const response = await api.get(`/billing`);
const normalized = (response.data || []).map((item, idx) => ({
id: item.id ?? String(idx + 1),
payments: Array.isArray(item.payments)
@@ -139,7 +139,7 @@ function BillingManager() {
const handleSaveChanges = async () => {
setLoading(true);
try {
await axios.post(`${API_URL}/billing`, { domains: billingData });
await api.post(`/billing`, { domains: billingData });
setSuccess('Данные успешно сохранены');
setTimeout(() => setSuccess(''), 3000);
} catch (err) {
+3 -3
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import axios from 'axios';
import api from './lib/api.js';
import {
IconPlus,
IconSearch,
@@ -39,7 +39,7 @@ function CommunitiesManager() {
const fetchItems = async () => {
setLoading(true);
try {
const res = await axios.get(`${API_URL}/communities`);
const res = await api.get(`/communities`);
setItems(res.data);
setError('');
} catch (e) {
@@ -53,7 +53,7 @@ function CommunitiesManager() {
const saveAll = async (data) => {
setLoading(true);
try {
await axios.post(`${API_URL}/communities`, { communities: data });
await api.post(`/communities`, { communities: data });
setSuccess('Справочник сохранён!');
setTimeout(() => setSuccess(''), 3000);
} catch (e) {
+7 -7
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import api from './lib/api.js';
import {
IconWorld,
IconNetwork,
@@ -80,12 +80,12 @@ function Dashboard() {
try {
// Загружаем данные с обработкой ошибок для каждого endpoint
const results = await Promise.allSettled([
axios.get('/api/domains-new', { params: { countOnly: true } }),
axios.get('/api/ip-ranges', { params: { countOnly: true } }),
axios.get('/api/asns', { params: { countOnly: true } }),
axios.get('/api/servers'),
axios.get('/api/servers/availability', { params: { ttlSeconds: 60 } }),
axios.get('/api/s3/last-modified')
api.get('/domains-new', { params: { countOnly: true } }),
api.get('/ip-ranges', { params: { countOnly: true } }),
api.get('/asns', { params: { countOnly: true } }),
api.get('/servers'),
api.get('/servers/availability', { params: { ttlSeconds: 60 } }),
api.get('/s3/last-modified')
]);
// Обрабатываем результаты
+3 -3
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import api from './lib/api.js';
import {
IconPlus,
IconSearch,
@@ -44,7 +44,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
const fetchItems = async () => {
setLoading(true);
try {
const response = await axios.get(`${API_URL}/${entityKey}`);
const response = await api.get(`/${entityKey}`);
setItems(response.data);
setError('');
} catch (error) {
@@ -102,7 +102,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
const handleSaveChanges = async () => {
setLoading(true);
try {
await axios.post(`${API_URL}/${entityKey}`, { domains: items });
await api.post(`/${entityKey}`, { domains: items });
setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
+27 -27
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import api from './lib/api.js';
import {
IconPlus,
IconSearch,
@@ -81,7 +81,7 @@ function FilterManager() {
useEffect(() => {
(async () => {
try {
const res = await axios.get(`${API_URL}/communities`);
const res = await api.get(`/communities`);
setCommunities(Array.isArray(res.data) ? res.data : []);
} catch (e) {
// мягко игнорируем, автокомплит необязателен
@@ -108,7 +108,7 @@ function FilterManager() {
const fetchServerFilters = async (serverId) => {
setLoading(true);
try {
const response = await axios.get(`${API_URL}/server-filters/${serverId}`);
const response = await api.get(`/server-filters/${serverId}`);
setServerFilters(response.data);
setError('');
} catch (error) {
@@ -122,7 +122,7 @@ function FilterManager() {
// Функции для управления серверами
const fetchServers = async () => {
try {
const response = await axios.get(`${API_URL}/server-configs`);
const response = await api.get(`/server-configs`);
setServers(response.data);
} catch (error) {
console.error('Error fetching servers:', error);
@@ -134,7 +134,7 @@ function FilterManager() {
const fetchSimpleFilters = async () => {
setSimpleLoading(true);
try {
const response = await axios.get(`${API_URL}/simple-filters`);
const response = await api.get(`/simple-filters`);
setSimpleFilters(response.data);
} catch (error) {
console.error('Error fetching simple filters:', error);
@@ -158,7 +158,7 @@ function FilterManager() {
};
const updatedServers = [...servers, serverToAdd];
await axios.post(`${API_URL}/server-configs`, { servers: updatedServers });
await api.post(`/server-configs`, { servers: updatedServers });
setServers(updatedServers);
setNewServer({ name: '', description: '', enabled: true });
@@ -177,7 +177,7 @@ function FilterManager() {
await axios.post(`${API_URL}/server-configs`, { servers: updatedServers });
// Удаляем все файлы сервера (конфигурацию и фильтры)
await axios.delete(`${API_URL}/server-configs/${serverId}/complete`);
await api.delete(`/server-configs/${serverId}/complete`);
setServers(updatedServers);
if (selectedServer?.id === serverId) {
@@ -201,7 +201,7 @@ function FilterManager() {
return;
}
await axios.post(`${API_URL}/server-configs/${serverId}`, { config });
await api.post(`/server-configs/${serverId}`, { config });
setSuccess(`Конфигурация для сервера "${servers.find(s => s.id === serverId)?.name}" сохранена!`);
setTimeout(() => setSuccess(''), 3000);
@@ -217,7 +217,7 @@ function FilterManager() {
const handleLoadServerConfig = async (serverId) => {
try {
const response = await axios.get(`${API_URL}/server-configs/${serverId}`);
const response = await api.get(`/server-configs/${serverId}`);
setGeneratedConfig(response.data.config);
setPreviewModalOpen(true);
} catch (error) {
@@ -249,11 +249,11 @@ function FilterManager() {
setLoading(true);
try {
// Получаем фильтры исходного сервера
const sourceFiltersResponse = await axios.get(`${API_URL}/server-filters/${sourceServerId}`);
const sourceFiltersResponse = await api.get(`/server-filters/${sourceServerId}`);
const sourceFilters = sourceFiltersResponse.data;
// Получаем конфигурацию исходного сервера
const sourceConfigResponse = await axios.get(`${API_URL}/server-configs/${sourceServerId}`);
const sourceConfigResponse = await api.get(`/server-configs/${sourceServerId}`);
const sourceConfig = sourceConfigResponse.data.config;
if (!sourceConfig || sourceConfig === '// Конфигурация не найдена') {
@@ -267,10 +267,10 @@ function FilterManager() {
for (const targetServer of targetServers) {
try {
// Копируем фильтры
await axios.post(`${API_URL}/server-filters/${targetServer.id}`, { filters: sourceFilters });
await api.post(`/server-filters/${targetServer.id}`, { filters: sourceFilters });
// Копируем конфигурацию
await axios.post(`${API_URL}/server-configs/${targetServer.id}`, { config: sourceConfig });
await api.post(`/server-configs/${targetServer.id}`, { config: sourceConfig });
results.push({ server: targetServer.name, status: 'success' });
} catch (error) {
@@ -349,10 +349,10 @@ function FilterManager() {
for (const server of enabledServers) {
try {
// Копируем фильтры
await axios.post(`${API_URL}/server-filters/${server.id}`, { filters: serverFilters });
await api.post(`/server-filters/${server.id}`, { filters: serverFilters });
// Копируем конфигурацию
await axios.post(`${API_URL}/server-configs/${server.id}`, { config });
await api.post(`/server-configs/${server.id}`, { config });
results.push({ server: server.name, status: 'success' });
} catch (error) {
@@ -408,7 +408,7 @@ function FilterManager() {
// Сохраняем фильтры сервера
try {
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters });
await api.post(`/server-filters/${selectedServer.id}`, { filters: updatedFilters });
setSuccess('Фильтр успешно добавлен!');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
@@ -437,7 +437,7 @@ function FilterManager() {
// Сохраняем фильтры сервера
try {
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters });
await api.post(`/server-filters/${selectedServer.id}`, { filters: updatedFilters });
setSuccess('Фильтр успешно обновлен!');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
@@ -658,7 +658,7 @@ function FilterManager() {
cancelInlineEdit();
setSelectedFilterKeys(new Set());
try {
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updated });
await api.post(`/server-filters/${selectedServer.id}`, { filters: updated });
setSuccess('Фильтр успешно обновлен!');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
@@ -696,7 +696,7 @@ function FilterManager() {
setServerFilters(updated);
setSelectedFilterKeys(new Set());
try {
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updated });
await api.post(`/server-filters/${selectedServer.id}`, { filters: updated });
setSuccess('Выбранные фильтры удалены.');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
@@ -719,9 +719,9 @@ function FilterManager() {
try {
// Копируем фильтры и генерируем конфиг для каждого сервера
for (const server of servers) {
await axios.post(`${API_URL}/server-filters/${server.id}`, { filters: simpleFilters });
await api.post(`/server-filters/${server.id}`, { filters: simpleFilters });
// Генерируем конфиг
const response = await axios.post(`${API_URL}/server-filters/generate-config`, { filters: simpleFilters });
const response = await api.post(`/server-filters/generate-config`, { filters: simpleFilters });
if (response.data && response.data.config) {
await axios.post(`${API_URL}/server-configs/${server.id}`, { config: response.data.config });
}
@@ -748,7 +748,7 @@ function FilterManager() {
// Сохраняем фильтры
try {
await axios.post(`${API_URL}/simple-filters`, { filters: updatedFilters });
await api.post(`/simple-filters`, { filters: updatedFilters });
setSimpleSuccess('Фильтр успешно добавлен!');
setTimeout(() => setSimpleSuccess(''), 3000);
} catch (error) {
@@ -779,7 +779,7 @@ function FilterManager() {
// Сохраняем фильтры
try {
await axios.post(`${API_URL}/simple-filters`, { filters: updated });
await api.post(`/simple-filters`, { filters: updated });
} catch (error) {
console.error('Error saving simple filters:', error);
setSimpleError('Не удалось сохранить изменения.');
@@ -1593,7 +1593,7 @@ function FilterManager() {
className="btn btn-outline-primary"
onClick={async () => {
try {
const response = await axios.post(`${API_URL}/server-filters/generate-config`, { filters: simpleFilters });
const response = await api.post(`/server-filters/generate-config`, { filters: simpleFilters });
if (response.data && response.data.config) {
setGeneratedConfig(response.data.config);
setPreviewMode('simple');
@@ -1614,7 +1614,7 @@ function FilterManager() {
className="btn btn-outline-primary"
onClick={async () => {
try {
const response = await axios.post(`${API_URL}/server-filters/generate-config`, { filters: simpleFilters });
const response = await api.post(`/server-filters/generate-config`, { filters: simpleFilters });
if (response.data && response.data.config) {
const config = response.data.config;
if (navigator.clipboard) {
@@ -1664,7 +1664,7 @@ function FilterManager() {
className="btn btn-outline-primary"
onClick={async () => {
try {
await axios.post(`${API_URL}/simple-filters`, { filters: simpleFilters });
await api.post(`/simple-filters`, { filters: simpleFilters });
setSimpleSuccess('Фильтры успешно сохранены!');
setTimeout(() => setSimpleSuccess(''), 3000);
} catch (error) {
@@ -1681,7 +1681,7 @@ function FilterManager() {
onClick={async () => {
if (window.confirm('Вы уверены, что хотите очистить все фильтры?')) {
try {
await axios.post(`${API_URL}/simple-filters`, { filters: [] });
await api.post(`/simple-filters`, { filters: [] });
setSimpleFilters([]);
setSimpleSuccess('Все фильтры очищены!');
setTimeout(() => setSimpleSuccess(''), 3000);
+3 -3
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import api from './lib/api.js';
import {
IconPlus,
IconSearch,
@@ -113,7 +113,7 @@ function ServerManager() {
const fetchServers = async () => {
setLoading(true);
try {
const response = await axios.get(`${API_URL}/servers`);
const response = await api.get(`/servers`);
setServers(response.data);
setError('');
} catch (error) {
@@ -316,7 +316,7 @@ function ServerManager() {
const handleSaveChanges = async () => {
setLoading(true);
try {
await axios.post(`${API_URL}/servers`, { domains: servers });
await api.post(`/servers`, { domains: servers });
setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
+4 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import axios from 'axios';
import api from '../lib/api.js';
function LockBanner({ resource, className = '' }) {
const [status, setStatus] = useState({ locked: false });
@@ -15,7 +15,7 @@ function LockBanner({ resource, className = '' }) {
const fetchStatus = async () => {
try {
const res = await axios.get(`/api/locks/${resource}`);
const res = await api.get(`/locks/${resource}`);
setStatus(res.data || { locked: false });
} catch {
// ignore
@@ -31,7 +31,7 @@ function LockBanner({ resource, className = '' }) {
const acquireOrRefresh = async () => {
setLoading(true);
try {
const res = await axios.post(`/api/locks/${resource}`, { owner, ttlSeconds: 180 });
const res = await api.post(`/locks/${resource}`, { owner, ttlSeconds: 180 });
setStatus(res.data || { locked: true, owner, expiresAt: Date.now() + 180_000 });
} catch {
// ignore
@@ -43,7 +43,7 @@ function LockBanner({ resource, className = '' }) {
const release = async () => {
setLoading(true);
try {
await axios.delete(`/api/locks/${resource}`);
await api.delete(`/locks/${resource}`);
setStatus({ locked: false });
} catch {
// ignore
+9
View File
@@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
secure: false,
},
},
},
})