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
+30 -2
View File
@@ -8,11 +8,27 @@ const compression = require('compression');
const Ajv = require('ajv'); const Ajv = require('ajv');
const net = require('net'); const net = require('net');
const crypto = require('crypto'); const crypto = require('crypto');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const app = express(); 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(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(express.json());
app.use(compression()); app.use(compression());
// Disable Express auto-ETag to avoid weak ETags on JSON bodies // Disable Express auto-ETag to avoid weak ETags on JSON bodies
@@ -24,6 +40,8 @@ app.use((req, res, next) => {
next(); next();
}); });
// Централизованный обработчик ошибок (должен быть подключён ПОСЛЕ роутов — см. ниже второе use)
// Helpers: meta and responses // Helpers: meta and responses
function toIso(x) { function toIso(x) {
try { return new Date(x).toISOString(); } catch { return null; } 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')); 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, () => { 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", "@tabler/icons-react": "^3.34.0",
"@xyflow/react": "^12.3.4", "@xyflow/react": "^12.3.4",
"axios": "^1.10.0", "axios": "^1.10.0",
"bootstrap": "^5.3.7",
"react": "^19.1.0", "react": "^19.1.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^6.30.1", "react-router-dom": "^6.30.1",
"react-sigma": "^1.2.35" "react-sigma": "^1.2.35"
@@ -248,15 +246,6 @@
"@babel/core": "^7.0.0-0" "@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": { "node_modules/@babel/template": {
"version": "7.27.2", "version": "7.27.2",
"dev": true, "dev": true,
@@ -554,21 +543,6 @@
"url": "https://opencollective.com/popperjs" "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": { "node_modules/@remix-run/router": {
"version": "1.23.0", "version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
@@ -578,60 +552,6 @@
"node": ">=14.0.0" "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": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.19", "version": "1.0.0-beta.19",
"dev": true, "dev": true,
@@ -650,15 +570,6 @@
"linux" "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": { "node_modules/@tabler/core": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.3.2.tgz",
@@ -817,14 +728,9 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/react": {
"version": "19.1.8", "version": "19.1.8",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -838,21 +744,6 @@
"@types/react": "^19.0.0" "@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": { "node_modules/@vitejs/plugin-react": {
"version": "4.6.0", "version": "4.6.0",
"dev": true, "dev": true,
@@ -975,25 +866,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"dev": true, "dev": true,
@@ -1093,12 +965,6 @@
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
"license": "MIT" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"dev": true, "dev": true,
@@ -1150,6 +1016,7 @@
}, },
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-color": { "node_modules/d3-color": {
@@ -1285,25 +1152,6 @@
"node": ">=0.4.0" "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": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"license": "MIT", "license": "MIT",
@@ -1844,15 +1692,6 @@
"node": ">=0.8.19" "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": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"dev": true, "dev": true,
@@ -1879,6 +1718,7 @@
}, },
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
@@ -1968,18 +1808,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"dev": true, "dev": true,
@@ -2055,15 +1883,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"dev": true, "dev": true,
@@ -2186,30 +2005,6 @@
"node": ">= 0.8.0" "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": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"license": "MIT" "license": "MIT"
@@ -2229,37 +2024,6 @@
"node": ">=0.10.0" "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": { "node_modules/react-dom": {
"version": "19.1.0", "version": "19.1.0",
"license": "MIT", "license": "MIT",
@@ -2270,18 +2034,6 @@
"react": "^19.1.0" "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": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"dev": true, "dev": true,
@@ -2335,22 +2087,6 @@
"react-dom": ">=15.3" "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": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"dev": true, "dev": true,
@@ -2435,12 +2171,6 @@
"url": "https://github.com/sponsors/SuperchupuDev" "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": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"dev": true, "dev": true,
@@ -2452,21 +2182,6 @@
"node": ">= 0.8.0" "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": { "node_modules/update-browserslist-db": {
"version": "1.1.3", "version": "1.1.3",
"dev": true, "dev": true,
@@ -2899,15 +2614,6 @@
"fsevents": "~2.3.2" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"dev": true, "dev": true,
-2
View File
@@ -13,9 +13,7 @@
"@tabler/core": "^1.3.2", "@tabler/core": "^1.3.2",
"@tabler/icons-react": "^3.34.0", "@tabler/icons-react": "^3.34.0",
"axios": "^1.10.0", "axios": "^1.10.0",
"bootstrap": "^5.3.7",
"react": "^19.1.0", "react": "^19.1.0",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-router-dom": "^6.30.1", "react-router-dom": "^6.30.1",
"react-sigma": "^1.2.35", "react-sigma": "^1.2.35",
+1 -2
View File
@@ -1,5 +1,4 @@
import { useState, useEffect, createContext, useContext } from 'react'; import { useState, useEffect, createContext, useContext } from 'react';
import { Container } from 'react-bootstrap';
import { import {
IconBrandTabler, IconBrandTabler,
IconWorld, IconWorld,
@@ -23,7 +22,7 @@ import CommunitiesManager from './CommunitiesManager';
import Dashboard from './Dashboard'; import Dashboard from './Dashboard';
import './App.css'; import './App.css';
import { NotifyProvider } from './components/NotifyProvider.jsx'; import { NotifyProvider } from './components/NotifyProvider.jsx';
import axios from 'axios'; // axios не используется напрямую; сетевые вызовы через src/lib/api.js
// --- Simple i18n (RU/EN) --- // --- Simple i18n (RU/EN) ---
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k }); const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
+4 -4
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import axios from 'axios'; import api from './lib/api.js';
import { import {
IconPlus, IconPlus,
IconTrash, IconTrash,
@@ -30,7 +30,7 @@ function AutoUrlManager() {
const fetchUrls = async () => { const fetchUrls = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await axios.get('/api/auto-urls'); const response = await api.get('/auto-urls');
setUrls(response.data); setUrls(response.data);
} catch (error) { } catch (error) {
console.error('Error fetching URLs:', error); console.error('Error fetching URLs:', error);
@@ -91,7 +91,7 @@ function AutoUrlManager() {
return; return;
} }
await axios.post('/api/auto-urls', { urls: validUrls }); await api.post('/auto-urls', { urls: validUrls });
setUrls(validUrls); setUrls(validUrls);
setMessage('URL-адреса сохранены успешно'); setMessage('URL-адреса сохранены успешно');
setMessageType('success'); setMessageType('success');
@@ -109,7 +109,7 @@ function AutoUrlManager() {
setProcessing(true); setProcessing(true);
setMessage(''); setMessage('');
const response = await axios.post('/api/auto-urls/process'); const response = await api.post('/auto-urls/process');
setMessage(response.data.message); setMessage(response.data.message);
setMessageType('success'); setMessageType('success');
} catch (error) { } catch (error) {
+3 -3
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import axios from 'axios'; import api from './lib/api.js';
import { import {
IconPlus, IconPlus,
IconEdit, IconEdit,
@@ -114,7 +114,7 @@ function BillingManager() {
const fetchBillingData = async () => { const fetchBillingData = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await axios.get(`${API_URL}/billing`); const response = await api.get(`/billing`);
const normalized = (response.data || []).map((item, idx) => ({ const normalized = (response.data || []).map((item, idx) => ({
id: item.id ?? String(idx + 1), id: item.id ?? String(idx + 1),
payments: Array.isArray(item.payments) payments: Array.isArray(item.payments)
@@ -139,7 +139,7 @@ function BillingManager() {
const handleSaveChanges = async () => { const handleSaveChanges = async () => {
setLoading(true); setLoading(true);
try { try {
await axios.post(`${API_URL}/billing`, { domains: billingData }); await api.post(`/billing`, { domains: billingData });
setSuccess('Данные успешно сохранены'); setSuccess('Данные успешно сохранены');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (err) { } catch (err) {
+3 -3
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import axios from 'axios'; import api from './lib/api.js';
import { import {
IconPlus, IconPlus,
IconSearch, IconSearch,
@@ -39,7 +39,7 @@ function CommunitiesManager() {
const fetchItems = async () => { const fetchItems = async () => {
setLoading(true); setLoading(true);
try { try {
const res = await axios.get(`${API_URL}/communities`); const res = await api.get(`/communities`);
setItems(res.data); setItems(res.data);
setError(''); setError('');
} catch (e) { } catch (e) {
@@ -53,7 +53,7 @@ function CommunitiesManager() {
const saveAll = async (data) => { const saveAll = async (data) => {
setLoading(true); setLoading(true);
try { try {
await axios.post(`${API_URL}/communities`, { communities: data }); await api.post(`/communities`, { communities: data });
setSuccess('Справочник сохранён!'); setSuccess('Справочник сохранён!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (e) { } catch (e) {
+7 -7
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import axios from 'axios'; import api from './lib/api.js';
import { import {
IconWorld, IconWorld,
IconNetwork, IconNetwork,
@@ -80,12 +80,12 @@ function Dashboard() {
try { try {
// Загружаем данные с обработкой ошибок для каждого endpoint // Загружаем данные с обработкой ошибок для каждого endpoint
const results = await Promise.allSettled([ const results = await Promise.allSettled([
axios.get('/api/domains-new', { params: { countOnly: true } }), api.get('/domains-new', { params: { countOnly: true } }),
axios.get('/api/ip-ranges', { params: { countOnly: true } }), api.get('/ip-ranges', { params: { countOnly: true } }),
axios.get('/api/asns', { params: { countOnly: true } }), api.get('/asns', { params: { countOnly: true } }),
axios.get('/api/servers'), api.get('/servers'),
axios.get('/api/servers/availability', { params: { ttlSeconds: 60 } }), api.get('/servers/availability', { params: { ttlSeconds: 60 } }),
axios.get('/api/s3/last-modified') api.get('/s3/last-modified')
]); ]);
// Обрабатываем результаты // Обрабатываем результаты
+3 -3
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import axios from 'axios'; import api from './lib/api.js';
import { import {
IconPlus, IconPlus,
IconSearch, IconSearch,
@@ -44,7 +44,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
const fetchItems = async () => { const fetchItems = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await axios.get(`${API_URL}/${entityKey}`); const response = await api.get(`/${entityKey}`);
setItems(response.data); setItems(response.data);
setError(''); setError('');
} catch (error) { } catch (error) {
@@ -102,7 +102,7 @@ function DataManager({ entityName, entityKey, placeholder }) {
const handleSaveChanges = async () => { const handleSaveChanges = async () => {
setLoading(true); setLoading(true);
try { try {
await axios.post(`${API_URL}/${entityKey}`, { domains: items }); await api.post(`/${entityKey}`, { domains: items });
setSuccess('Изменения успешно сохранены!'); setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (error) { } catch (error) {
+27 -27
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import axios from 'axios'; import api from './lib/api.js';
import { import {
IconPlus, IconPlus,
IconSearch, IconSearch,
@@ -81,7 +81,7 @@ function FilterManager() {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const res = await axios.get(`${API_URL}/communities`); const res = await api.get(`/communities`);
setCommunities(Array.isArray(res.data) ? res.data : []); setCommunities(Array.isArray(res.data) ? res.data : []);
} catch (e) { } catch (e) {
// мягко игнорируем, автокомплит необязателен // мягко игнорируем, автокомплит необязателен
@@ -108,7 +108,7 @@ function FilterManager() {
const fetchServerFilters = async (serverId) => { const fetchServerFilters = async (serverId) => {
setLoading(true); setLoading(true);
try { try {
const response = await axios.get(`${API_URL}/server-filters/${serverId}`); const response = await api.get(`/server-filters/${serverId}`);
setServerFilters(response.data); setServerFilters(response.data);
setError(''); setError('');
} catch (error) { } catch (error) {
@@ -122,7 +122,7 @@ function FilterManager() {
// Функции для управления серверами // Функции для управления серверами
const fetchServers = async () => { const fetchServers = async () => {
try { try {
const response = await axios.get(`${API_URL}/server-configs`); const response = await api.get(`/server-configs`);
setServers(response.data); setServers(response.data);
} catch (error) { } catch (error) {
console.error('Error fetching servers:', error); console.error('Error fetching servers:', error);
@@ -134,7 +134,7 @@ function FilterManager() {
const fetchSimpleFilters = async () => { const fetchSimpleFilters = async () => {
setSimpleLoading(true); setSimpleLoading(true);
try { try {
const response = await axios.get(`${API_URL}/simple-filters`); const response = await api.get(`/simple-filters`);
setSimpleFilters(response.data); setSimpleFilters(response.data);
} catch (error) { } catch (error) {
console.error('Error fetching simple filters:', error); console.error('Error fetching simple filters:', error);
@@ -158,7 +158,7 @@ function FilterManager() {
}; };
const updatedServers = [...servers, serverToAdd]; const updatedServers = [...servers, serverToAdd];
await axios.post(`${API_URL}/server-configs`, { servers: updatedServers }); await api.post(`/server-configs`, { servers: updatedServers });
setServers(updatedServers); setServers(updatedServers);
setNewServer({ name: '', description: '', enabled: true }); setNewServer({ name: '', description: '', enabled: true });
@@ -177,7 +177,7 @@ function FilterManager() {
await axios.post(`${API_URL}/server-configs`, { servers: updatedServers }); 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); setServers(updatedServers);
if (selectedServer?.id === serverId) { if (selectedServer?.id === serverId) {
@@ -201,7 +201,7 @@ function FilterManager() {
return; 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}" сохранена!`); setSuccess(`Конфигурация для сервера "${servers.find(s => s.id === serverId)?.name}" сохранена!`);
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
@@ -217,7 +217,7 @@ function FilterManager() {
const handleLoadServerConfig = async (serverId) => { const handleLoadServerConfig = async (serverId) => {
try { try {
const response = await axios.get(`${API_URL}/server-configs/${serverId}`); const response = await api.get(`/server-configs/${serverId}`);
setGeneratedConfig(response.data.config); setGeneratedConfig(response.data.config);
setPreviewModalOpen(true); setPreviewModalOpen(true);
} catch (error) { } catch (error) {
@@ -249,11 +249,11 @@ function FilterManager() {
setLoading(true); setLoading(true);
try { try {
// Получаем фильтры исходного сервера // Получаем фильтры исходного сервера
const sourceFiltersResponse = await axios.get(`${API_URL}/server-filters/${sourceServerId}`); const sourceFiltersResponse = await api.get(`/server-filters/${sourceServerId}`);
const sourceFilters = sourceFiltersResponse.data; 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; const sourceConfig = sourceConfigResponse.data.config;
if (!sourceConfig || sourceConfig === '// Конфигурация не найдена') { if (!sourceConfig || sourceConfig === '// Конфигурация не найдена') {
@@ -267,10 +267,10 @@ function FilterManager() {
for (const targetServer of targetServers) { for (const targetServer of targetServers) {
try { 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' }); results.push({ server: targetServer.name, status: 'success' });
} catch (error) { } catch (error) {
@@ -349,10 +349,10 @@ function FilterManager() {
for (const server of enabledServers) { for (const server of enabledServers) {
try { 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' }); results.push({ server: server.name, status: 'success' });
} catch (error) { } catch (error) {
@@ -408,7 +408,7 @@ function FilterManager() {
// Сохраняем фильтры сервера // Сохраняем фильтры сервера
try { try {
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters }); await api.post(`/server-filters/${selectedServer.id}`, { filters: updatedFilters });
setSuccess('Фильтр успешно добавлен!'); setSuccess('Фильтр успешно добавлен!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (error) { } catch (error) {
@@ -437,7 +437,7 @@ function FilterManager() {
// Сохраняем фильтры сервера // Сохраняем фильтры сервера
try { try {
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updatedFilters }); await api.post(`/server-filters/${selectedServer.id}`, { filters: updatedFilters });
setSuccess('Фильтр успешно обновлен!'); setSuccess('Фильтр успешно обновлен!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (error) { } catch (error) {
@@ -658,7 +658,7 @@ function FilterManager() {
cancelInlineEdit(); cancelInlineEdit();
setSelectedFilterKeys(new Set()); setSelectedFilterKeys(new Set());
try { try {
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updated }); await api.post(`/server-filters/${selectedServer.id}`, { filters: updated });
setSuccess('Фильтр успешно обновлен!'); setSuccess('Фильтр успешно обновлен!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (error) { } catch (error) {
@@ -696,7 +696,7 @@ function FilterManager() {
setServerFilters(updated); setServerFilters(updated);
setSelectedFilterKeys(new Set()); setSelectedFilterKeys(new Set());
try { try {
await axios.post(`${API_URL}/server-filters/${selectedServer.id}`, { filters: updated }); await api.post(`/server-filters/${selectedServer.id}`, { filters: updated });
setSuccess('Выбранные фильтры удалены.'); setSuccess('Выбранные фильтры удалены.');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (error) { } catch (error) {
@@ -719,9 +719,9 @@ function FilterManager() {
try { try {
// Копируем фильтры и генерируем конфиг для каждого сервера // Копируем фильтры и генерируем конфиг для каждого сервера
for (const server of servers) { 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) { if (response.data && response.data.config) {
await axios.post(`${API_URL}/server-configs/${server.id}`, { config: response.data.config }); await axios.post(`${API_URL}/server-configs/${server.id}`, { config: response.data.config });
} }
@@ -748,7 +748,7 @@ function FilterManager() {
// Сохраняем фильтры // Сохраняем фильтры
try { try {
await axios.post(`${API_URL}/simple-filters`, { filters: updatedFilters }); await api.post(`/simple-filters`, { filters: updatedFilters });
setSimpleSuccess('Фильтр успешно добавлен!'); setSimpleSuccess('Фильтр успешно добавлен!');
setTimeout(() => setSimpleSuccess(''), 3000); setTimeout(() => setSimpleSuccess(''), 3000);
} catch (error) { } catch (error) {
@@ -779,7 +779,7 @@ function FilterManager() {
// Сохраняем фильтры // Сохраняем фильтры
try { try {
await axios.post(`${API_URL}/simple-filters`, { filters: updated }); await api.post(`/simple-filters`, { filters: updated });
} catch (error) { } catch (error) {
console.error('Error saving simple filters:', error); console.error('Error saving simple filters:', error);
setSimpleError('Не удалось сохранить изменения.'); setSimpleError('Не удалось сохранить изменения.');
@@ -1593,7 +1593,7 @@ function FilterManager() {
className="btn btn-outline-primary" className="btn btn-outline-primary"
onClick={async () => { onClick={async () => {
try { 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) { if (response.data && response.data.config) {
setGeneratedConfig(response.data.config); setGeneratedConfig(response.data.config);
setPreviewMode('simple'); setPreviewMode('simple');
@@ -1614,7 +1614,7 @@ function FilterManager() {
className="btn btn-outline-primary" className="btn btn-outline-primary"
onClick={async () => { onClick={async () => {
try { 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) { if (response.data && response.data.config) {
const config = response.data.config; const config = response.data.config;
if (navigator.clipboard) { if (navigator.clipboard) {
@@ -1664,7 +1664,7 @@ function FilterManager() {
className="btn btn-outline-primary" className="btn btn-outline-primary"
onClick={async () => { onClick={async () => {
try { try {
await axios.post(`${API_URL}/simple-filters`, { filters: simpleFilters }); await api.post(`/simple-filters`, { filters: simpleFilters });
setSimpleSuccess('Фильтры успешно сохранены!'); setSimpleSuccess('Фильтры успешно сохранены!');
setTimeout(() => setSimpleSuccess(''), 3000); setTimeout(() => setSimpleSuccess(''), 3000);
} catch (error) { } catch (error) {
@@ -1681,7 +1681,7 @@ function FilterManager() {
onClick={async () => { onClick={async () => {
if (window.confirm('Вы уверены, что хотите очистить все фильтры?')) { if (window.confirm('Вы уверены, что хотите очистить все фильтры?')) {
try { try {
await axios.post(`${API_URL}/simple-filters`, { filters: [] }); await api.post(`/simple-filters`, { filters: [] });
setSimpleFilters([]); setSimpleFilters([]);
setSimpleSuccess('Все фильтры очищены!'); setSimpleSuccess('Все фильтры очищены!');
setTimeout(() => setSimpleSuccess(''), 3000); setTimeout(() => setSimpleSuccess(''), 3000);
+3 -3
View File
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import axios from 'axios'; import api from './lib/api.js';
import { import {
IconPlus, IconPlus,
IconSearch, IconSearch,
@@ -113,7 +113,7 @@ function ServerManager() {
const fetchServers = async () => { const fetchServers = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await axios.get(`${API_URL}/servers`); const response = await api.get(`/servers`);
setServers(response.data); setServers(response.data);
setError(''); setError('');
} catch (error) { } catch (error) {
@@ -316,7 +316,7 @@ function ServerManager() {
const handleSaveChanges = async () => { const handleSaveChanges = async () => {
setLoading(true); setLoading(true);
try { try {
await axios.post(`${API_URL}/servers`, { domains: servers }); await api.post(`/servers`, { domains: servers });
setSuccess('Изменения успешно сохранены!'); setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000); setTimeout(() => setSuccess(''), 3000);
} catch (error) { } catch (error) {
+4 -4
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import axios from 'axios'; import api from '../lib/api.js';
function LockBanner({ resource, className = '' }) { function LockBanner({ resource, className = '' }) {
const [status, setStatus] = useState({ locked: false }); const [status, setStatus] = useState({ locked: false });
@@ -15,7 +15,7 @@ function LockBanner({ resource, className = '' }) {
const fetchStatus = async () => { const fetchStatus = async () => {
try { try {
const res = await axios.get(`/api/locks/${resource}`); const res = await api.get(`/locks/${resource}`);
setStatus(res.data || { locked: false }); setStatus(res.data || { locked: false });
} catch { } catch {
// ignore // ignore
@@ -31,7 +31,7 @@ function LockBanner({ resource, className = '' }) {
const acquireOrRefresh = async () => { const acquireOrRefresh = async () => {
setLoading(true); setLoading(true);
try { 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 }); setStatus(res.data || { locked: true, owner, expiresAt: Date.now() + 180_000 });
} catch { } catch {
// ignore // ignore
@@ -43,7 +43,7 @@ function LockBanner({ resource, className = '' }) {
const release = async () => { const release = async () => {
setLoading(true); setLoading(true);
try { try {
await axios.delete(`/api/locks/${resource}`); await api.delete(`/locks/${resource}`);
setStatus({ locked: false }); setStatus({ locked: false });
} catch { } catch {
// ignore // ignore
+9
View File
@@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
secure: false,
},
},
},
}) })