mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-07 21:55:01 +03:00
Compare commits
6 Commits
814cda3fb4
...
44f2f426d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 44f2f426d8 | |||
| c4a1139d3f | |||
| 476bec451d | |||
| f905c2dcec | |||
| 30f6bc1833 | |||
| 2c95e29297 |
Generated
+141
-120
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "3x-ui-frontend",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "3x-ui-frontend",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.3",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.3.2",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
@@ -18,13 +18,13 @@
|
||||
"antd": "^6.5.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"dayjs": "^1.11.21",
|
||||
"i18next": "^26.3.5",
|
||||
"i18next": "^26.3.6",
|
||||
"otpauth": "^9.5.1",
|
||||
"persian-calendar-suite": "^1.5.5",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-hook-form": "^7.81.0",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-i18next": "^17.0.9",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"swagger-ui-react": "^5.32.8",
|
||||
"uplot": "^1.6.32",
|
||||
@@ -32,9 +32,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@storybook/addon-a11y": "^10.4.6",
|
||||
"@storybook/addon-docs": "^10.4.6",
|
||||
"@storybook/react-vite": "^10.4.6",
|
||||
"@storybook/addon-a11y": "^10.5.0",
|
||||
"@storybook/addon-docs": "^10.5.0",
|
||||
"@storybook/react-vite": "^10.5.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19.2.17",
|
||||
@@ -42,7 +42,7 @@
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint": "^10.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"globals": "^17.7.0",
|
||||
@@ -50,7 +50,7 @@
|
||||
"jsdom": "^29.1.1",
|
||||
"lint-staged": "^17.0.8",
|
||||
"msw": "^2.15.0",
|
||||
"storybook": "^10.4.6",
|
||||
"storybook": "^10.5.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.63.0",
|
||||
"vite": "8.1.4",
|
||||
@@ -2572,13 +2572,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/dropdown": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz",
|
||||
"integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.3.tgz",
|
||||
"integrity": "sha512-YTST/N6kpqpDz3IMuM/PSSZnrDpSOA6dgHv12gPA90ZTSLv2CoqkZ0+9NtwTY6BeO7dstPblSic2QJg7dSFy/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/trigger": "^3.0.0",
|
||||
"@rc-component/util": "^1.2.1",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -3084,15 +3084,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/trigger": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.1.tgz",
|
||||
"integrity": "sha512-LNsYvz60mrLJ/kRvKcHE7boUvcQfVMCfRqZ71x3Fo9AOiZ1KKIEqkzMA8DNvz2V3Bcvir/vwQNn7JF1NPODQ7Q==",
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.10.0.tgz",
|
||||
"integrity": "sha512-uC3QSG7Ax3qLOE5Q2jLqJCJc4iBtJEHzNTPhqGvlRvRcU8x8CT5moIavRVe24YSQKCp2/D1GSq7y76SCSheuVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rc-component/motion": "^1.1.4",
|
||||
"@rc-component/portal": "^2.2.0",
|
||||
"@rc-component/resize-observer": "^1.1.1",
|
||||
"@rc-component/util": "^1.2.1",
|
||||
"@rc-component/motion": "^1.3.3",
|
||||
"@rc-component/portal": "^2.2.1",
|
||||
"@rc-component/resize-observer": "^1.1.2",
|
||||
"@rc-component/util": "^1.11.1",
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3132,12 +3132,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/virtual-list": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.2.0.tgz",
|
||||
"integrity": "sha512-iavRm1Jo4GDbASQwdGa7jFyk93RvSOo9xHyBT4QL1pgFJj/Fdf1G+3RErH7/7BmAMvx2AkF62mjGYxDbXsK9TQ==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.3.2.tgz",
|
||||
"integrity": "sha512-/smuvWBFdP/Is9QuNDKynD0+T3XTXWFyNXXNKJ4sno8CE3bTOK8sfgYmQJtYwLUNX+lv0Ytd+PMshgpmdReq5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@babel/runtime": "^8.0.0",
|
||||
"@rc-component/resize-observer": "^1.0.1",
|
||||
"@rc-component/util": "^1.4.0",
|
||||
"clsx": "^2.1.1"
|
||||
@@ -3150,6 +3150,12 @@
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rc-component/virtual-list/node_modules/@babel/runtime": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.0.tgz",
|
||||
"integrity": "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
|
||||
@@ -3510,9 +3516,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-a11y": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.4.6.tgz",
|
||||
"integrity": "sha512-XCJy+f0DFOiCgUU9knRDlLDxVFI+AAQ3/wE/NF85zB9iDPPS2DwkSN+mas3zDgHt66zhN8Cq3/UiyCDUweV9Zw==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.0.tgz",
|
||||
"integrity": "sha512-AguqTsYS2NX20AF1vWt6IonbkpMXur30/dAOOYltbEW7uKmPxBNtjuiBmYB2WI4aFN4r8jje4bbDGk/Yfq58hQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3524,20 +3530,20 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.6"
|
||||
"storybook": "^10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.6.tgz",
|
||||
"integrity": "sha512-aWAfP5JMiT5a3zBJizwroCRzOCqZwDTJmvsYvwMD3ilIEa/kT1vhf6Xrbk4XIPhDwbh8Hpb/Gfnka1xBYEISWg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.0.tgz",
|
||||
"integrity": "sha512-kbZGdX+mysiY4xezhpcFEwzQ1zSXcMhSS3u09Qt8+w5XetCAMMSv/yAxzpi6z+YoH+EdQ53e1q3MFtPUkzkfUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.4.6",
|
||||
"@storybook/csf-plugin": "10.5.0",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
"@storybook/react-dom-shim": "10.4.6",
|
||||
"@storybook/react-dom-shim": "10.5.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -3548,7 +3554,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6"
|
||||
"storybook": "^10.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -3557,13 +3563,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.4.6.tgz",
|
||||
"integrity": "sha512-BHBtD81HiXUiDQz/CaFynLtWmm7AFUQn8VnXuHipZ8KlnUANopa4yqdVuy/Gwz8ub254uFI5NMZsW/KlgWNgNg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.0.tgz",
|
||||
"integrity": "sha512-KXlifNIThDgS84KqVAJXyilool8OLTWp6DGoO9h5bHM2IPLe7UcdKfOzMUBkQ807mWBk4aW1yGEekj7kC2dvmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/csf-plugin": "10.4.6",
|
||||
"@storybook/csf-plugin": "10.5.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
@@ -3571,14 +3577,14 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.6",
|
||||
"storybook": "^10.5.0",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.6.tgz",
|
||||
"integrity": "sha512-NILLxDqpA/JR/AazGWpsz+4fadJwRU4uhHephGtYpVOWnQA/DkJfKT6zpcJVq8+QA8A2zKMLX3GVKsXIrxjuDA==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.0.tgz",
|
||||
"integrity": "sha512-R7VFw6FnDZTWct0ekOcFnTfDgdHnnAuQW+AbGG9A9IZPvyH9uLJivK7L86+r9MITRrO2+v81HaFDsT/OFk6ZkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3591,7 +3597,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.4.6",
|
||||
"storybook": "^10.5.0",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@@ -3628,14 +3634,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.6.tgz",
|
||||
"integrity": "sha512-9Y7YecrVFe1/01KYjfOLxVqTg2Aq+IO6TEv6sC2U0PfD0AWCSCmQ91QqgBpN/XW4aFFWoiZNinyXMUlU8zxy2w==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.0.tgz",
|
||||
"integrity": "sha512-2IhddiREy7NqAqnFsEOfW6HBG7azIcLxhRQYploSsaemOncR29xhzvAZsG+xlWgrtvxv4h3fYQTTR4TNn8CgPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "10.4.6",
|
||||
"@storybook/react-dom-shim": "10.5.0",
|
||||
"react-docgen": "^8.0.2",
|
||||
"react-docgen-typescript": "^2.2.2"
|
||||
},
|
||||
@@ -3648,7 +3654,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6",
|
||||
"storybook": "^10.5.0",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -3664,9 +3670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.6.tgz",
|
||||
"integrity": "sha512-iGNmKzrq9vgl2PDrYAnZKI+yvac3Ym+lJXXuQaqlFRS23zA5MNm4EBX+rAG7WulqchoK6NaZ0KQOs2mAgEpTMg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.0.tgz",
|
||||
"integrity": "sha512-GwGA6zDj4Cfw6vGsdfPKfF39L0W6solNbbnjdjGa57m2ooASkidLhrXo2wgC9wh3o/jcfonmYPDRGY6sEhGDtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -3678,7 +3684,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6"
|
||||
"storybook": "^10.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -3690,19 +3696,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-vite": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.4.6.tgz",
|
||||
"integrity": "sha512-0arEQtybqGYXHbXpTot+Wv9YtG+V5Vp43QayXavPKQ20M8mpEzhyCPKd0EhqMGSC1Z1UEt0hm365WUBhI9LfKA==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.0.tgz",
|
||||
"integrity": "sha512-d9n/I3pViscXpJYT9JHxcpxVSam14HsHOr2wBqTQTiRtaiH4xSsT00HTEGm6CEZTyq/npTJyV0Qday9XhrtiDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0",
|
||||
"@rollup/pluginutils": "^5.0.2",
|
||||
"@storybook/builder-vite": "10.4.6",
|
||||
"@storybook/react": "10.4.6",
|
||||
"@storybook/builder-vite": "10.5.0",
|
||||
"@storybook/react": "10.5.0",
|
||||
"empathic": "^2.0.0",
|
||||
"magic-string": "^0.30.0",
|
||||
"react-docgen": "^8.0.0",
|
||||
"react-docgen": "^8.0.2",
|
||||
"resolve": "^1.22.8",
|
||||
"tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
@@ -3713,8 +3719,14 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6",
|
||||
"storybook": "^10.5.0",
|
||||
"typescript": ">= 4.9.x",
|
||||
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swagger-api/apidom-ast": {
|
||||
@@ -4621,9 +4633,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
|
||||
"integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
@@ -4771,9 +4783,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
|
||||
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -5658,9 +5670,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.42",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
|
||||
"integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
|
||||
"version": "2.10.43",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
|
||||
"integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -5693,9 +5705,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
|
||||
"integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==",
|
||||
"version": "4.28.6",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz",
|
||||
"integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -5714,9 +5726,9 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.42",
|
||||
"caniuse-lite": "^1.0.30001800",
|
||||
"electron-to-chromium": "^1.5.387",
|
||||
"node-releases": "^2.0.50",
|
||||
"caniuse-lite": "^1.0.30001803",
|
||||
"electron-to-chromium": "^1.5.389",
|
||||
"node-releases": "^2.0.51",
|
||||
"update-browserslist-db": "^1.2.3"
|
||||
},
|
||||
"bin": {
|
||||
@@ -5814,9 +5826,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001803",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz",
|
||||
"integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==",
|
||||
"version": "1.0.30001805",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz",
|
||||
"integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -6481,9 +6493,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.11",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
|
||||
"integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
|
||||
"version": "3.4.12",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
|
||||
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
@@ -6669,9 +6681,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz",
|
||||
"integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
|
||||
"integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -6802,9 +6814,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
|
||||
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz",
|
||||
"integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
@@ -7741,9 +7753,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.3.5",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.5.tgz",
|
||||
"integrity": "sha512-cd5lgkvjYQAPDfP3bbrAIE1wJmjTIlsrsjDqlvsHn4wFAerP5O0NfAe8RiDcXRfukN5ETe7Tlmfp6DkyuqDn6Q==",
|
||||
"version": "26.3.6",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
|
||||
"integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -7760,7 +7772,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": "^5 || ^6"
|
||||
"typescript": "^5 || ^6 || ^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -8577,6 +8589,13 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||||
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@@ -9346,9 +9365,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -9409,9 +9428,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.50",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
|
||||
"integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
|
||||
"version": "2.0.51",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
|
||||
"integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -9893,9 +9912,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||
"version": "8.5.17",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz",
|
||||
"integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -10158,9 +10177,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
|
||||
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
|
||||
"version": "17.0.9",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.9.tgz",
|
||||
"integrity": "sha512-buLzOSqHtXxjf+qgSrLWNTXVZ1jSwO6kUv3uJqSP1roGBPgNnbhFm7OmdVwWcgf2gIbUyP0J333uPyx+Btsi3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
@@ -10170,7 +10189,7 @@
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 26.2.0",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5 || ^6"
|
||||
"typescript": "^5 || ^6 || ^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
@@ -10754,9 +10773,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.1.tgz",
|
||||
"integrity": "sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz",
|
||||
"integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -11062,20 +11081,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.4.6.tgz",
|
||||
"integrity": "sha512-6wkA6LxfDSSilloITsrFOJfsnw0mDUP2h8Ls+lRt8oRsudtz2RWFhLv+Toiwg6NW7hUpdTDc2hzR7DztJid6+A==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.0.tgz",
|
||||
"integrity": "sha512-dRhM/kSSvHQR8DmZO41v5sJuz9U6zDjjR2gRBTgZN2RBSXbmF0Brvgszrvvxyx2VfxuYKzhB+xumKwWkwlBtig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@vitest/expect": "3.2.4",
|
||||
"@vitest/spy": "3.2.4",
|
||||
"@webcontainer/env": "^1.1.1",
|
||||
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
"open": "^10.2.0",
|
||||
"oxc-parser": "^0.127.0",
|
||||
"oxc-resolver": "^11.19.1",
|
||||
@@ -11094,7 +11115,7 @@
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"prettier": "^2 || ^3",
|
||||
"vite-plus": "^0.1.15"
|
||||
"vite-plus": "^0.1.15 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -11145,9 +11166,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz",
|
||||
"integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
|
||||
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -11492,22 +11513,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.7.tgz",
|
||||
"integrity": "sha512-56L0/9HELHSsG1bFCzay8UoLxzRL7kpFf7Wl5q/kSYwiSJGACvro61xnKzPNM+SadxllzdtXsKDSXE7HPeqIAw==",
|
||||
"version": "7.4.8",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.8.tgz",
|
||||
"integrity": "sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.4.7"
|
||||
"tldts-core": "^7.4.8"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.7.tgz",
|
||||
"integrity": "sha512-rNlAI8fKn/JckBMUSbNL/ES2kmDiurWaE49l+ikwEc9A6lFR7gMx9AhgQMQKBK4H5w4pKLH64JzZfB99uRsGNQ==",
|
||||
"version": "7.4.8",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.8.tgz",
|
||||
"integrity": "sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "3x-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.3",
|
||||
"type": "module",
|
||||
"description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
|
||||
"engines": {
|
||||
@@ -37,13 +37,13 @@
|
||||
"antd": "^6.5.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"dayjs": "^1.11.21",
|
||||
"i18next": "^26.3.5",
|
||||
"i18next": "^26.3.6",
|
||||
"otpauth": "^9.5.1",
|
||||
"persian-calendar-suite": "^1.5.5",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-hook-form": "^7.81.0",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-i18next": "^17.0.9",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"swagger-ui-react": "^5.32.8",
|
||||
"uplot": "^1.6.32",
|
||||
@@ -51,9 +51,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@storybook/addon-a11y": "^10.4.6",
|
||||
"@storybook/addon-docs": "^10.4.6",
|
||||
"@storybook/react-vite": "^10.4.6",
|
||||
"@storybook/addon-a11y": "^10.5.0",
|
||||
"@storybook/addon-docs": "^10.5.0",
|
||||
"@storybook/react-vite": "^10.5.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19.2.17",
|
||||
@@ -61,7 +61,7 @@
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint": "^10.7.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"globals": "^17.7.0",
|
||||
@@ -69,7 +69,7 @@
|
||||
"jsdom": "^29.1.1",
|
||||
"lint-staged": "^17.0.8",
|
||||
"msw": "^2.15.0",
|
||||
"storybook": "^10.4.6",
|
||||
"storybook": "^10.5.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.63.0",
|
||||
"vite": "8.1.4",
|
||||
|
||||
@@ -881,6 +881,7 @@
|
||||
"ApiToken": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
@@ -910,6 +911,7 @@
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"example": 1736000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
@@ -960,6 +962,7 @@
|
||||
},
|
||||
"created_at": {
|
||||
"description": "Creation timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"email": {
|
||||
@@ -972,6 +975,7 @@
|
||||
},
|
||||
"expiryTime": {
|
||||
"description": "Expiration timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"flow": {
|
||||
@@ -1033,14 +1037,17 @@
|
||||
},
|
||||
"tgId": {
|
||||
"description": "Telegram user ID for notifications",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"totalGB": {
|
||||
"description": "Total traffic limit in GB",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"description": "Last update timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
@@ -1064,6 +1071,7 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"flowOverride": {
|
||||
@@ -1096,6 +1104,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"email": {
|
||||
@@ -1105,6 +1114,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"expiryTime": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"flow": {
|
||||
@@ -1148,12 +1158,15 @@
|
||||
"type": "string"
|
||||
},
|
||||
"tgId": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"totalGB": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"uuid": {
|
||||
@@ -1206,6 +1219,7 @@
|
||||
"properties": {
|
||||
"down": {
|
||||
"example": 2097152,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"email": {
|
||||
@@ -1218,6 +1232,7 @@
|
||||
},
|
||||
"expiryTime": {
|
||||
"example": 1735689600000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
@@ -1230,6 +1245,7 @@
|
||||
},
|
||||
"lastOnline": {
|
||||
"example": 1735680000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"reset": {
|
||||
@@ -1242,10 +1258,12 @@
|
||||
},
|
||||
"total": {
|
||||
"example": 10737418240,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"up": {
|
||||
"example": 1048576,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"uuid": {
|
||||
@@ -1316,6 +1334,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"echConfigList": {
|
||||
@@ -1431,6 +1450,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"verifyPeerCertByName": {
|
||||
@@ -1660,6 +1680,7 @@
|
||||
},
|
||||
"down": {
|
||||
"description": "Download traffic in bytes",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enable": {
|
||||
@@ -1669,6 +1690,7 @@
|
||||
},
|
||||
"expiryTime": {
|
||||
"description": "Expiration timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"fallbackParent": {
|
||||
@@ -1687,6 +1709,7 @@
|
||||
},
|
||||
"lastTrafficResetTime": {
|
||||
"description": "Last traffic reset timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"listen": {
|
||||
@@ -1755,6 +1778,7 @@
|
||||
},
|
||||
"total": {
|
||||
"description": "Total traffic limit in bytes",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"trafficReset": {
|
||||
@@ -1770,6 +1794,7 @@
|
||||
},
|
||||
"up": {
|
||||
"description": "Upload traffic in bytes",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
@@ -1981,6 +2006,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"configDirtyAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"cpuPct": {
|
||||
@@ -1989,6 +2015,7 @@
|
||||
},
|
||||
"createdAt": {
|
||||
"example": 1700000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"depletedCount": {
|
||||
@@ -2034,6 +2061,7 @@
|
||||
"lastHeartbeat": {
|
||||
"description": "unix seconds, 0 = never",
|
||||
"example": 1700000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"latencyMs": {
|
||||
@@ -2050,10 +2078,12 @@
|
||||
},
|
||||
"netDown": {
|
||||
"example": 2097152,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"netUp": {
|
||||
"example": 1048576,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"onlineCount": {
|
||||
@@ -2110,10 +2140,12 @@
|
||||
},
|
||||
"updatedAt": {
|
||||
"example": 1700000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"uptimeSecs": {
|
||||
"example": 86400,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"xrayError": {
|
||||
@@ -2175,6 +2207,7 @@
|
||||
"description": "OutboundTraffics tracks traffic statistics for Xray outbound connections.",
|
||||
"properties": {
|
||||
"down": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
@@ -2184,9 +2217,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"total": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"up": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
@@ -2208,6 +2243,7 @@
|
||||
},
|
||||
"finishedAt": {
|
||||
"example": 1735689612,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"runId": {
|
||||
@@ -2254,6 +2290,7 @@
|
||||
},
|
||||
"uptimeSecs": {
|
||||
"example": 86400,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"xrayError": {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { Button, Input, Modal, message } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Input, Modal, Tabs, message } from 'antd';
|
||||
import { CopyOutlined, DownloadOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import JsonEditor from '@/components/form/JsonEditor';
|
||||
import { ClipboardManager, FileManager } from '@/utils';
|
||||
|
||||
export interface TextModalTab {
|
||||
key: string;
|
||||
label: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface TextModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -12,13 +19,23 @@ interface TextModalProps {
|
||||
content: string;
|
||||
fileName?: string;
|
||||
json?: boolean;
|
||||
tabs?: TextModalTab[];
|
||||
}
|
||||
|
||||
export default function TextModal({ open, onClose, title, content, fileName = '', json = false }: TextModalProps) {
|
||||
export default function TextModal({ open, onClose, title, content, fileName = '', json = false, tabs }: TextModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [activeKey, setActiveKey] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (open && tabs && tabs.length > 0) setActiveKey(tabs[0].key);
|
||||
}, [open, tabs]);
|
||||
|
||||
const activeTab = tabs?.find((tab) => tab.key === activeKey) ?? tabs?.[0];
|
||||
const activeContent = activeTab ? activeTab.content : content;
|
||||
|
||||
async function copy() {
|
||||
const ok = await ClipboardManager.copyText(content || '');
|
||||
const ok = await ClipboardManager.copyText(activeContent || '');
|
||||
if (ok) {
|
||||
messageApi.success(t('copied'));
|
||||
onClose();
|
||||
@@ -27,7 +44,7 @@ export default function TextModal({ open, onClose, title, content, fileName = ''
|
||||
|
||||
function download() {
|
||||
if (!fileName) return;
|
||||
FileManager.downloadTextFile(content, fileName);
|
||||
FileManager.downloadTextFile(activeContent, fileName);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -47,11 +64,18 @@ export default function TextModal({ open, onClose, title, content, fileName = ''
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{tabs && tabs.length > 0 && (
|
||||
<Tabs
|
||||
activeKey={activeTab?.key}
|
||||
onChange={setActiveKey}
|
||||
items={tabs.map((tab) => ({ key: tab.key, label: tab.label }))}
|
||||
/>
|
||||
)}
|
||||
{json ? (
|
||||
<JsonEditor value={content} readOnly minHeight="240px" maxHeight="60vh" />
|
||||
<JsonEditor value={activeContent} readOnly minHeight="240px" maxHeight="60vh" />
|
||||
) : (
|
||||
<Input.TextArea
|
||||
value={content}
|
||||
value={activeContent}
|
||||
readOnly
|
||||
autoSize={{ minRows: 10, maxRows: 20 }}
|
||||
style={{
|
||||
|
||||
@@ -855,6 +855,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"ApiToken": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
@@ -884,6 +885,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"example": 1736000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
@@ -934,6 +936,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"created_at": {
|
||||
"description": "Creation timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"email": {
|
||||
@@ -946,6 +949,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"expiryTime": {
|
||||
"description": "Expiration timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"flow": {
|
||||
@@ -1007,14 +1011,17 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"tgId": {
|
||||
"description": "Telegram user ID for notifications",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"totalGB": {
|
||||
"description": "Total traffic limit in GB",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"description": "Last update timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
@@ -1038,6 +1045,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "integer"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"flowOverride": {
|
||||
@@ -1070,6 +1078,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"email": {
|
||||
@@ -1079,6 +1088,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "boolean"
|
||||
},
|
||||
"expiryTime": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"flow": {
|
||||
@@ -1122,12 +1132,15 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "string"
|
||||
},
|
||||
"tgId": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"totalGB": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"uuid": {
|
||||
@@ -1180,6 +1193,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"properties": {
|
||||
"down": {
|
||||
"example": 2097152,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"email": {
|
||||
@@ -1192,6 +1206,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"expiryTime": {
|
||||
"example": 1735689600000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
@@ -1204,6 +1219,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"lastOnline": {
|
||||
"example": 1735680000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"reset": {
|
||||
@@ -1216,10 +1232,12 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"total": {
|
||||
"example": 10737418240,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"up": {
|
||||
"example": 1048576,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"uuid": {
|
||||
@@ -1290,6 +1308,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "array"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"echConfigList": {
|
||||
@@ -1405,6 +1424,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "array"
|
||||
},
|
||||
"updatedAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"verifyPeerCertByName": {
|
||||
@@ -1634,6 +1654,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"down": {
|
||||
"description": "Download traffic in bytes",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enable": {
|
||||
@@ -1643,6 +1664,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"expiryTime": {
|
||||
"description": "Expiration timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"fallbackParent": {
|
||||
@@ -1661,6 +1683,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"lastTrafficResetTime": {
|
||||
"description": "Last traffic reset timestamp",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"listen": {
|
||||
@@ -1729,6 +1752,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"total": {
|
||||
"description": "Total traffic limit in bytes",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"trafficReset": {
|
||||
@@ -1744,6 +1768,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"up": {
|
||||
"description": "Upload traffic in bytes",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
@@ -1955,6 +1980,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "boolean"
|
||||
},
|
||||
"configDirtyAt": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"cpuPct": {
|
||||
@@ -1963,6 +1989,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"createdAt": {
|
||||
"example": 1700000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"depletedCount": {
|
||||
@@ -2008,6 +2035,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"lastHeartbeat": {
|
||||
"description": "unix seconds, 0 = never",
|
||||
"example": 1700000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"latencyMs": {
|
||||
@@ -2024,10 +2052,12 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"netDown": {
|
||||
"example": 2097152,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"netUp": {
|
||||
"example": 1048576,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"onlineCount": {
|
||||
@@ -2084,10 +2114,12 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"updatedAt": {
|
||||
"example": 1700000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"uptimeSecs": {
|
||||
"example": 86400,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"xrayError": {
|
||||
@@ -2149,6 +2181,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"description": "OutboundTraffics tracks traffic statistics for Xray outbound connections.",
|
||||
"properties": {
|
||||
"down": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
@@ -2158,9 +2191,11 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
"type": "string"
|
||||
},
|
||||
"total": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"up": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
@@ -2182,6 +2217,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"finishedAt": {
|
||||
"example": 1735689612,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"runId": {
|
||||
@@ -2228,6 +2264,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
||||
},
|
||||
"uptimeSecs": {
|
||||
"example": 86400,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"xrayError": {
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { InboundOption } from '@/hooks/useClients';
|
||||
import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||
import type { BulkAttachResult } from '@/schemas/client';
|
||||
|
||||
const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
|
||||
const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'wireguard', 'mtproto']);
|
||||
|
||||
interface BulkAttachInboundsModalProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { InboundOption } from '@/hooks/useClients';
|
||||
import { formatInboundLabel } from '@/lib/inbounds/label';
|
||||
import type { BulkDetachResult } from '@/schemas/client';
|
||||
|
||||
const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
|
||||
const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks', 'wireguard', 'mtproto']);
|
||||
|
||||
interface BulkDetachInboundsModalProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
|
||||
import { HttpUtil, SizeFormatter, RandomUtil } from '@/utils';
|
||||
import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
|
||||
import { genInboundLinks, preferPublicHost } from '@/lib/xray/inbound-link';
|
||||
import { genInboundLinks, genWireguardLinks, preferPublicHost } from '@/lib/xray/inbound-link';
|
||||
import { inboundFromDb } from '@/lib/xray/inbound-from-db';
|
||||
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
@@ -33,6 +33,7 @@ import { useWebSocket } from '@/hooks/useWebSocket';
|
||||
import { useNodesQuery } from '@/api/queries/useNodesQuery';
|
||||
import AppSidebar from '@/layouts/AppSidebar';
|
||||
const TextModal = lazy(() => import('@/components/feedback/TextModal'));
|
||||
import type { TextModalTab } from '@/components/feedback/TextModal';
|
||||
const PromptModal = lazy(() => import('@/components/feedback/PromptModal'));
|
||||
|
||||
import { useInbounds } from './useInbounds';
|
||||
@@ -148,6 +149,7 @@ export default function InboundsPage() {
|
||||
const [textContent, setTextContent] = useState('');
|
||||
const [textFileName, setTextFileName] = useState('');
|
||||
const [textJson, setTextJson] = useState(false);
|
||||
const [textTabs, setTextTabs] = useState<TextModalTab[] | undefined>(undefined);
|
||||
|
||||
const [promptOpen, setPromptOpen] = useState(false);
|
||||
const [promptTitle, setPromptTitle] = useState('');
|
||||
@@ -166,11 +168,12 @@ export default function InboundsPage() {
|
||||
const infoNodeAddress = useMemo(() => hostOverrideFor(infoDbInbound), [infoDbInbound, hostOverrideFor]);
|
||||
const qrNodeAddress = useMemo(() => hostOverrideFor(qrDbInbound), [qrDbInbound, hostOverrideFor]);
|
||||
|
||||
const openText = useCallback((opts: { title: string; content: string; fileName?: string; json?: boolean }) => {
|
||||
const openText = useCallback((opts: { title: string; content: string; fileName?: string; json?: boolean; tabs?: TextModalTab[] }) => {
|
||||
setTextTitle(opts.title);
|
||||
setTextContent(opts.content);
|
||||
setTextFileName(opts.fileName || '');
|
||||
setTextJson(opts.json || false);
|
||||
setTextTabs(opts.tabs);
|
||||
setTextOpen(true);
|
||||
}, []);
|
||||
|
||||
@@ -259,15 +262,24 @@ export default function InboundsPage() {
|
||||
|
||||
const exportInboundLinks = useCallback((dbInbound: DBInbound) => {
|
||||
const projected = checkFallback(dbInbound);
|
||||
const genInput = {
|
||||
inbound: inboundFromDb(projected),
|
||||
remark: projected.remark,
|
||||
hostOverride: hostOverrideFor(dbInbound),
|
||||
fallbackHostname: preferPublicHost(window.location.hostname, subSettings.publicHost),
|
||||
};
|
||||
const content = genInboundLinks(genInput);
|
||||
const tabs: TextModalTab[] | undefined = projected.isWireguard
|
||||
? [
|
||||
{ key: 'config', label: t('pages.clients.config'), content },
|
||||
{ key: 'links', label: t('pages.clients.tabLinks'), content: genWireguardLinks(genInput) },
|
||||
]
|
||||
: undefined;
|
||||
openText({
|
||||
title: t('pages.inbounds.exportLinksTitle'),
|
||||
content: genInboundLinks({
|
||||
inbound: inboundFromDb(projected),
|
||||
remark: projected.remark,
|
||||
hostOverride: hostOverrideFor(dbInbound),
|
||||
fallbackHostname: preferPublicHost(window.location.hostname, subSettings.publicHost),
|
||||
}),
|
||||
content,
|
||||
fileName: projected.remark || 'inbound',
|
||||
tabs,
|
||||
});
|
||||
}, [checkFallback, hostOverrideFor, subSettings.publicHost, openText, t]);
|
||||
|
||||
@@ -706,6 +718,7 @@ export default function InboundsPage() {
|
||||
content={textContent}
|
||||
fileName={textFileName}
|
||||
json={textJson}
|
||||
tabs={textTabs}
|
||||
/>
|
||||
</LazyMount>
|
||||
<LazyMount when={promptOpen}>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function buildRowActionsMenu({ record, subEnable, t, isMobile, hasClients
|
||||
label: `${t('pages.inbounds.export')} — ${t('pages.settings.subSettings')}`,
|
||||
});
|
||||
}
|
||||
} else if (!record.isWireguard) {
|
||||
} else {
|
||||
items.push({ key: 'showInfo', icon: <InfoCircleOutlined />, label: t('pages.inbounds.inboundInfo') });
|
||||
}
|
||||
items.push({ key: 'clipboard', icon: <CopyOutlined />, label: t('pages.inbounds.exportInbound') });
|
||||
|
||||
@@ -73,6 +73,7 @@ export function isInboundMultiUser(record: { protocol: string; settings: unknown
|
||||
case 'trojan':
|
||||
case 'hysteria':
|
||||
case 'mtproto':
|
||||
case 'wireguard':
|
||||
return true;
|
||||
case 'shadowsocks':
|
||||
return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
|
||||
|
||||
@@ -60,6 +60,8 @@ const TRACKED_PROTOCOLS: readonly string[] = [
|
||||
Protocols.TROJAN,
|
||||
Protocols.SHADOWSOCKS,
|
||||
Protocols.HYSTERIA,
|
||||
Protocols.WIREGUARD,
|
||||
Protocols.MTPROTO,
|
||||
];
|
||||
|
||||
async function fetchSlimInbounds(): Promise<unknown[]> {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { CloudOutlined } from '@ant-design/icons';
|
||||
|
||||
interface CountryPillProps {
|
||||
flag: string;
|
||||
name: string;
|
||||
warp?: string;
|
||||
}
|
||||
|
||||
export default function CountryPill({ flag, name, warp }: CountryPillProps) {
|
||||
const isWarp = !!warp && warp.toLowerCase() !== 'off';
|
||||
return (
|
||||
<span className={isWarp ? 'country-pill warp-on' : 'country-pill'}>
|
||||
{isWarp && <CloudOutlined className="warp-cloud-icon" />}
|
||||
{flag && <span>{flag}</span>}
|
||||
<span>{name}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Dropdown, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
@@ -9,15 +10,21 @@ import {
|
||||
ThunderboltOutlined,
|
||||
LoadingOutlined,
|
||||
ExportOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { SizeFormatter } from '@/utils';
|
||||
import { activateOnKey } from '@/utils/a11y';
|
||||
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
|
||||
import type { OutboundTestMode, OutboundTestState, OutboundTrafficRow } from '@/hooks/useXraySetting';
|
||||
|
||||
import type { OutboundRow } from './outbounds-tab-types';
|
||||
import CountryPill from './CountryPill';
|
||||
import TestResultPopover from './TestResultPopover';
|
||||
import {
|
||||
countryFlag,
|
||||
countryName,
|
||||
isTesting,
|
||||
isUntestable,
|
||||
outboundAddresses,
|
||||
@@ -49,7 +56,55 @@ export default function OutboundCardList({
|
||||
confirmDelete,
|
||||
onTest,
|
||||
}: OutboundCardListProps) {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [showEgressIp, setShowEgressIp] = useState<Record<string, boolean>>({});
|
||||
|
||||
const setCardEgressVisible = (key: string, visible: boolean) => {
|
||||
setShowEgressIp((prev) => ({ ...prev, [key]: visible }));
|
||||
};
|
||||
|
||||
const renderEgress = (index: number, rowKey: string) => {
|
||||
const result = testResult(outboundTestStates, index);
|
||||
const egress = result?.egress;
|
||||
const isEgressVisible = !!showEgressIp[rowKey];
|
||||
const flag = countryFlag(egress?.country);
|
||||
const name = countryName(egress?.country, i18n.language);
|
||||
const addresses = [
|
||||
egress?.ipv4 ? { label: 'v4', value: egress.ipv4 } : null,
|
||||
egress?.ipv6 ? { label: 'v6', value: egress.ipv6 } : null,
|
||||
].filter((item): item is { label: string; value: string } => Boolean(item));
|
||||
|
||||
if (!egress || (addresses.length === 0 && !egress.country)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card-egress">
|
||||
<div className="card-egress-row">
|
||||
<span>{t('pages.xray.outbound.egress')}:</span>
|
||||
<Tooltip title={t('pages.index.toggleIpVisibility')}>
|
||||
{isEgressVisible ? (
|
||||
<EyeOutlined className="ip-toggle-icon" role="button" tabIndex={0} aria-label={t('pages.index.toggleIpVisibility')} onClick={() => setCardEgressVisible(rowKey, false)} onKeyDown={activateOnKey(() => setCardEgressVisible(rowKey, false))} />
|
||||
) : (
|
||||
<EyeInvisibleOutlined className="ip-toggle-icon" role="button" tabIndex={0} aria-label={t('pages.index.toggleIpVisibility')} onClick={() => setCardEgressVisible(rowKey, true)} onKeyDown={activateOnKey(() => setCardEgressVisible(rowKey, true))} />
|
||||
)}
|
||||
</Tooltip>
|
||||
{egress.country && (
|
||||
<CountryPill flag={flag} name={name || egress.country} warp={egress.warp} />
|
||||
)}
|
||||
</div>
|
||||
{addresses.map((addr) => (
|
||||
<Tooltip key={addr.label} title={addr.value}>
|
||||
<div className="card-egress-row">
|
||||
<span className="egress-family">{addr.label}:</span>
|
||||
<span className={isEgressVisible ? 'address-visible egress-ip' : 'address-hidden egress-ip'}>{addr.value}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<div className="card-empty">
|
||||
@@ -101,6 +156,7 @@ export default function OutboundCardList({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{renderEgress(index, String(record.key))}
|
||||
<div className="card-foot">
|
||||
<span className="traffic-up">↑ {SizeFormatter.sizeFormat(trafficFor(outboundsTraffic, record).up)}</span>
|
||||
<span className="traffic-sep" />
|
||||
|
||||
@@ -75,6 +75,81 @@
|
||||
background: var(--ant-color-fill-tertiary);
|
||||
}
|
||||
|
||||
.egress-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.egress-ip {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.egress-stack {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.egress-address {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.egress-family {
|
||||
color: var(--ant-color-text-secondary);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.country-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--ant-color-border);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.country-pill.warp-on {
|
||||
border-color: #f48120;
|
||||
}
|
||||
|
||||
.warp-cloud-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
color: #f48120;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.warp-cloud-icon svg {
|
||||
fill: #f48120;
|
||||
}
|
||||
|
||||
.ip-toggle-icon {
|
||||
cursor: pointer;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.ip-toggle-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.address-hidden {
|
||||
filter: blur(5px);
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
.address-visible {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -110,6 +185,23 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.card-egress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.card-egress-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-test {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -61,6 +61,22 @@ export function trafficFor(outboundsTraffic: OutboundTrafficRow[], o: OutboundRo
|
||||
return { up: tr?.up || 0, down: tr?.down || 0 };
|
||||
}
|
||||
|
||||
export function countryFlag(country?: string): string {
|
||||
const code = (country || '').trim().toUpperCase();
|
||||
if (!/^[A-Z]{2}$/.test(code)) return '';
|
||||
return String.fromCodePoint(...[...code].map((ch) => 0x1f1e6 + ch.charCodeAt(0) - 65));
|
||||
}
|
||||
|
||||
export function countryName(country?: string, locale?: string): string {
|
||||
const code = (country || '').trim().toUpperCase();
|
||||
if (!/^[A-Z]{2}$/.test(code)) return '';
|
||||
try {
|
||||
return new Intl.DisplayNames(locale ? [locale] : undefined, { type: 'region' }).of(code) || code;
|
||||
} catch {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
export function isTesting<K extends string | number>(states: Record<K, OutboundTestState>, idx: K): boolean {
|
||||
return !!states?.[idx]?.testing;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Dropdown, Tag, Tooltip } from 'antd';
|
||||
import {
|
||||
@@ -11,17 +11,23 @@ import {
|
||||
LoadingOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
import { SizeFormatter } from '@/utils';
|
||||
import { activateOnKey } from '@/utils/a11y';
|
||||
import { OutboundProtocols as Protocols } from '@/schemas/primitives';
|
||||
import type { OutboundTestMode, OutboundTestState, OutboundTrafficRow } from '@/hooks/useXraySetting';
|
||||
|
||||
import type { OutboundRow } from './outbounds-tab-types';
|
||||
import CountryPill from './CountryPill';
|
||||
import TestResultPopover from './TestResultPopover';
|
||||
import {
|
||||
effectiveTestMode,
|
||||
countryFlag,
|
||||
countryName,
|
||||
isTesting,
|
||||
isUntestable,
|
||||
outboundAddresses,
|
||||
@@ -58,7 +64,8 @@ export function useOutboundColumns({
|
||||
onResetTraffic,
|
||||
onTest,
|
||||
}: OutboundColumnsParams): ColumnsType<OutboundRow> {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const [showEgressIp, setShowEgressIp] = useState(false);
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -135,6 +142,72 @@ export function useOutboundColumns({
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span className="egress-header">
|
||||
{t('pages.xray.outbound.egress')}
|
||||
<Tooltip title={t('pages.index.toggleIpVisibility')}>
|
||||
{showEgressIp ? (
|
||||
<EyeOutlined className="ip-toggle-icon" role="button" tabIndex={0} aria-label={t('pages.index.toggleIpVisibility')} onClick={() => setShowEgressIp(false)} onKeyDown={activateOnKey(() => setShowEgressIp(false))} />
|
||||
) : (
|
||||
<EyeInvisibleOutlined className="ip-toggle-icon" role="button" tabIndex={0} aria-label={t('pages.index.toggleIpVisibility')} onClick={() => setShowEgressIp(true)} onKeyDown={activateOnKey(() => setShowEgressIp(true))} />
|
||||
)}
|
||||
</Tooltip>
|
||||
</span>
|
||||
),
|
||||
key: 'egress',
|
||||
align: 'left',
|
||||
width: 210,
|
||||
render: (_v, _record, index) => {
|
||||
const egress = testResult(outboundTestStates, index)?.egress;
|
||||
const addresses = [
|
||||
egress?.ipv4 ? { label: 'v4', value: egress.ipv4 } : null,
|
||||
egress?.ipv6 ? { label: 'v6', value: egress.ipv6 } : null,
|
||||
].filter((item): item is { label: string; value: string } => Boolean(item));
|
||||
if (addresses.length === 0) {
|
||||
return (
|
||||
<Tooltip title={t('pages.xray.outbound.egressHint')}>
|
||||
<span className="empty">—</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="egress-stack">
|
||||
{addresses.map((addr) => (
|
||||
<Tooltip key={addr.label} title={addr.value}>
|
||||
<span className="egress-address">
|
||||
<span className="egress-family">{addr.label}</span>
|
||||
<span className={showEgressIp ? 'address-visible egress-ip' : 'address-hidden egress-ip'}>{addr.value}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('pages.xray.outbound.country'),
|
||||
key: 'egressCountry',
|
||||
align: 'left',
|
||||
width: 160,
|
||||
render: (_v, _record, index) => {
|
||||
const egress = testResult(outboundTestStates, index)?.egress;
|
||||
if (!egress?.country) {
|
||||
return (
|
||||
<Tooltip title={t('pages.xray.outbound.egressHint')}>
|
||||
<span className="empty">—</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
const flag = countryFlag(egress.country);
|
||||
const name = countryName(egress.country, i18n.language);
|
||||
return (
|
||||
<Tooltip title={egress.warp ? `Cloudflare trace · WARP ${egress.warp}` : 'Cloudflare trace'}>
|
||||
<CountryPill flag={flag} name={name || egress.country} warp={egress.warp} />
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('pages.inbounds.traffic'),
|
||||
key: 'traffic',
|
||||
@@ -183,6 +256,6 @@ export function useOutboundColumns({
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[t, testMode, rows, outboundTestStates, outboundsTraffic],
|
||||
[t, i18n.language, testMode, rows, outboundTestStates, outboundsTraffic, showEgressIp],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,6 +78,15 @@ export const OutboundTestResultSchema = z.object({
|
||||
}).loose(),
|
||||
)
|
||||
.optional(),
|
||||
egress: z
|
||||
.object({
|
||||
ipv4: z.string().optional(),
|
||||
ipv6: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
warp: z.string().optional(),
|
||||
})
|
||||
.loose()
|
||||
.optional(),
|
||||
}).loose();
|
||||
|
||||
// Batch results from /xray/testOutbounds, aligned with the request order.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package outbound
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseCloudflareTrace(t *testing.T) {
|
||||
values := parseCloudflareTrace("ip=104.28.1.2\nloc=NL\nwarp=on\n")
|
||||
|
||||
if values["ip"] != "104.28.1.2" {
|
||||
t.Fatalf("ip = %q", values["ip"])
|
||||
}
|
||||
if values["loc"] != "NL" {
|
||||
t.Fatalf("loc = %q", values["loc"])
|
||||
}
|
||||
if values["warp"] != "on" {
|
||||
t.Fatalf("warp = %q", values["warp"])
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,7 @@ type TestOutboundResult struct {
|
||||
TTFBMs int64 `json:"ttfbMs,omitempty"`
|
||||
|
||||
Endpoints []TestEndpointResult `json:"endpoints,omitempty"`
|
||||
Egress *TestEgressResult `json:"egress,omitempty"`
|
||||
}
|
||||
|
||||
// TestEndpointResult is one entry in a TCP-mode probe — the per-endpoint
|
||||
@@ -153,6 +154,15 @@ type TestEndpointResult struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// TestEgressResult is populated by HTTP-mode probes from Cloudflare's trace
|
||||
// endpoint. It reports what an external service sees after the outbound chain.
|
||||
type TestEgressResult struct {
|
||||
IPv4 string `json:"ipv4,omitempty"`
|
||||
IPv6 string `json:"ipv6,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
Warp string `json:"warp,omitempty"`
|
||||
}
|
||||
|
||||
func (s *OutboundService) testOutboundTCP(outboundJSON string) (*TestOutboundResult, error) {
|
||||
var ob map[string]any
|
||||
if err := json.Unmarshal([]byte(outboundJSON), &ob); err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -54,8 +55,13 @@ const (
|
||||
// tcpBatchConcurrency caps parallel TCP-mode items in a batch (each item
|
||||
// already dials its endpoints concurrently).
|
||||
tcpBatchConcurrency = 8
|
||||
// egressTraceTimeout keeps diagnostic trace metadata from extending a
|
||||
// successful HTTP probe by the full reachability timeout.
|
||||
egressTraceTimeout = 3 * time.Second
|
||||
|
||||
defaultTestURL = "https://www.google.com/generate_204"
|
||||
defaultTestURL = "https://www.google.com/generate_204"
|
||||
egressTraceHost = "cloudflare.com"
|
||||
egressTracePath = "/cdn-cgi/trace"
|
||||
)
|
||||
|
||||
// httpTestSemaphore serialises HTTP-mode batches (each spawns a temp xray
|
||||
@@ -76,6 +82,8 @@ var newBatchProcess = func(cfg *xray.Config, configPath string) batchProcess {
|
||||
return xray.NewTestProcess(cfg, configPath)
|
||||
}
|
||||
|
||||
var egressTraceProbe = probeEgressTrace
|
||||
|
||||
// httpBatchItem is one outbound inside an HTTP-mode batch. result is the
|
||||
// pre-allocated entry in the caller's result slice, filled in place.
|
||||
type httpBatchItem struct {
|
||||
@@ -542,6 +550,150 @@ func probeThroughSocks(port int, testURL string, timeout time.Duration, realDela
|
||||
}
|
||||
}
|
||||
result.Delay = max(delay, 1)
|
||||
if !realDelay {
|
||||
result.Egress = egressTraceProbe(proxyURL)
|
||||
}
|
||||
}
|
||||
|
||||
// probeEgressTrace fetches Cloudflare's plain-text trace endpoint through the
|
||||
// same SOCKS route used by the HTTP probe. It asks one IPv4 and one IPv6
|
||||
// Cloudflare address directly, when available, while keeping the TLS SNI as
|
||||
// cloudflare.com. Failures are intentionally ignored by the caller: egress
|
||||
// metadata is diagnostic, not reachability.
|
||||
func probeEgressTrace(proxyURL *url.URL) *TestEgressResult {
|
||||
ipv4, ipv6 := cloudflareTraceTargets()
|
||||
if ipv4 == nil && ipv6 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
TLSClientConfig: &tls.Config{
|
||||
ServerName: egressTraceHost,
|
||||
},
|
||||
}
|
||||
defer tr.CloseIdleConnections()
|
||||
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: egressTraceTimeout,
|
||||
}
|
||||
|
||||
egress := &TestEgressResult{}
|
||||
targets := make([]net.IP, 0, 2)
|
||||
if ipv4 != nil {
|
||||
targets = append(targets, ipv4)
|
||||
}
|
||||
if ipv6 != nil {
|
||||
targets = append(targets, ipv6)
|
||||
}
|
||||
results := make(chan map[string]string, len(targets))
|
||||
for _, target := range targets {
|
||||
go func(ip net.IP) {
|
||||
results <- fetchCloudflareTrace(client, ip)
|
||||
}(target)
|
||||
}
|
||||
for range targets {
|
||||
applyEgressTrace(egress, <-results)
|
||||
}
|
||||
if egress.IPv4 == "" && egress.IPv6 == "" && egress.Country == "" && egress.Warp == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return egress
|
||||
}
|
||||
|
||||
func cloudflareTraceTargets() (net.IP, net.IP) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), egressTraceTimeout)
|
||||
defer cancel()
|
||||
|
||||
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, egressTraceHost)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
var ipv4, ipv6 net.IP
|
||||
for _, addr := range addrs {
|
||||
ip := addr.IP
|
||||
if ipv4 == nil {
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
ipv4 = v4
|
||||
continue
|
||||
}
|
||||
}
|
||||
if ipv6 == nil && ip.To4() == nil && ip.To16() != nil {
|
||||
ipv6 = ip
|
||||
}
|
||||
if ipv4 != nil && ipv6 != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ipv4, ipv6
|
||||
}
|
||||
|
||||
func fetchCloudflareTrace(client *http.Client, ip net.IP) map[string]string {
|
||||
if ip == nil {
|
||||
return nil
|
||||
}
|
||||
traceURL := (&url.URL{
|
||||
Scheme: "https",
|
||||
Host: net.JoinHostPort(ip.String(), "443"),
|
||||
Path: egressTracePath,
|
||||
}).String()
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, traceURL, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
req.Host = egressTraceHost
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 16<<10))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return parseCloudflareTrace(string(body))
|
||||
}
|
||||
|
||||
func applyEgressTrace(egress *TestEgressResult, values map[string]string) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
if ip := net.ParseIP(values["ip"]); ip != nil {
|
||||
if ip.To4() != nil {
|
||||
if egress.IPv4 == "" {
|
||||
egress.IPv4 = values["ip"]
|
||||
}
|
||||
} else if egress.IPv6 == "" {
|
||||
egress.IPv6 = values["ip"]
|
||||
}
|
||||
}
|
||||
if egress.Country == "" {
|
||||
egress.Country = values["loc"]
|
||||
}
|
||||
if values["warp"] == "on" || egress.Warp == "" {
|
||||
egress.Warp = values["warp"]
|
||||
}
|
||||
}
|
||||
|
||||
func parseCloudflareTrace(body string) map[string]string {
|
||||
values := make(map[string]string)
|
||||
for line := range strings.SplitSeq(body, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
values[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// timedWarmGet re-issues the probe request over the transport's kept-alive
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -135,6 +136,13 @@ func withStubProcess(t *testing.T, factory func(cfg *xray.Config, configPath str
|
||||
t.Cleanup(func() { newBatchProcess = orig })
|
||||
}
|
||||
|
||||
func withEgressTraceProbe(t *testing.T, probe func(*url.URL) *TestEgressResult) {
|
||||
t.Helper()
|
||||
orig := egressTraceProbe
|
||||
egressTraceProbe = probe
|
||||
t.Cleanup(func() { egressTraceProbe = orig })
|
||||
}
|
||||
|
||||
func mustJSON(t *testing.T, v any) string {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
@@ -417,6 +425,9 @@ func TestTestOutboundsHTTPBatchThroughStubSocks(t *testing.T) {
|
||||
proc = &stubProcess{cfg: cfg, serveSocks: true}
|
||||
return proc
|
||||
})
|
||||
withEgressTraceProbe(t, func(*url.URL) *TestEgressResult {
|
||||
return &TestEgressResult{IPv4: "198.51.100.1", Country: "ZZ", Warp: "off"}
|
||||
})
|
||||
|
||||
batch := mustJSON(t, []any{
|
||||
map[string]any{"tag": "a", "protocol": "vless"},
|
||||
@@ -445,6 +456,9 @@ func TestTestOutboundsHTTPBatchThroughStubSocks(t *testing.T) {
|
||||
if r.Mode != "http" {
|
||||
t.Errorf("result %d mode = %q", i, r.Mode)
|
||||
}
|
||||
if r.Egress == nil || r.Egress.IPv4 != "198.51.100.1" {
|
||||
t.Errorf("result %d egress = %+v", i, r.Egress)
|
||||
}
|
||||
}
|
||||
if proc.IsRunning() {
|
||||
t.Error("temp process not stopped after batch")
|
||||
@@ -525,6 +539,9 @@ func TestTestOutboundsTCPModeForcesUDPToHTTPProbe(t *testing.T) {
|
||||
withStubProcess(t, func(cfg *xray.Config, configPath string) batchProcess {
|
||||
return &stubProcess{cfg: cfg, serveSocks: true}
|
||||
})
|
||||
withEgressTraceProbe(t, func(*url.URL) *TestEgressResult {
|
||||
return &TestEgressResult{IPv4: "198.51.100.2", Country: "ZZ", Warp: "off"}
|
||||
})
|
||||
|
||||
batch := mustJSON(t, []any{map[string]any{"tag": "wg", "protocol": "wireguard"}})
|
||||
results, err := (&OutboundService{}).TestOutbounds(batch, srv.URL, "", "tcp")
|
||||
@@ -535,6 +552,9 @@ func TestTestOutboundsTCPModeForcesUDPToHTTPProbe(t *testing.T) {
|
||||
if !r.Success || r.Mode != "http" {
|
||||
t.Errorf("UDP outbound in tcp mode = %+v, want success with mode %q", r, "http")
|
||||
}
|
||||
if r.Egress == nil || r.Egress.IPv4 != "198.51.100.2" {
|
||||
t.Errorf("UDP outbound egress = %+v", r.Egress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeModeLabel(t *testing.T) {
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "الوسم",
|
||||
"tagDesc": "تاج فريد",
|
||||
"address": "العنوان",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "عكسي",
|
||||
"domain": "النطاق",
|
||||
"type": "النوع",
|
||||
|
||||
@@ -1739,6 +1739,8 @@
|
||||
"tag": "Tag",
|
||||
"tagDesc": "Unique Tag",
|
||||
"address": "Address",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "Reverse",
|
||||
"domain": "Domain",
|
||||
"type": "Type",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "Etiqueta",
|
||||
"tagDesc": "etiqueta única",
|
||||
"address": "Dirección",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "Reverso",
|
||||
"domain": "Dominio",
|
||||
"type": "Tipo",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "تگ",
|
||||
"tagDesc": "برچسب یگانه",
|
||||
"address": "آدرس",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "معکوس",
|
||||
"domain": "دامنه",
|
||||
"type": "نوع",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "Tag",
|
||||
"tagDesc": "Tag Unik",
|
||||
"address": "Alamat",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "Revers",
|
||||
"domain": "Domain",
|
||||
"type": "Tipe",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "タグ",
|
||||
"tagDesc": "一意のタグ",
|
||||
"address": "アドレス",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "リバース",
|
||||
"domain": "ドメイン",
|
||||
"type": "タイプ",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "Tag",
|
||||
"tagDesc": "Tag Única",
|
||||
"address": "Endereço",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "Reverso",
|
||||
"domain": "Domínio",
|
||||
"type": "Tipo",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "Тег",
|
||||
"tagDesc": "Уникальный тег",
|
||||
"address": "Адрес",
|
||||
"egress": "Выход",
|
||||
"egressHint": "Запустите HTTP-тест, чтобы показать выходной IP и страну.",
|
||||
"reverse": "Реверс-прокси",
|
||||
"domain": "Домен",
|
||||
"type": "Тип",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "Etiket",
|
||||
"tagDesc": "Benzersiz Etiket",
|
||||
"address": "Adres",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "Ters",
|
||||
"domain": "Alan Adı",
|
||||
"type": "Tür",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "Тег",
|
||||
"tagDesc": "Унікальний тег",
|
||||
"address": "Адреса",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "Зворотний",
|
||||
"domain": "Домен",
|
||||
"type": "Тип",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "Tag",
|
||||
"tagDesc": "thẻ duy nhất",
|
||||
"address": "Địa chỉ",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "Đảo ngược",
|
||||
"domain": "Tên miền",
|
||||
"type": "Loại",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "标签",
|
||||
"tagDesc": "唯一标签",
|
||||
"address": "地址",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "反向",
|
||||
"domain": "域名",
|
||||
"type": "类型",
|
||||
|
||||
@@ -1622,6 +1622,8 @@
|
||||
"tag": "標籤",
|
||||
"tagDesc": "唯一標籤",
|
||||
"address": "地址",
|
||||
"egress": "Egress",
|
||||
"egressHint": "Run an HTTP test to show egress IP and country.",
|
||||
"reverse": "反向",
|
||||
"domain": "網域",
|
||||
"type": "類型",
|
||||
|
||||
@@ -97,7 +97,11 @@ func (g *schemaGen) typeSchema(t TypeRef) map[string]any {
|
||||
}
|
||||
return map[string]any{"type": "string"}
|
||||
case KindInt:
|
||||
return map[string]any{"type": "integer"}
|
||||
sch := map[string]any{"type": "integer"}
|
||||
if t.Name == "int64" {
|
||||
sch["format"] = "int64"
|
||||
}
|
||||
return sch
|
||||
case KindNumber:
|
||||
return map[string]any{"type": "number"}
|
||||
case KindBool:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIntegerSchemaFormats(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
goType string
|
||||
wantFormat string
|
||||
}{
|
||||
{name: "int", goType: "int"},
|
||||
{name: "int32", goType: "int32"},
|
||||
{name: "int64", goType: "int64", wantFormat: "int64"},
|
||||
{name: "uint64", goType: "uint64", wantFormat: "int64"},
|
||||
}
|
||||
|
||||
gen := &schemaGen{}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
schema := gen.typeSchema(identType(tt.goType))
|
||||
if got := schema["type"]; got != "integer" {
|
||||
t.Fatalf("type = %v, want integer", got)
|
||||
}
|
||||
got, _ := schema["format"].(string)
|
||||
if got != tt.wantFormat {
|
||||
t.Fatalf("format = %q, want %q", got, tt.wantFormat)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -197,8 +197,10 @@ func identType(name string) TypeRef {
|
||||
return TypeRef{Kind: KindString}
|
||||
case "bool":
|
||||
return TypeRef{Kind: KindBool}
|
||||
case "int", "int8", "int16", "int32", "int64",
|
||||
"uint", "uint8", "uint16", "uint32", "uint64":
|
||||
case "int64", "uint64":
|
||||
return TypeRef{Kind: KindInt, Name: "int64"}
|
||||
case "int", "int8", "int16", "int32",
|
||||
"uint", "uint8", "uint16", "uint32":
|
||||
return TypeRef{Kind: KindInt}
|
||||
case "float32", "float64":
|
||||
return TypeRef{Kind: KindNumber}
|
||||
|
||||
Reference in New Issue
Block a user