From 6afd43f9e85786e5b7be672d37fe1c2e5e3aa4fc Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Mon, 11 Aug 2025 19:11:30 +0700 Subject: [PATCH] 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 --- backend/server.js | 34 ++- frontend/package-lock.json | 300 +------------------------ frontend/package.json | 2 - frontend/src/App.jsx | 3 +- frontend/src/AutoUrlManager.jsx | 8 +- frontend/src/BillingManager.jsx | 6 +- frontend/src/CommunitiesManager.jsx | 6 +- frontend/src/Dashboard.jsx | 14 +- frontend/src/DataManager.jsx | 6 +- frontend/src/FilterManager.jsx | 54 ++--- frontend/src/ServerManager.jsx | 6 +- frontend/src/components/LockBanner.jsx | 8 +- frontend/vite.config.js | 9 + 13 files changed, 98 insertions(+), 358 deletions(-) diff --git a/backend/server.js b/backend/server.js index ddf0241..95a9d48 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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}`); -}); \ No newline at end of file + console.log(`Server is running on http://localhost:${port}`); +}); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 57ac903..1e3b51f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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, diff --git a/frontend/package.json b/frontend/package.json index c8b65d8..cd99cba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ede574c..7976425 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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 }); diff --git a/frontend/src/AutoUrlManager.jsx b/frontend/src/AutoUrlManager.jsx index f379b36..0cfbcf1 100644 --- a/frontend/src/AutoUrlManager.jsx +++ b/frontend/src/AutoUrlManager.jsx @@ -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) { diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx index ed1f707..11e02c4 100644 --- a/frontend/src/BillingManager.jsx +++ b/frontend/src/BillingManager.jsx @@ -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) { diff --git a/frontend/src/CommunitiesManager.jsx b/frontend/src/CommunitiesManager.jsx index f433af1..9475736 100644 --- a/frontend/src/CommunitiesManager.jsx +++ b/frontend/src/CommunitiesManager.jsx @@ -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) { diff --git a/frontend/src/Dashboard.jsx b/frontend/src/Dashboard.jsx index 42abce9..a762459 100644 --- a/frontend/src/Dashboard.jsx +++ b/frontend/src/Dashboard.jsx @@ -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') ]); // Обрабатываем результаты diff --git a/frontend/src/DataManager.jsx b/frontend/src/DataManager.jsx index 206122c..fab7d5e 100644 --- a/frontend/src/DataManager.jsx +++ b/frontend/src/DataManager.jsx @@ -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) { diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index f888d70..77e17fe 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -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); diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index 9b8e999..c7adca3 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -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) { diff --git a/frontend/src/components/LockBanner.jsx b/frontend/src/components/LockBanner.jsx index 48b6881..fdffa4a 100644 --- a/frontend/src/components/LockBanner.jsx +++ b/frontend/src/components/LockBanner.jsx @@ -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 diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8b0f57b..a7f6c30 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -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, + }, + }, + }, })