mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-26 10:43:09 +00:00
Compare commits
1 Commits
main
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
990f9640e5 |
@@ -1957,7 +1957,7 @@
|
|||||||
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
|
||||||
"sshSudo": "Allow sudo",
|
"sshSudo": "Allow sudo",
|
||||||
"sshSudoCommands": "Sudo Commands",
|
"sshSudoCommands": "Sudo Commands",
|
||||||
"sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo. Absolute paths must be used.",
|
"sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo.",
|
||||||
"sshCreateHomeDir": "Create Home Directory",
|
"sshCreateHomeDir": "Create Home Directory",
|
||||||
"sshUnixGroups": "Unix Groups",
|
"sshUnixGroups": "Unix Groups",
|
||||||
"sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.",
|
"sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.",
|
||||||
|
|||||||
71
package-lock.json
generated
71
package-lock.json
generated
@@ -1058,7 +1058,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -2354,7 +2353,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2377,7 +2375,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2400,7 +2397,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2417,7 +2413,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2434,7 +2429,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2451,7 +2445,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2468,7 +2461,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2485,7 +2477,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2502,7 +2493,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2519,7 +2509,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2536,7 +2525,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2553,7 +2541,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2576,7 +2563,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2599,7 +2585,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2622,7 +2607,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2645,7 +2629,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2668,7 +2651,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2691,7 +2673,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2714,7 +2695,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2734,7 +2714,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2754,7 +2733,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2774,7 +2752,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3034,7 +3011,6 @@
|
|||||||
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.21.3 || >=16"
|
"node": "^14.21.3 || >=16"
|
||||||
},
|
},
|
||||||
@@ -6981,7 +6957,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz",
|
||||||
"integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==",
|
"integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
@@ -8442,7 +8417,6 @@
|
|||||||
"version": "5.90.21",
|
"version": "5.90.21",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
|
||||||
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
|
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/query-core": "5.90.20"
|
"@tanstack/query-core": "5.90.20"
|
||||||
},
|
},
|
||||||
@@ -8558,7 +8532,6 @@
|
|||||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@@ -8906,7 +8879,6 @@
|
|||||||
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
"@types/express-serve-static-core": "^5.0.0",
|
"@types/express-serve-static-core": "^5.0.0",
|
||||||
@@ -9002,7 +8974,6 @@
|
|||||||
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
|
"integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.18.0"
|
||||||
}
|
}
|
||||||
@@ -9030,7 +9001,6 @@
|
|||||||
"integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
|
"integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"pg-protocol": "*",
|
"pg-protocol": "*",
|
||||||
@@ -9056,7 +9026,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -9067,7 +9036,6 @@
|
|||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -9154,7 +9122,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
@@ -9228,7 +9197,6 @@
|
|||||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.1",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.56.1",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
@@ -9702,7 +9670,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -10152,7 +10119,6 @@
|
|||||||
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.0"
|
"@babel/types": "^7.26.0"
|
||||||
}
|
}
|
||||||
@@ -10224,7 +10190,6 @@
|
|||||||
"integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==",
|
"integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bindings": "^1.5.0",
|
"bindings": "^1.5.0",
|
||||||
"prebuild-install": "^7.1.1"
|
"prebuild-install": "^7.1.1"
|
||||||
@@ -10310,9 +10275,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
@@ -10353,7 +10318,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -11260,7 +11224,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -11701,6 +11664,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
||||||
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
@@ -12335,7 +12299,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -12421,7 +12384,6 @@
|
|||||||
"integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==",
|
"integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
@@ -12507,9 +12469,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint-config-next/node_modules/brace-expansion": {
|
"node_modules/eslint-config-next/node_modules/brace-expansion": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -12558,7 +12520,6 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -12952,7 +12913,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.1",
|
"body-parser": "^2.2.1",
|
||||||
@@ -15370,6 +15330,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "3.2.7",
|
"dompurify": "3.2.7",
|
||||||
"marked": "14.0.0"
|
"marked": "14.0.0"
|
||||||
@@ -15380,6 +15341,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
|
||||||
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
@@ -15468,7 +15430,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz",
|
||||||
"integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==",
|
"integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "15.5.15",
|
"@next/env": "15.5.15",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
@@ -16428,7 +16389,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.12.0",
|
"pg-connection-string": "^2.12.0",
|
||||||
"pg-pool": "^3.13.0",
|
"pg-pool": "^3.13.0",
|
||||||
@@ -16936,7 +16896,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -16968,7 +16927,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -17261,7 +17219,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz",
|
||||||
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
|
"integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -18723,8 +18680,7 @@
|
|||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
||||||
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
@@ -19199,7 +19155,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -19627,7 +19582,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||||
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@colors/colors": "^1.6.0",
|
"@colors/colors": "^1.6.0",
|
||||||
"@dabh/diagnostics": "^2.0.8",
|
"@dabh/diagnostics": "^2.0.8",
|
||||||
@@ -19834,7 +19788,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,18 +221,10 @@ async function handleResource(
|
|||||||
)
|
)
|
||||||
.where(eq(targets.resourceId, resource.resourceId));
|
.where(eq(targets.resourceId, resource.resourceId));
|
||||||
|
|
||||||
const monitoredTargets = otherTargets.filter(
|
|
||||||
(t) => t.hcHealth !== "unknown"
|
|
||||||
);
|
|
||||||
|
|
||||||
let health = "healthy";
|
let health = "healthy";
|
||||||
const allUnknown = monitoredTargets.length === 0;
|
const allUnknown = otherTargets.every((t) => t.hcHealth === "unknown");
|
||||||
const allHealthy = monitoredTargets.every(
|
const allHealthy = otherTargets.every((t) => t.hcHealth === "healthy");
|
||||||
(t) => t.hcHealth === "healthy"
|
const allUnhealthy = otherTargets.every((t) => t.hcHealth === "unhealthy");
|
||||||
);
|
|
||||||
const allUnhealthy = monitoredTargets.every(
|
|
||||||
(t) => t.hcHealth === "unhealthy"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allUnknown) {
|
if (allUnknown) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const RuleSchema = z
|
|||||||
.object({
|
.object({
|
||||||
action: z.enum(["allow", "deny", "pass"]),
|
action: z.enum(["allow", "deny", "pass"]),
|
||||||
match: z.enum(["cidr", "path", "ip", "country", "asn", "region"]),
|
match: z.enum(["cidr", "path", "ip", "country", "asn", "region"]),
|
||||||
value: z.coerce.string(),
|
value: z.string(),
|
||||||
priority: z.int().optional()
|
priority: z.int().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
@@ -340,8 +340,7 @@ export const ResourceSchema = z
|
|||||||
if (parts.includes("*", 1)) return false; // no further wildcards
|
if (parts.includes("*", 1)) return false; // no further wildcards
|
||||||
if (parts.length < 3) return false; // need at least *.label.tld
|
if (parts.length < 3) return false; // need at least *.label.tld
|
||||||
|
|
||||||
const labelRegex =
|
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
||||||
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
|
|
||||||
return parts.slice(1).every((label) => labelRegex.test(label));
|
return parts.slice(1).every((label) => labelRegex.test(label));
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
userOrgRoles,
|
userOrgRoles,
|
||||||
userSiteResources
|
userSiteResources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, count, eq, inArray, ne } from "drizzle-orm";
|
import { and, eq, inArray, ne } from "drizzle-orm";
|
||||||
|
|
||||||
import { deletePeer as newtDeletePeer } from "@server/routers/newt/peers";
|
import { deletePeer as newtDeletePeer } from "@server/routers/newt/peers";
|
||||||
import {
|
import {
|
||||||
@@ -39,11 +39,6 @@ import {
|
|||||||
removePeerData,
|
removePeerData,
|
||||||
removeTargets as removeSubnetProxyTargets
|
removeTargets as removeSubnetProxyTargets
|
||||||
} from "@server/routers/client/targets";
|
} from "@server/routers/client/targets";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
|
||||||
|
|
||||||
// TTL for rebuild-association locks. These functions can fan out into many
|
|
||||||
// peer/proxy updates, so give them a generous window.
|
|
||||||
const REBUILD_ASSOCIATIONS_LOCK_TTL_MS = 120000;
|
|
||||||
|
|
||||||
export async function getClientSiteResourceAccess(
|
export async function getClientSiteResourceAccess(
|
||||||
siteResource: SiteResource,
|
siteResource: SiteResource,
|
||||||
@@ -166,23 +161,6 @@ export async function rebuildClientAssociationsFromSiteResource(
|
|||||||
pubKey: string | null;
|
pubKey: string | null;
|
||||||
subnet: string | null;
|
subnet: string | null;
|
||||||
}[];
|
}[];
|
||||||
}> {
|
|
||||||
return await lockManager.withLock(
|
|
||||||
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
|
||||||
() => rebuildClientAssociationsFromSiteResourceImpl(siteResource, trx),
|
|
||||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rebuildClientAssociationsFromSiteResourceImpl(
|
|
||||||
siteResource: SiteResource,
|
|
||||||
trx: Transaction | typeof db = db
|
|
||||||
): Promise<{
|
|
||||||
mergedAllClients: {
|
|
||||||
clientId: number;
|
|
||||||
pubKey: string | null;
|
|
||||||
subnet: string | null;
|
|
||||||
}[];
|
|
||||||
}> {
|
}> {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] START siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} orgId=${siteResource.orgId}`
|
`rebuildClientAssociations: [rebuildClientAssociationsFromSiteResource] START siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} orgId=${siteResource.orgId}`
|
||||||
@@ -561,29 +539,6 @@ async function handleMessagesForSiteClients(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the number of sites on each of these clients so we can log it and make decisions about whether to send messages based on it
|
|
||||||
const clientSiteCounts: Record<number, number> = {};
|
|
||||||
if (clientsToProcess.size > 0) {
|
|
||||||
const clientIdsToProcess = Array.from(clientsToProcess.keys());
|
|
||||||
const siteCounts = await trx
|
|
||||||
.select({
|
|
||||||
clientId: clientSitesAssociationsCache.clientId,
|
|
||||||
siteCount: count(clientSitesAssociationsCache.siteId)
|
|
||||||
})
|
|
||||||
.from(clientSitesAssociationsCache)
|
|
||||||
.where(
|
|
||||||
inArray(
|
|
||||||
clientSitesAssociationsCache.clientId,
|
|
||||||
clientIdsToProcess
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.groupBy(clientSitesAssociationsCache.clientId);
|
|
||||||
|
|
||||||
for (const row of siteCounts) {
|
|
||||||
clientSiteCounts[row.clientId] = Number(row.siteCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const client of clientsToProcess.values()) {
|
for (const client of clientsToProcess.values()) {
|
||||||
// UPDATE THE NEWT
|
// UPDATE THE NEWT
|
||||||
if (!client.subnet || !client.pubKey) {
|
if (!client.subnet || !client.pubKey) {
|
||||||
@@ -627,14 +582,7 @@ async function handleMessagesForSiteClients(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isAdd) {
|
if (isAdd) {
|
||||||
if (clientSiteCounts[client.clientId] > 250) {
|
// TODO: if we are in jit mode here should we really be sending this?
|
||||||
// skip adding the peer if we have more than 250 sites because we are in jit mode anyway
|
|
||||||
logger.info(
|
|
||||||
`rebuildClientAssociations: Client ${client.clientId} has ${clientSiteCounts[client.clientId]} sites so skipping adding peer to newt and olm because it is likely in jit mode`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await initPeerAddHandshake(
|
await initPeerAddHandshake(
|
||||||
// this will kick off the add peer process for the client
|
// this will kick off the add peer process for the client
|
||||||
client.clientId,
|
client.clientId,
|
||||||
@@ -652,24 +600,9 @@ async function handleMessagesForSiteClients(
|
|||||||
exitNodeJobs.push(updateClientSiteDestinations(client, trx));
|
exitNodeJobs.push(updateClientSiteDestinations(client, trx));
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(exitNodeJobs).catch((error) => {
|
await Promise.all(exitNodeJobs);
|
||||||
logger.error(
|
await Promise.all(newtJobs); // do the servers first to make sure they are ready?
|
||||||
`rebuildClientAssociations: Error updating client site destinations for site ${site.siteId}:`,
|
await Promise.all(olmJobs);
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Promise.all(newtJobs).catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
`rebuildClientAssociations: Error updating Newt peers for site ${site.siteId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Promise.all(olmJobs).catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
`rebuildClientAssociations: Error updating Olm peers for site ${site.siteId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PeerDestination {
|
interface PeerDestination {
|
||||||
@@ -952,17 +885,6 @@ async function handleSubnetProxyTargetUpdates(
|
|||||||
export async function rebuildClientAssociationsFromClient(
|
export async function rebuildClientAssociationsFromClient(
|
||||||
client: Client,
|
client: Client,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<void> {
|
|
||||||
return await lockManager.withLock(
|
|
||||||
`rebuild-client-associations:client:${client.clientId}`,
|
|
||||||
() => rebuildClientAssociationsFromClientImpl(client, trx),
|
|
||||||
REBUILD_ASSOCIATIONS_LOCK_TTL_MS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rebuildClientAssociationsFromClientImpl(
|
|
||||||
client: Client,
|
|
||||||
trx: Transaction | typeof db = db
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let newSiteResourceIds: number[] = [];
|
let newSiteResourceIds: number[] = [];
|
||||||
|
|
||||||
@@ -1235,12 +1157,6 @@ async function handleMessagesForClientSites(
|
|||||||
const olmJobs: Promise<any>[] = [];
|
const olmJobs: Promise<any>[] = [];
|
||||||
const exitNodeJobs: Promise<any>[] = [];
|
const exitNodeJobs: Promise<any>[] = [];
|
||||||
|
|
||||||
const totalSitesOnClient = await trx
|
|
||||||
.select({ count: count(clientSitesAssociationsCache.siteId) })
|
|
||||||
.from(clientSitesAssociationsCache)
|
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId))
|
|
||||||
.then((rows) => Number(rows[0].count));
|
|
||||||
|
|
||||||
for (const siteData of sitesData) {
|
for (const siteData of sitesData) {
|
||||||
const site = siteData.sites;
|
const site = siteData.sites;
|
||||||
const exitNode = siteData.exitNodes;
|
const exitNode = siteData.exitNodes;
|
||||||
@@ -1301,14 +1217,7 @@ async function handleMessagesForClientSites(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalSitesOnClient > 250) {
|
// TODO: if we are in jit mode here should we really be sending this?
|
||||||
// skip adding the site if we have more than 250 because we are in jit mode anyway
|
|
||||||
logger.info(
|
|
||||||
`rebuildClientAssociations: Client ${client.clientId} has ${totalSitesOnClient} sites so skipping adding peer to newt and olm because it is likely in jit mode`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await initPeerAddHandshake(
|
await initPeerAddHandshake(
|
||||||
// this will kick off the add peer process for the client
|
// this will kick off the add peer process for the client
|
||||||
client.clientId,
|
client.clientId,
|
||||||
@@ -1336,24 +1245,9 @@ async function handleMessagesForClientSites(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(exitNodeJobs).catch((error) => {
|
await Promise.all(exitNodeJobs);
|
||||||
logger.error(
|
await Promise.all(newtJobs);
|
||||||
`rebuildClientAssociations: Error updating client site destinations for client ${client.clientId}:`,
|
await Promise.all(olmJobs);
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Promise.all(newtJobs).catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
`rebuildClientAssociations: Error updating Newt peers for client ${client.clientId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Promise.all(olmJobs).catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
`rebuildClientAssociations: Error updating Olm peers for client ${client.clientId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessagesForClientResources(
|
async function handleMessagesForClientResources(
|
||||||
@@ -1634,269 +1528,3 @@ async function handleMessagesForClientResources(
|
|||||||
|
|
||||||
await Promise.all([...proxyJobs, ...olmJobs]);
|
await Promise.all([...proxyJobs, ...olmJobs]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClientAssociationsCacheVerification = {
|
|
||||||
clientId: number;
|
|
||||||
consistent: boolean;
|
|
||||||
// What permissions say the cache should contain
|
|
||||||
expectedSiteResourceIds: number[];
|
|
||||||
expectedSiteIds: number[];
|
|
||||||
// What the cache currently contains
|
|
||||||
actualSiteResourceIds: number[];
|
|
||||||
actualSiteIds: number[];
|
|
||||||
// Diff
|
|
||||||
missingSiteResourceIds: number[]; // present in expected, missing from cache
|
|
||||||
extraSiteResourceIds: number[]; // present in cache, not in expected
|
|
||||||
missingSiteIds: number[];
|
|
||||||
extraSiteIds: number[];
|
|
||||||
};
|
|
||||||
|
|
||||||
// verifyClientAssociationsCache walks the same permission-derivation logic as
|
|
||||||
// rebuildClientAssociationsFromClient but does NOT modify the database. It
|
|
||||||
// returns the expected vs actual cache contents and a boolean indicating
|
|
||||||
// whether the cache is in sync with what permissions imply.
|
|
||||||
export async function verifyClientAssociationsCache(
|
|
||||||
client: Client,
|
|
||||||
trx: Transaction | typeof db = db
|
|
||||||
): Promise<ClientAssociationsCacheVerification> {
|
|
||||||
let newSiteResourceIds: number[] = [];
|
|
||||||
|
|
||||||
// 1. Direct client associations
|
|
||||||
const directSiteResources = await trx
|
|
||||||
.select({ siteResourceId: clientSiteResources.siteResourceId })
|
|
||||||
.from(clientSiteResources)
|
|
||||||
.innerJoin(
|
|
||||||
siteResources,
|
|
||||||
eq(siteResources.siteResourceId, clientSiteResources.siteResourceId)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(clientSiteResources.clientId, client.clientId),
|
|
||||||
eq(siteResources.orgId, client.orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
newSiteResourceIds.push(
|
|
||||||
...directSiteResources.map((r) => r.siteResourceId)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. User-based and role-based access (if client has a userId)
|
|
||||||
if (client.userId) {
|
|
||||||
const userSiteResourceIds = await trx
|
|
||||||
.select({ siteResourceId: userSiteResources.siteResourceId })
|
|
||||||
.from(userSiteResources)
|
|
||||||
.innerJoin(
|
|
||||||
siteResources,
|
|
||||||
eq(
|
|
||||||
siteResources.siteResourceId,
|
|
||||||
userSiteResources.siteResourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userSiteResources.userId, client.userId),
|
|
||||||
eq(siteResources.orgId, client.orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
newSiteResourceIds.push(
|
|
||||||
...userSiteResourceIds.map((r) => r.siteResourceId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const roleIds = await trx
|
|
||||||
.select({ roleId: userOrgRoles.roleId })
|
|
||||||
.from(userOrgRoles)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userOrgRoles.userId, client.userId),
|
|
||||||
eq(userOrgRoles.orgId, client.orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then((rows) => rows.map((row) => row.roleId));
|
|
||||||
|
|
||||||
if (roleIds.length > 0) {
|
|
||||||
const roleSiteResourceIds = await trx
|
|
||||||
.select({ siteResourceId: roleSiteResources.siteResourceId })
|
|
||||||
.from(roleSiteResources)
|
|
||||||
.innerJoin(
|
|
||||||
siteResources,
|
|
||||||
eq(
|
|
||||||
siteResources.siteResourceId,
|
|
||||||
roleSiteResources.siteResourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
inArray(roleSiteResources.roleId, roleIds),
|
|
||||||
eq(siteResources.orgId, client.orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
newSiteResourceIds.push(
|
|
||||||
...roleSiteResourceIds.map((r) => r.siteResourceId)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newSiteResourceIds = Array.from(new Set(newSiteResourceIds));
|
|
||||||
|
|
||||||
const newSiteResources =
|
|
||||||
newSiteResourceIds.length > 0
|
|
||||||
? await trx
|
|
||||||
.select()
|
|
||||||
.from(siteResources)
|
|
||||||
.where(
|
|
||||||
inArray(siteResources.siteResourceId, newSiteResourceIds)
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const networkIds = Array.from(
|
|
||||||
new Set(
|
|
||||||
newSiteResources
|
|
||||||
.map((sr) => sr.networkId)
|
|
||||||
.filter((id): id is number => id !== null)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const newSiteIds =
|
|
||||||
networkIds.length > 0
|
|
||||||
? await trx
|
|
||||||
.select({ siteId: siteNetworks.siteId })
|
|
||||||
.from(siteNetworks)
|
|
||||||
.where(inArray(siteNetworks.networkId, networkIds))
|
|
||||||
.then((rows) =>
|
|
||||||
Array.from(new Set(rows.map((r) => r.siteId)))
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Read the existing cache state
|
|
||||||
const existingResourceAssociations = await trx
|
|
||||||
.select({
|
|
||||||
siteResourceId: clientSiteResourcesAssociationsCache.siteResourceId
|
|
||||||
})
|
|
||||||
.from(clientSiteResourcesAssociationsCache)
|
|
||||||
.where(
|
|
||||||
eq(clientSiteResourcesAssociationsCache.clientId, client.clientId)
|
|
||||||
);
|
|
||||||
const existingSiteResourceIds = existingResourceAssociations.map(
|
|
||||||
(r) => r.siteResourceId
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingSiteAssociations = await trx
|
|
||||||
.select({ siteId: clientSitesAssociationsCache.siteId })
|
|
||||||
.from(clientSitesAssociationsCache)
|
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
|
||||||
const existingSiteIds = existingSiteAssociations.map((s) => s.siteId);
|
|
||||||
|
|
||||||
const expectedSiteResourceSet = new Set(newSiteResourceIds);
|
|
||||||
const actualSiteResourceSet = new Set(existingSiteResourceIds);
|
|
||||||
const expectedSiteSet = new Set(newSiteIds);
|
|
||||||
const actualSiteSet = new Set(existingSiteIds);
|
|
||||||
|
|
||||||
const missingSiteResourceIds = newSiteResourceIds.filter(
|
|
||||||
(id) => !actualSiteResourceSet.has(id)
|
|
||||||
);
|
|
||||||
const extraSiteResourceIds = existingSiteResourceIds.filter(
|
|
||||||
(id) => !expectedSiteResourceSet.has(id)
|
|
||||||
);
|
|
||||||
const missingSiteIds = newSiteIds.filter((id) => !actualSiteSet.has(id));
|
|
||||||
const extraSiteIds = existingSiteIds.filter(
|
|
||||||
(id) => !expectedSiteSet.has(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const consistent =
|
|
||||||
missingSiteResourceIds.length === 0 &&
|
|
||||||
extraSiteResourceIds.length === 0 &&
|
|
||||||
missingSiteIds.length === 0 &&
|
|
||||||
extraSiteIds.length === 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
clientId: client.clientId,
|
|
||||||
consistent,
|
|
||||||
expectedSiteResourceIds: Array.from(expectedSiteResourceSet).sort(
|
|
||||||
(a, b) => a - b
|
|
||||||
),
|
|
||||||
expectedSiteIds: Array.from(expectedSiteSet).sort((a, b) => a - b),
|
|
||||||
actualSiteResourceIds: Array.from(actualSiteResourceSet).sort(
|
|
||||||
(a, b) => a - b
|
|
||||||
),
|
|
||||||
actualSiteIds: Array.from(actualSiteSet).sort((a, b) => a - b),
|
|
||||||
missingSiteResourceIds: missingSiteResourceIds.sort((a, b) => a - b),
|
|
||||||
extraSiteResourceIds: extraSiteResourceIds.sort((a, b) => a - b),
|
|
||||||
missingSiteIds: missingSiteIds.sort((a, b) => a - b),
|
|
||||||
extraSiteIds: extraSiteIds.sort((a, b) => a - b)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupSiteAssociations efficiently removes all client associations for a
|
|
||||||
// site that is being deleted. Instead of calling
|
|
||||||
// rebuildClientAssociationsFromSiteResource once per site resource (which is
|
|
||||||
// O(resources) in DB round-trips and message fan-out), this function performs
|
|
||||||
// a single bulk lookup of affected clients and site resources, deletes all
|
|
||||||
// cache rows at once, and fires all peer/proxy removal messages in parallel.
|
|
||||||
//
|
|
||||||
// The caller is responsible for deleting the site row itself (and for sending
|
|
||||||
// the newt/wg/terminate signal to the newt process).
|
|
||||||
export async function cleanupSiteAssociations(
|
|
||||||
site: Site,
|
|
||||||
trx: Transaction | typeof db = db
|
|
||||||
): Promise<void> {
|
|
||||||
const siteId = site.siteId;
|
|
||||||
|
|
||||||
logger.debug(`cleanupSiteAssociations: START siteId=${siteId}`);
|
|
||||||
|
|
||||||
// 1. Find every client currently cached against this site.
|
|
||||||
const cachedSiteClientRows = await trx
|
|
||||||
.select({ clientId: clientSitesAssociationsCache.clientId })
|
|
||||||
.from(clientSitesAssociationsCache)
|
|
||||||
.where(eq(clientSitesAssociationsCache.siteId, siteId));
|
|
||||||
|
|
||||||
const cachedClientIds = cachedSiteClientRows.map((r) => r.clientId);
|
|
||||||
|
|
||||||
// 2. Load full client details (needed for WireGuard public-key references).
|
|
||||||
const allClients =
|
|
||||||
cachedClientIds.length > 0
|
|
||||||
? await trx
|
|
||||||
.select({
|
|
||||||
clientId: clients.clientId,
|
|
||||||
pubKey: clients.pubKey,
|
|
||||||
subnet: clients.subnet
|
|
||||||
})
|
|
||||||
.from(clients)
|
|
||||||
.where(inArray(clients.clientId, cachedClientIds))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// 6. Bulk-delete all cache entries for this site. Do this before sending
|
|
||||||
// destination-update messages so updateClientSiteDestinations computes
|
|
||||||
// the correct (post-deletion) set of destinations.
|
|
||||||
await trx
|
|
||||||
.delete(clientSitesAssociationsCache)
|
|
||||||
.where(eq(clientSitesAssociationsCache.siteId, siteId));
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`cleanupSiteAssociations: siteId=${siteId} cache cleared. clients=${allClients.length}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// 7. Fire all removal messages in parallel.
|
|
||||||
const jobs: Promise<any>[] = [];
|
|
||||||
|
|
||||||
for (const client of allClients) {
|
|
||||||
// Tell each olm to drop the site's WireGuard peer.
|
|
||||||
if (site.publicKey) {
|
|
||||||
jobs.push(olmDeletePeer(client.clientId, siteId, site.publicKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recompute and push updated relay destinations (now excluding this site).
|
|
||||||
if (client.pubKey && client.subnet) {
|
|
||||||
jobs.push(updateClientSiteDestinations(client, trx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(jobs).catch((error) => {
|
|
||||||
logger.error(
|
|
||||||
`cleanupSiteAssociations: error sending cleanup messages for siteId=${siteId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug(`cleanupSiteAssociations: DONE siteId=${siteId}`);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
|
|||||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||||
import * as alertRule from "#private/routers/alertRule";
|
import * as alertRule from "#private/routers/alertRule";
|
||||||
import * as healthChecks from "#private/routers/healthChecks";
|
import * as healthChecks from "#private/routers/healthChecks";
|
||||||
import * as client from "@server/routers/client";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
@@ -776,15 +775,3 @@ authenticated.get(
|
|||||||
verifyUserHasAction(ActionsEnum.getTarget),
|
verifyUserHasAction(ActionsEnum.getTarget),
|
||||||
healthChecks.getHealthCheckStatusHistory
|
healthChecks.getHealthCheckStatusHistory
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/client/:clientId/verify-associations-cache",
|
|
||||||
verifyClientAccess,
|
|
||||||
client.verifyClientAssociationsCache
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/client/:clientId/rebuild-associations-cache",
|
|
||||||
verifyClientAccess,
|
|
||||||
client.rebuildClientAssociationsCacheRoute
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, InferInsertModel } from "drizzle-orm";
|
import { eq, InferInsertModel } from "drizzle-orm";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { validateLocalPath } from "@app/lib/validateLocalPath";
|
||||||
import config from "#private/lib/config";
|
import config from "#private/lib/config";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
@@ -34,9 +35,78 @@ const paramsSchema = z.strictObject({
|
|||||||
|
|
||||||
const bodySchema = z.strictObject({
|
const bodySchema = z.strictObject({
|
||||||
logoUrl: z
|
logoUrl: z
|
||||||
.string()
|
.union([
|
||||||
.optional()
|
z.literal(""),
|
||||||
.transform((val) => (val === "" ? null : val)),
|
z
|
||||||
|
.string()
|
||||||
|
.superRefine(async (urlOrPath, ctx) => {
|
||||||
|
const parseResult = z.url().safeParse(urlOrPath);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
if (build !== "enterprise") {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "Must be a valid URL"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
validateLocalPath(urlOrPath);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "Must be either a valid image URL or a valid pathname starting with `/` and not containing query parameters, `..` or `*`"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(urlOrPath, {
|
||||||
|
method: "HEAD"
|
||||||
|
}).catch(() => {
|
||||||
|
// If HEAD fails (CORS or method not allowed), try GET
|
||||||
|
return fetch(urlOrPath, { method: "GET" });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: `Failed to load image. Please check that the URL is accessible.`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType =
|
||||||
|
response.headers.get("content-type") ?? "";
|
||||||
|
if (!contentType.startsWith("image/")) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: `URL does not point to an image. Please provide a URL to an image file (e.g., .png, .jpg, .svg).`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
let errorMessage =
|
||||||
|
"Unable to verify image URL. Please check that the URL is accessible and points to an image file.";
|
||||||
|
|
||||||
|
if (error instanceof TypeError && error.message.includes("fetch")) {
|
||||||
|
errorMessage =
|
||||||
|
"Network error: Unable to reach the URL. Please check your internet connection and verify the URL is correct.";
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
errorMessage = `Error verifying URL: ${error.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.transform((val) => (val === "" ? null : val))
|
||||||
|
.nullish(),
|
||||||
logoWidth: z.coerce.number<number>().min(1),
|
logoWidth: z.coerce.number<number>().min(1),
|
||||||
logoHeight: z.coerce.number<number>().min(1),
|
logoHeight: z.coerce.number<number>().min(1),
|
||||||
resourceTitle: z.string(),
|
resourceTitle: z.string(),
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
logsDb,
|
logsDb,
|
||||||
newts,
|
newts,
|
||||||
roles,
|
roles,
|
||||||
roleSiteResources,
|
|
||||||
roundTripMessageTracker,
|
roundTripMessageTracker,
|
||||||
siteResources,
|
siteResources,
|
||||||
siteNetworks,
|
siteNetworks,
|
||||||
@@ -362,26 +361,9 @@ export async function signSshKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const roleRows = await db
|
const roleRows = await db
|
||||||
.select({
|
.select()
|
||||||
sshSudoCommands: roles.sshSudoCommands,
|
|
||||||
sshUnixGroups: roles.sshUnixGroups,
|
|
||||||
sshCreateHomeDir: roles.sshCreateHomeDir,
|
|
||||||
sshSudoMode: roles.sshSudoMode
|
|
||||||
})
|
|
||||||
.from(roles)
|
.from(roles)
|
||||||
.innerJoin(
|
.where(inArray(roles.roleId, roleIds));
|
||||||
roleSiteResources,
|
|
||||||
eq(roleSiteResources.roleId, roles.roleId)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
inArray(roles.roleId, roleIds),
|
|
||||||
eq(
|
|
||||||
roleSiteResources.siteResourceId,
|
|
||||||
resource.siteResourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const parsedSudoCommands: string[] = [];
|
const parsedSudoCommands: string[] = [];
|
||||||
const parsedGroupsSet = new Set<string>();
|
const parsedGroupsSet = new Set<string>();
|
||||||
@@ -397,17 +379,13 @@ export async function signSshKey(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const grps = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
|
const grps = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
|
||||||
if (Array.isArray(grps))
|
if (Array.isArray(grps)) grps.forEach((g: string) => parsedGroupsSet.add(g));
|
||||||
grps.forEach((g: string) => parsedGroupsSet.add(g));
|
|
||||||
} catch {
|
} catch {
|
||||||
// skip
|
// skip
|
||||||
}
|
}
|
||||||
if (roleRow?.sshCreateHomeDir === true) homedir = true;
|
if (roleRow?.sshCreateHomeDir === true) homedir = true;
|
||||||
const m = roleRow?.sshSudoMode ?? "none";
|
const m = roleRow?.sshSudoMode ?? "none";
|
||||||
if (
|
if (sudoModeOrder[m as keyof typeof sudoModeOrder] > sudoModeOrder[sudoMode]) {
|
||||||
sudoModeOrder[m as keyof typeof sudoModeOrder] >
|
|
||||||
sudoModeOrder[sudoMode]
|
|
||||||
) {
|
|
||||||
sudoMode = m as "none" | "commands" | "full";
|
sudoMode = m as "none" | "commands" | "full";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { logsDb, requestAuditLog, driver } from "@server/db";
|
import { logsDb, requestAuditLog, driver, primaryLogsDb } from "@server/db";
|
||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
import { NextFunction } from "express";
|
import { NextFunction } from "express";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
@@ -74,12 +74,12 @@ async function query(query: Q) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [all] = await logsDb
|
const [all] = await primaryLogsDb
|
||||||
.select({ total: count() })
|
.select({ total: count() })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions);
|
.where(baseConditions);
|
||||||
|
|
||||||
const [blocked] = await logsDb
|
const [blocked] = await primaryLogsDb
|
||||||
.select({ total: count() })
|
.select({ total: count() })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(and(baseConditions, eq(requestAuditLog.action, false)));
|
.where(and(baseConditions, eq(requestAuditLog.action, false)));
|
||||||
@@ -90,7 +90,7 @@ async function query(query: Q) {
|
|||||||
|
|
||||||
const DISTINCT_LIMIT = 500;
|
const DISTINCT_LIMIT = 500;
|
||||||
|
|
||||||
const requestsPerCountry = await logsDb
|
const requestsPerCountry = await primaryLogsDb
|
||||||
.selectDistinct({
|
.selectDistinct({
|
||||||
code: requestAuditLog.location,
|
code: requestAuditLog.location,
|
||||||
count: totalQ
|
count: totalQ
|
||||||
@@ -118,7 +118,7 @@ async function query(query: Q) {
|
|||||||
const booleanTrue = driver === "pg" ? sql`true` : sql`1`;
|
const booleanTrue = driver === "pg" ? sql`true` : sql`1`;
|
||||||
const booleanFalse = driver === "pg" ? sql`false` : sql`0`;
|
const booleanFalse = driver === "pg" ? sql`false` : sql`0`;
|
||||||
|
|
||||||
const requestsPerDay = await logsDb
|
const requestsPerDay = await primaryLogsDb
|
||||||
.select({
|
.select({
|
||||||
day: groupByDayFunction.as("day"),
|
day: groupByDayFunction.as("day"),
|
||||||
allowedCount:
|
allowedCount:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { logsDb, requestAuditLog, resources, siteResources, db, primaryDb } from "@server/db";
|
import { logsDb, primaryLogsDb, requestAuditLog, resources, siteResources, db, primaryDb } from "@server/db";
|
||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
import { NextFunction } from "express";
|
import { NextFunction } from "express";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
@@ -110,7 +110,7 @@ function getWhere(data: Q) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function queryRequest(data: Q) {
|
export function queryRequest(data: Q) {
|
||||||
return logsDb
|
return primaryLogsDb
|
||||||
.select({
|
.select({
|
||||||
id: requestAuditLog.id,
|
id: requestAuditLog.id,
|
||||||
timestamp: requestAuditLog.timestamp,
|
timestamp: requestAuditLog.timestamp,
|
||||||
@@ -211,7 +211,7 @@ async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function countRequestQuery(data: Q) {
|
export function countRequestQuery(data: Q) {
|
||||||
const countQuery = logsDb
|
const countQuery = primaryLogsDb
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(getWhere(data));
|
.where(getWhere(data));
|
||||||
@@ -254,34 +254,34 @@ async function queryUniqueFilterAttributes(
|
|||||||
uniqueResources,
|
uniqueResources,
|
||||||
uniqueSiteResources
|
uniqueSiteResources
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
logsDb
|
primaryLogsDb
|
||||||
.selectDistinct({ actor: requestAuditLog.actor })
|
.selectDistinct({ actor: requestAuditLog.actor })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT + 1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
logsDb
|
primaryLogsDb
|
||||||
.selectDistinct({ locations: requestAuditLog.location })
|
.selectDistinct({ locations: requestAuditLog.location })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT + 1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
logsDb
|
primaryLogsDb
|
||||||
.selectDistinct({ hosts: requestAuditLog.host })
|
.selectDistinct({ hosts: requestAuditLog.host })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT + 1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
logsDb
|
primaryLogsDb
|
||||||
.selectDistinct({ paths: requestAuditLog.path })
|
.selectDistinct({ paths: requestAuditLog.path })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT + 1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
logsDb
|
primaryLogsDb
|
||||||
.selectDistinct({
|
.selectDistinct({
|
||||||
id: requestAuditLog.resourceId
|
id: requestAuditLog.resourceId
|
||||||
})
|
})
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.limit(DISTINCT_LIMIT + 1),
|
.limit(DISTINCT_LIMIT + 1),
|
||||||
logsDb
|
primaryLogsDb
|
||||||
.selectDistinct({
|
.selectDistinct({
|
||||||
id: requestAuditLog.siteResourceId
|
id: requestAuditLog.siteResourceId
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,5 +10,3 @@ export * from "./listUserDevices";
|
|||||||
export * from "./updateClient";
|
export * from "./updateClient";
|
||||||
export * from "./getClient";
|
export * from "./getClient";
|
||||||
export * from "./createUserClient";
|
export * from "./createUserClient";
|
||||||
export * from "./verifyClientAssociationsCache";
|
|
||||||
export * from "./rebuildClientAssociationsCacheRoute";
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { clients } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
|
||||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
|
||||||
});
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "post",
|
|
||||||
path: "/client/{clientId}/rebuild-associations-cache",
|
|
||||||
description:
|
|
||||||
"Rebuild the client's site/site-resource association cache based on current permissions.",
|
|
||||||
tags: [OpenAPITags.Client],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function rebuildClientAssociationsCacheRoute(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { clientId } = parsedParams.data;
|
|
||||||
|
|
||||||
const [client] = await db
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.clientId, clientId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Client with ID ${clientId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await rebuildClientAssociationsFromClient(client);
|
|
||||||
|
|
||||||
return response(res, {
|
|
||||||
data: null,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "Client association cache rebuilt successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to rebuild client association cache"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { clients } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
import { verifyClientAssociationsCache as verifyClientAssociationsCacheLib } from "@server/lib/rebuildClientAssociations";
|
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
|
||||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
|
||||||
});
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "get",
|
|
||||||
path: "/client/{clientId}/verify-associations-cache",
|
|
||||||
description:
|
|
||||||
"Read-only check of whether the client's site/site-resource association cache matches what the current permissions imply.",
|
|
||||||
tags: [OpenAPITags.Client],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function verifyClientAssociationsCache(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { clientId } = parsedParams.data;
|
|
||||||
|
|
||||||
const [client] = await db
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.clientId, clientId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!client) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Client with ID ${clientId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const report = await verifyClientAssociationsCacheLib(client);
|
|
||||||
|
|
||||||
return response(res, {
|
|
||||||
data: report,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: report.consistent
|
|
||||||
? "Client association cache is consistent"
|
|
||||||
: "Client association cache is INCONSISTENT",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to verify client association cache"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, Site, siteNetworks, siteResources } from "@server/db";
|
||||||
import { newts, sites } from "@server/db";
|
import { newts, newtSessions, sites } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -11,7 +11,7 @@ import { deletePeer } from "../gerbil/peers";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { cleanupSiteAssociations } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
import { usageService } from "@server/lib/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
import { FeatureId } from "@server/lib/billing";
|
import { FeatureId } from "@server/lib/billing";
|
||||||
|
|
||||||
@@ -63,11 +63,7 @@ export async function deleteSite(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [deletedNewt] = await db
|
let deletedNewtId: string | null = null;
|
||||||
.select()
|
|
||||||
.from(newts)
|
|
||||||
.where(eq(newts.siteId, siteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
if (site.type == "wireguard") {
|
if (site.type == "wireguard") {
|
||||||
@@ -75,24 +71,56 @@ export async function deleteSite(
|
|||||||
await deletePeer(site.exitNodeId!, site.pubKey);
|
await deletePeer(site.exitNodeId!, site.pubKey);
|
||||||
}
|
}
|
||||||
} else if (site.type == "newt") {
|
} else if (site.type == "newt") {
|
||||||
// Clean up all client associations and send peer/proxy removal
|
const networks = await trx
|
||||||
// messages in a single efficient pass before deleting the row.
|
.select({ networkId: siteNetworks.networkId })
|
||||||
await cleanupSiteAssociations(site, trx);
|
.from(siteNetworks)
|
||||||
|
.where(eq(siteNetworks.siteId, siteId));
|
||||||
|
|
||||||
await trx.delete(sites).where(eq(sites.siteId, siteId));
|
// loop through them
|
||||||
|
const updatedSiteResources = await trx
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
inArray(
|
||||||
|
siteResources.networkId,
|
||||||
|
networks.map((n) => n.networkId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (const siteResource of updatedSiteResources) {
|
||||||
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
|
siteResource,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the newt on the site by querying the newt table for siteId
|
||||||
|
const [deletedNewt] = await trx
|
||||||
|
.delete(newts)
|
||||||
|
.where(eq(newts.siteId, siteId))
|
||||||
|
.returning();
|
||||||
|
if (deletedNewt) {
|
||||||
|
deletedNewtId = deletedNewt.newtId;
|
||||||
|
|
||||||
|
// delete all of the sessions for the newt
|
||||||
|
await trx
|
||||||
|
.delete(newtSessions)
|
||||||
|
.where(eq(newtSessions.newtId, deletedNewt.newtId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await trx.delete(sites).where(eq(sites.siteId, siteId));
|
||||||
|
|
||||||
await usageService.add(site.orgId, FeatureId.SITES, -1, trx);
|
await usageService.add(site.orgId, FeatureId.SITES, -1, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send termination message outside of transaction to prevent blocking
|
// Send termination message outside of transaction to prevent blocking
|
||||||
if (deletedNewt) {
|
if (deletedNewtId) {
|
||||||
const payload = {
|
const payload = {
|
||||||
type: `newt/wg/terminate`,
|
type: `newt/wg/terminate`,
|
||||||
data: {}
|
data: {}
|
||||||
};
|
};
|
||||||
// Don't await this to prevent blocking the response
|
// Don't await this to prevent blocking the response
|
||||||
sendToClient(deletedNewt.newtId, payload).catch((error) => {
|
sendToClient(deletedNewtId, payload).catch((error) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Failed to send termination message to newt:",
|
"Failed to send termination message to newt:",
|
||||||
error
|
error
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
import {
|
||||||
|
rebuildClientAssociationsFromClient,
|
||||||
|
rebuildClientAssociationsFromSiteResource
|
||||||
|
} from "@server/lib/rebuildClientAssociations";
|
||||||
|
|
||||||
const batchAddClientToSiteResourcesParamsSchema = z
|
const batchAddClientToSiteResourcesParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -153,65 +153,6 @@ export default function GeneralPage() {
|
|||||||
const [approvalId, setApprovalId] = useState<number | null>(null);
|
const [approvalId, setApprovalId] = useState<number | null>(null);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
const [cacheCheck, setCacheCheck] = useState<null | {
|
|
||||||
consistent: boolean;
|
|
||||||
missingSiteResourceIds: number[];
|
|
||||||
extraSiteResourceIds: number[];
|
|
||||||
missingSiteIds: number[];
|
|
||||||
extraSiteIds: number[];
|
|
||||||
expectedSiteResourceIds: number[];
|
|
||||||
actualSiteResourceIds: number[];
|
|
||||||
expectedSiteIds: number[];
|
|
||||||
actualSiteIds: number[];
|
|
||||||
}>(null);
|
|
||||||
const [isCheckingCache, setIsCheckingCache] = useState(false);
|
|
||||||
const [isRebuildingCache, setIsRebuildingCache] = useState(false);
|
|
||||||
|
|
||||||
const handleRebuildCache = async () => {
|
|
||||||
if (!client.clientId) return;
|
|
||||||
setIsRebuildingCache(true);
|
|
||||||
try {
|
|
||||||
await api.post(
|
|
||||||
`/client/${client.clientId}/rebuild-associations-cache`
|
|
||||||
);
|
|
||||||
// Re-verify after rebuild so the result refreshes
|
|
||||||
const res = await api.get(
|
|
||||||
`/client/${client.clientId}/verify-associations-cache`
|
|
||||||
);
|
|
||||||
setCacheCheck(res.data.data);
|
|
||||||
toast({
|
|
||||||
title: "Cache rebuilt",
|
|
||||||
description: "Association cache rebuilt successfully."
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Rebuild failed",
|
|
||||||
description: formatAxiosError(e, "Failed to rebuild cache")
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsRebuildingCache(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVerifyCache = async () => {
|
|
||||||
if (!client.clientId) return;
|
|
||||||
setIsCheckingCache(true);
|
|
||||||
try {
|
|
||||||
const res = await api.get(
|
|
||||||
`/client/${client.clientId}/verify-associations-cache`
|
|
||||||
);
|
|
||||||
setCacheCheck(res.data.data);
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Cache check failed",
|
|
||||||
description: formatAxiosError(e, "Failed to verify cache")
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsCheckingCache(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const showApprovalFeatures =
|
const showApprovalFeatures =
|
||||||
@@ -903,75 +844,6 @@ export default function GeneralPage() {
|
|||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hidden cache verification — subtle button, dev/admin diagnostic */}
|
|
||||||
<div className="mt-8 flex flex-col gap-2 items-start opacity-30 hover:opacity-100 transition-opacity">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleVerifyCache}
|
|
||||||
disabled={isCheckingCache}
|
|
||||||
className="text-xs text-muted-foreground underline disabled:opacity-50"
|
|
||||||
title="Verify the client's site association cache against current permissions (read-only)"
|
|
||||||
>
|
|
||||||
{isCheckingCache
|
|
||||||
? "Checking cache…"
|
|
||||||
: "Verify association cache"}
|
|
||||||
</button>
|
|
||||||
{cacheCheck && (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
"text-xs rounded border px-2 py-1 " +
|
|
||||||
(cacheCheck.consistent
|
|
||||||
? "border-green-600 text-green-700"
|
|
||||||
: "border-red-600 text-red-700")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{cacheCheck.consistent ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<CheckCircle2 className="h-3 w-3" />
|
|
||||||
Cache is consistent
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-1 font-semibold">
|
|
||||||
<XCircle className="h-3 w-3" />
|
|
||||||
Cache is INCONSISTENT
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Missing site resources: [
|
|
||||||
{cacheCheck.missingSiteResourceIds.join(
|
|
||||||
", "
|
|
||||||
)}
|
|
||||||
]
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Extra site resources: [
|
|
||||||
{cacheCheck.extraSiteResourceIds.join(", ")}
|
|
||||||
]
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Missing sites: [
|
|
||||||
{cacheCheck.missingSiteIds.join(", ")}]
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Extra sites: [
|
|
||||||
{cacheCheck.extraSiteIds.join(", ")}]
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleRebuildCache}
|
|
||||||
disabled={isRebuildingCache}
|
|
||||||
className="mt-1 text-xs underline font-semibold disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isRebuildingCache
|
|
||||||
? "Rebuilding…"
|
|
||||||
: "Rebuild cache now"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,11 +44,77 @@ export type AuthPageCustomizationProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AuthPageFormSchema = z.object({
|
const AuthPageFormSchema = z.object({
|
||||||
logoUrl: z
|
logoUrl: z.union([
|
||||||
.string()
|
z.literal(""),
|
||||||
.optional()
|
z.string().superRefine(async (urlOrPath, ctx) => {
|
||||||
.transform((val) => (val === "" ? undefined : val)),
|
const parseResult = z.url().safeParse(urlOrPath);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
if (build !== "enterprise") {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "Must be a valid URL"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
validateLocalPath(urlOrPath);
|
||||||
|
} catch (error) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message:
|
||||||
|
"Must be either a valid image URL or a valid pathname starting with `/` and not containing query parameters, `..` or `*`"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(urlOrPath, {
|
||||||
|
method: "HEAD"
|
||||||
|
}).catch(() => {
|
||||||
|
// If HEAD fails (CORS or method not allowed), try GET
|
||||||
|
return fetch(urlOrPath, { method: "GET" });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: `Failed to load image. Please check that the URL is accessible.`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get("content-type") ?? "";
|
||||||
|
if (!contentType.startsWith("image/")) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: `URL does not point to an image. Please provide a URL to an image file (e.g., .png, .jpg, .svg).`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
let errorMessage =
|
||||||
|
"Unable to verify image URL. Please check that the URL is accessible and points to an image file.";
|
||||||
|
|
||||||
|
if (
|
||||||
|
error instanceof TypeError &&
|
||||||
|
error.message.includes("fetch")
|
||||||
|
) {
|
||||||
|
errorMessage =
|
||||||
|
"Network error: Unable to reach the URL. Please check your internet connection and verify the URL is correct.";
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
errorMessage = `Error verifying URL: ${error.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]),
|
||||||
logoWidth: z.coerce.number<number>().min(1),
|
logoWidth: z.coerce.number<number>().min(1),
|
||||||
logoHeight: z.coerce.number<number>().min(1),
|
logoHeight: z.coerce.number<number>().min(1),
|
||||||
orgTitle: z.string().optional(),
|
orgTitle: z.string().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user