Compare commits

..
Author SHA1 Message Date
gorkem-bwl ea698b6558 feat: add HTTP basic authentication support for monitors (#3033)
## Changes
- Add basicAuthUser and basicAuthPassword fields to Monitor model, types, and validation (server + client)
- Update networkService to send Authorization: Basic header when credentials are provided
- Add basic auth form fields (username/password) to the create/edit monitor page for HTTP monitors
- Add i18n strings for the new basic authentication section

## Benefits
- Users can monitor endpoints protected by HTTP basic authentication
- Credentials are sent as standard Authorization: Basic base64-encoded header
- Basic auth is only used when bearer token (secret) is not configured
2026-03-04 15:45:38 -05:00
65 changed files with 2040 additions and 2431 deletions

No files matched your search

+171 -141
View File
@@ -44,7 +44,7 @@
"redux-persist": "6.0.0",
"swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
"zod": "^4.3.6"
"zod": "^4.1.11"
},
"devDependencies": {
"@types/node": "24.5.2",
@@ -58,7 +58,7 @@
"lint-staged": "^16.2.7",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "6.4.1"
"vite": "6.3.6"
}
},
"node_modules/@babel/code-frame": {
@@ -83,7 +83,6 @@
"node_modules/@babel/core": {
"version": "7.29.0",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -354,7 +353,6 @@
"node_modules/@emotion/react": {
"version": "11.14.0",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -392,7 +390,6 @@
"node_modules/@emotion/styled": {
"version": "11.14.1",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -437,6 +434,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -453,6 +451,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -469,6 +468,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -485,6 +485,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -501,6 +502,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -517,6 +519,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -533,6 +536,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -549,6 +553,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -565,6 +570,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -581,6 +587,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -597,6 +604,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -613,6 +621,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -629,6 +638,7 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -645,6 +655,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -661,6 +672,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -677,6 +689,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -691,6 +704,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -707,6 +721,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -723,6 +738,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -739,6 +755,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -755,6 +772,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -771,6 +789,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -787,6 +806,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -803,6 +823,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -819,6 +840,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -835,6 +857,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1162,7 +1185,6 @@
"node_modules/@mui/material": {
"version": "7.3.7",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.7",
@@ -1267,7 +1289,6 @@
"node_modules/@mui/system": {
"version": "7.3.7",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/private-theming": "^7.3.7",
@@ -1639,12 +1660,13 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1652,12 +1674,13 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1665,12 +1688,13 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1678,12 +1702,13 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1691,12 +1716,13 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1704,12 +1730,13 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1717,12 +1744,13 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1730,12 +1758,13 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1743,12 +1772,13 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1756,12 +1786,13 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1769,12 +1800,13 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1782,12 +1814,13 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1795,12 +1828,13 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1808,12 +1842,13 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1821,12 +1856,13 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1834,12 +1870,13 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1847,12 +1884,13 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1860,12 +1898,11 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"version": "4.57.1",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1873,12 +1910,13 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1886,12 +1924,13 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1899,12 +1938,13 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1912,12 +1952,13 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1925,12 +1966,13 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1938,12 +1980,13 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1951,12 +1994,13 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2306,9 +2350,8 @@
},
"node_modules/@types/node": {
"version": "24.5.2",
"devOptional": true,
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.12.0"
}
@@ -2323,8 +2366,8 @@
},
"node_modules/@types/react": {
"version": "18.3.27",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -2446,7 +2489,6 @@
"version": "8.15.0",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2463,9 +2505,7 @@
}
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"version": "6.12.6",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2771,7 +2811,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3203,8 +3242,7 @@
},
"node_modules/dayjs": {
"version": "1.11.13",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
@@ -3528,6 +3566,7 @@
},
"node_modules/esbuild": {
"version": "0.25.12",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -3586,7 +3625,6 @@
"version": "8.57.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -3846,6 +3884,7 @@
},
"node_modules/fdir": {
"version": "6.5.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -3979,6 +4018,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -4331,7 +4371,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -5115,7 +5154,6 @@
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -5213,9 +5251,7 @@
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5281,6 +5317,7 @@
},
"node_modules/nanoid": {
"version": "3.3.11",
"dev": true,
"funding": [
{
"type": "github",
@@ -5613,6 +5650,7 @@
},
"node_modules/postcss": {
"version": "8.5.6",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -5763,7 +5801,6 @@
"node_modules/react-dom": {
"version": "18.3.1",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -5775,7 +5812,6 @@
"node_modules/react-hook-form": {
"version": "7.71.1",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -5975,8 +6011,7 @@
},
"node_modules/redux": {
"version": "5.0.1",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/redux-persist": {
"version": "6.0.0",
@@ -6122,11 +6157,9 @@
"license": "Unlicense"
},
"node_modules/rollup": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"version": "4.57.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -6138,31 +6171,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"@rollup/rollup-android-arm-eabi": "4.57.1",
"@rollup/rollup-android-arm64": "4.57.1",
"@rollup/rollup-darwin-arm64": "4.57.1",
"@rollup/rollup-darwin-x64": "4.57.1",
"@rollup/rollup-freebsd-arm64": "4.57.1",
"@rollup/rollup-freebsd-x64": "4.57.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
"@rollup/rollup-linux-arm64-musl": "4.57.1",
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
"@rollup/rollup-linux-loong64-musl": "4.57.1",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
"@rollup/rollup-linux-x64-gnu": "4.57.1",
"@rollup/rollup-linux-x64-musl": "4.57.1",
"@rollup/rollup-openbsd-x64": "4.57.1",
"@rollup/rollup-openharmony-arm64": "4.57.1",
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
"@rollup/rollup-win32-x64-gnu": "4.57.1",
"@rollup/rollup-win32-x64-msvc": "4.57.1",
"fsevents": "~2.3.2"
}
},
@@ -6497,6 +6530,7 @@
},
"node_modules/source-map-js": {
"version": "1.2.1",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -6784,6 +6818,7 @@
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -6913,9 +6948,8 @@
},
"node_modules/typescript": {
"version": "5.9.3",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6958,7 +6992,7 @@
},
"node_modules/undici-types": {
"version": "7.12.0",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/union-value": {
@@ -7047,11 +7081,9 @@
}
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"version": "6.3.6",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -7334,7 +7366,7 @@
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"devOptional": true,
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -7359,8 +7391,6 @@
},
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
+3 -3
View File
@@ -13,7 +13,7 @@
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "prettier --write"
"**/*": "npm run format"
},
"dependencies": {
"@emotion/react": "^11.13.3",
@@ -52,7 +52,7 @@
"redux-persist": "6.0.0",
"swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0",
"zod": "^4.3.6"
"zod": "^4.1.11"
},
"devDependencies": {
"@types/node": "24.5.2",
@@ -66,6 +66,6 @@
"lint-staged": "^16.2.7",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "6.4.1"
"vite": "6.3.6"
}
}
@@ -32,7 +32,7 @@ export const HeatmapResponseTime = ({
if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-25);
const latestChecks = checks.slice(-25).reverse();
let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) {
@@ -1,6 +1,9 @@
import Box from "@mui/material/Box";
import { useState, useEffect, useRef } from "react";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import BackgroundSVG from "@/assets/Images/background.svg";
import type { RootState } from "@/Types/state";
import { OfflineBanner } from "@/Components/design-elements";
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
@@ -10,6 +13,7 @@ interface AppLayoutProps {
const AppLayout = ({ children }: AppLayoutProps) => {
const theme = useTheme();
const mode = useSelector((state: RootState) => state.ui.mode);
const [serverUnreachable, setServerUnreachable] = useState(false);
const retryIntervalRef = useRef<number | null>(null);
@@ -43,6 +47,10 @@ const AppLayout = ({ children }: AppLayoutProps) => {
sx={{
minHeight: "100vh",
backgroundColor: theme.palette.background.default,
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<OfflineBanner visible={serverUnreachable} />
@@ -7,7 +7,7 @@ import type { MonitorType } from "@/Types/Monitor";
import { Typography, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
const types = ["http", "ping", "port", "docker", "game", "grpc", "websocket"];
const types = ["http", "ping", "port", "docker", "game", "grpc"];
const typeDisplayNames: Record<string, string> = {
http: "HTTP",
ping: "Ping",
@@ -15,7 +15,6 @@ const typeDisplayNames: Record<string, string> = {
docker: "Docker",
game: "Game",
grpc: "gRPC",
websocket: "WebSocket",
};
const statuses = ["up", "down"];
const states = ["active", "paused"];
+4 -8
View File
@@ -40,6 +40,8 @@ export const useMonitorForm = ({
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
basicAuthUser: data?.basicAuthUser || "",
basicAuthPassword: data?.basicAuthPassword || "",
};
break;
case "ping":
@@ -103,14 +105,6 @@ export const useMonitorForm = ({
selectedDisks: data?.selectedDisks || [],
};
break;
case "websocket":
defaults = {
...base,
type: "websocket",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
default:
defaults = {
...base,
@@ -121,6 +115,8 @@ export const useMonitorForm = ({
matchMethod: "",
expectedValue: "",
jsonPath: "",
basicAuthUser: "",
basicAuthPassword: "",
};
}
+11 -64
View File
@@ -11,17 +11,12 @@ import { ConfigBox, BasePage } from "@/Components/design-elements";
import { UserRoles } from "@/Types/User";
import { useTranslation } from "react-i18next";
import type { UserRole, User } from "@/Types/User";
import { useParams, useNavigate } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useTheme } from "@mui/material";
import { useEditUserForm } from "@/Hooks/useEditUserForm";
import { useGet, usePatch, useDelete } from "@/Hooks/UseApi";
import { useGet, usePatch } from "@/Hooks/UseApi";
import type { EditUserFormData } from "@/Validation/editUser";
import { useEffect, useState } from "react";
import { DialogInput } from "@/Components/inputs/Dialog";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin";
import { useEffect } from "react";
interface RoleOption {
id: UserRole;
@@ -34,12 +29,6 @@ const EditUserPage = () => {
const { userId } = useParams<{ userId: string }>();
const { resolver, defaults } = useEditUserForm();
const { patch, loading: isSaving } = usePatch();
const { deleteFn, loading: isDeleting } = useDelete();
const navigate = useNavigate();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const currentUser = useSelector((state: RootState) => state.auth.user);
const isSuperAdmin = useIsSuperAdmin();
const isAdmin = useIsAdmin();
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
@@ -71,20 +60,6 @@ const EditUserPage = () => {
setValue("role", newRoles, { shouldValidate: true });
};
const canDeleteUser =
isAdmin &&
userId !== currentUser?.id &&
!user?.role?.includes("demo") &&
(isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin"));
const handleDeleteUser = async () => {
const result = await deleteFn(`/auth/users/${userId}`);
setShowDeleteDialog(false);
if (result) {
navigate("/account", { state: { tab: "team" } });
}
};
const onSubmit = async (data: EditUserFormData) => {
await patch(`/auth/users/${userId}`, data);
};
@@ -190,45 +165,17 @@ const EditUserPage = () => {
</Stack>
}
/>
<Stack
gap={LAYOUT.XS}
direction="row"
justifyContent={"flex-end"}
width="100%"
<Button
type="submit"
variant="contained"
color="primary"
loading={isSaving}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
>
{canDeleteUser && (
<Button
variant="contained"
color="error"
onClick={() => setShowDeleteDialog(true)}
sx={{ minWidth: 100 }}
>
{t("common.buttons.removeUser")}
</Button>
)}
<Button
type="submit"
variant="contained"
color="primary"
loading={isSaving}
sx={{ minWidth: 100 }}
>
{t("common.buttons.save")}
</Button>
</Stack>
{t("common.buttons.save")}
</Button>
</Stack>
</form>
<DialogInput
open={showDeleteDialog}
title={t("pages.editUser.dialog.removeUser.title")}
content={t("pages.editUser.dialog.removeUser.content", {
name: `${user?.firstName} ${user?.lastName}`,
})}
onCancel={() => setShowDeleteDialog(false)}
onConfirm={handleDeleteUser}
confirmText={t("common.buttons.removeUser")}
loading={isDeleting}
/>
</BasePage>
);
};
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Table } from "@/Components/design-elements";
import type { Header } from "@/Components/design-elements/Table";
import { useIsAdmin } from "@/Hooks/useIsAdmin";
import { useIsSuperAdmin } from "@/Hooks/useIsAdmin";
import type { User } from "@/Types/User";
interface TeamTableProps {
@@ -13,7 +13,7 @@ interface TeamTableProps {
export const TeamTable = ({ users }: TeamTableProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const isSuperAdmin = useIsSuperAdmin();
const headers: Header<User>[] = [
{
@@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
];
const handleRowClick = (row: User) => {
if (isAdmin) {
if (isSuperAdmin) {
navigate(`/account/team/${row.id}`);
}
};
@@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
<Table
headers={headers}
data={users}
onRowClick={isAdmin ? handleRowClick : undefined}
onRowClick={isSuperAdmin ? handleRowClick : undefined}
/>
);
};
+47 -28
View File
@@ -32,12 +32,7 @@ import {
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
import { useMonitorForm } from "@/Hooks/useMonitorForm";
import {
type Monitor,
type MonitorType,
type GamesMap,
supportsGeoCheck,
} from "@/Types/Monitor";
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor";
@@ -146,17 +141,6 @@ const getGeneralSettingsConfig = (
showGrpcServiceName: false,
showIgnoreTls: false,
},
websocket: {
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.wsUrl.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: true,
},
};
return configs[type] || configs.http;
};
@@ -342,13 +326,6 @@ const CreateMonitorPage = () => {
"pages.createMonitor.form.type.optionGrpcDescription"
)}
/>
<RadioWithDescription
value="websocket"
label={t("pages.createMonitor.form.type.optionWebSocket")}
description={t(
"pages.createMonitor.form.type.optionWebSocketDescription"
)}
/>
</RadioGroup>
</FormControl>
)}
@@ -765,9 +742,7 @@ const CreateMonitorPage = () => {
}
/>
{(watchedType === "http" ||
watchedType === "grpc" ||
watchedType === "websocket") && (
{(watchedType === "http" || watchedType === "grpc") && (
<ConfigBox
title={t("pages.createMonitor.form.ignoreTls.title")}
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
@@ -795,6 +770,50 @@ const CreateMonitorPage = () => {
/>
)}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.basicAuth.title")}
subtitle={t("pages.createMonitor.form.basicAuth.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="basicAuthUser"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.createMonitor.form.basicAuth.option.username.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="basicAuthPassword"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
type="password"
fieldLabel={t(
"pages.createMonitor.form.basicAuth.option.password.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
)}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.advanced.title")}
@@ -910,7 +929,7 @@ const CreateMonitorPage = () => {
/>
)}
{supportsGeoCheck(watchedType) && (
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")}
+3 -3
View File
@@ -20,7 +20,7 @@ import { useState, useMemo } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useGet } from "@/Hooks/UseApi";
import { type MonitorDetailsResponse, supportsGeoCheck } from "@/Types/Monitor";
import type { MonitorDetailsResponse } from "@/Types/Monitor";
import type { ChecksResponse } from "@/Types/Check";
import type {
GeoChecksResult,
@@ -122,7 +122,7 @@ const UptimeDetailsPage = () => {
);
const geoChecksUrl = useMemo(() => {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
return null;
}
const params = new URLSearchParams();
@@ -141,7 +141,7 @@ const UptimeDetailsPage = () => {
// Fetch paginated geo checks for the table
const geoChecksTableUrl = useMemo(() => {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
return null;
}
const params = new URLSearchParams();
+1 -1
View File
@@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => {
const effectiveTypes =
selectedTypes.length > 0
? selectedTypes
: ["http", "ping", "docker", "port", "game", "grpc", "websocket"];
: ["http", "ping", "docker", "port", "game", "grpc"];
// Build URL for monitors with checks
const monitorsWithChecksUrl = useMemo(() => {
+2 -10
View File
@@ -12,20 +12,10 @@ export const MonitorTypes = [
"port",
"game",
"grpc",
"websocket",
"unknown",
] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType | undefined): boolean => {
if (!type) {
return false;
}
return GeoCheckSupportedTypes.includes(type);
};
export const MonitorStatuses = [
"up",
"down",
@@ -54,6 +44,8 @@ export interface Monitor {
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
basicAuthUser?: string;
basicAuthPassword?: string;
url: string;
port?: number;
isActive: boolean;
-1
View File
@@ -9,7 +9,6 @@ export const getMonitorPath = (type: MonitorType): string => {
ping: "uptime",
game: "uptime",
grpc: "uptime",
websocket: "uptime",
unknown: "uptime",
docker: "uptime",
hardware: "infrastructure",
-6
View File
@@ -12,8 +12,6 @@ export const typographyLevels = {
export const colors = {
gray200: "#EFEFEF",
gray700: "#313131",
gray900: "#151518",
gray850: "#1c1c21",
blueBlueWave: "#1570EF",
};
@@ -33,8 +31,4 @@ export const darkPalette = {
secondary: {
main: colors.gray700,
},
background: {
default: colors.gray900,
paper: colors.gray850,
},
};
+2 -9
View File
@@ -38,6 +38,8 @@ const httpSchema = baseSchema.extend({
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(),
jsonPath: z.string().optional(),
basicAuthUser: z.string().optional(),
basicAuthPassword: z.string().optional(),
});
// Ping monitor schema
@@ -115,13 +117,6 @@ const hardwareSchema = baseSchema.extend({
selectedDisks: z.array(z.string()),
});
// WebSocket monitor schema
const websocketSchema = baseSchema.extend({
type: z.literal("websocket"),
url: z.string().min(1, "WebSocket URL is required"),
ignoreTlsErrors: z.boolean(),
});
// Discriminated union of all monitor types
export const monitorSchema = z.discriminatedUnion("type", [
httpSchema,
@@ -132,7 +127,6 @@ export const monitorSchema = z.discriminatedUnion("type", [
grpcSchema,
pagespeedSchema,
hardwareSchema,
websocketSchema,
]);
export type MonitorFormData = z.infer<typeof monitorSchema>;
@@ -147,5 +141,4 @@ export {
grpcSchema,
pagespeedSchema,
hardwareSchema,
websocketSchema,
};
+13 -15
View File
@@ -46,8 +46,7 @@
"sendTestEmail": "Send test email",
"exportToJSON": "Export to JSON",
"importFromJSON": "Import from JSON",
"clearFilters": "Clear filters",
"removeUser": "Remove user"
"clearFilters": "Clear filters"
},
"charts": {
"labels": {
@@ -488,10 +487,6 @@
"grpcServiceName": {
"label": "Service name",
"placeholder": "e.g. my.service.v1 (leave empty for overall health)"
},
"wsUrl": {
"label": "WebSocket URL",
"placeholder": "wss://example.com/socket"
}
},
"title": "General settings",
@@ -503,10 +498,21 @@
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
"websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) and add a display name.",
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
}
},
"basicAuth": {
"description": "Configure HTTP basic authentication credentials for this monitor.",
"option": {
"username": {
"label": "Username"
},
"password": {
"label": "Password"
}
},
"title": "Basic authentication"
},
"ignoreTls": {
"description": "Configure TLS/SSL certificate validation for HTTPS connections.",
"option": {
@@ -540,8 +546,6 @@
"optionGameDescription": "Monitor if a specific game server is online.",
"optionGrpc": "gRPC",
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
"optionWebSocket": "WebSocket",
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
"optionHttp": "HTTP(S)",
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
"optionPing": "Ping",
@@ -627,12 +631,6 @@
"title": "Roles",
"description": "Assign roles to the user. Multiple roles can be selected."
}
},
"dialog": {
"removeUser": {
"title": "Remove user",
"content": "Are you sure you want to remove {{name}} from your team? This action cannot be undone."
}
}
},
"incidents": {
+1 -1
View File
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig(({}) => {
let version = "3.5.1";
let version = "3.5.0";
return {
base: "/",
+705 -657
View File
File diff suppressed because it is too large. Load diff
+3 -5
View File
@@ -15,7 +15,7 @@
"format-check": "prettier --check ."
},
"lint-staged": {
"**/*": "prettier --write"
"**/*": "npm run format"
},
"keywords": [],
"author": "",
@@ -38,7 +38,7 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "8.0.1",
"gamedig": "5.2.0",
"gamedig": "^5.3.1",
"got": "14.4.7",
"handlebars": "^4.7.8",
"helmet": "^8.0.0",
@@ -51,14 +51,13 @@
"mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
"nodemailer": "8.0.1",
"nodemailer": "^6.9.14",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
"super-simple-scheduler": "1.4.5",
"swagger-ui-express": "5.0.1",
"winston": "^3.13.0",
"ws": "^8.19.0",
"zod": "^4.3.6"
},
"devDependencies": {
@@ -79,7 +78,6 @@
"@types/papaparse": "^5.5.2",
"@types/ping": "0.4.4",
"@types/swagger-ui-express": "4.1.8",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"c8": "10.1.3",
+2 -16
View File
@@ -74,22 +74,8 @@ export const createApp = ({
},
})
);
// Swagger UI — dynamically set server URL from request
app.use("/api-docs", swaggerUi.serve, (req: express.Request, res: express.Response, next: express.NextFunction) => {
const protocol = req.protocol;
const host = req.get("host");
const dynamicSpec = {
...openApiSpec,
servers: [
{
url: `${protocol}://${host}/api/v1`,
description: "Current Server",
},
...openApiSpec.servers,
],
};
swaggerUi.setup(dynamicSpec)(req, res, next);
});
// Swagger UI
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
app.use("/api/v1/health", (req, res) => {
res.json({
+52 -92
View File
@@ -1,69 +1,35 @@
import MongoDB from "../db/MongoDB.js";
import { IDb } from "@/db/IDb.js";
import NetworkService from "../service/infrastructure/networkService.js";
import EmailService from "../service/infrastructure/emailService.js";
import BufferService from "../service/infrastructure/bufferService.js";
import {
// Service classes
NetworkService,
EmailService,
BufferService,
GlobalPingService,
SuperSimpleQueue,
SuperSimpleQueueHelper,
NotificationsService,
StatusService,
NotificationMessageBuilder,
MonitorService,
StatusPageService,
UserService,
CheckService,
GeoChecksService,
DiagnosticService,
InviteService,
MaintenanceWindowService,
IncidentService,
// Notification providers
WebhookProvider,
SlackProvider,
EmailProvider,
DiscordProvider,
PagerDutyProvider,
MatrixProvider,
// Interfaces
INetworkService,
IEmailService,
IBufferService,
ISuperSimpleQueue,
INotificationsService,
IStatusService,
IMonitorService,
IUserService,
ICheckService,
IGeoChecksService,
IDiagnosticService,
IInviteService,
IMaintenanceWindowService,
IStatusPageService,
IIncidentService,
INotificationMessageBuilder,
ISettingsService,
EnvConfig,
} from "@/service/index.js";
// Network providers
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js";
// Third-party
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import UserService from "../service/business/userService.js";
import CheckService from "../service/business/checkService.js";
import GeoChecksService from "../service/business/geoChecksService.js";
import GlobalPingService from "../service/infrastructure/globalPingService.js";
import DiagnosticService from "../service/business/diagnosticService.js";
import InviteService from "../service/business/inviteService.js";
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import { MonitorService } from "@/service/index.js";
import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
import IncidentService from "../service/business/incidentService.js";
import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js";
import axios from "axios";
import got from "got";
import ping from "ping";
import https from "https";
import Docker from "dockerode";
import net from "net";
import fs from "fs";
@@ -78,9 +44,8 @@ import { games, GameDig } from "gamedig";
import jmespath from "jmespath";
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import WebSocket from "ws";
// Repositories
// repositories
import {
MongoMonitorsRepository,
MongoChecksRepository,
@@ -109,23 +74,24 @@ import {
IMaintenanceWindowsRepository,
} from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
export type InitializedServices = {
settingsService: ISettingsService;
db: IDb;
networkService: INetworkService;
emailService: IEmailService;
bufferService: IBufferService;
statusService: IStatusService;
jobQueue: ISuperSimpleQueue;
userService: IUserService;
checkService: ICheckService;
geoChecksService: IGeoChecksService;
diagnosticService: IDiagnosticService;
inviteService: IInviteService;
maintenanceWindowService: IMaintenanceWindowService;
monitorService: IMonitorService;
incidentService: IIncidentService;
settingsService: any;
db: any;
networkService: any;
emailService: any;
bufferService: any;
statusService: any;
jobQueue: any;
userService: any;
checkService: any;
geoChecksService: any;
diagnosticService: any;
inviteService: any;
maintenanceWindowService: any;
monitorService: any;
incidentService: any;
logger: ILogger;
notificationsService: INotificationsService;
statusPageService: IStatusPageService;
@@ -155,7 +121,7 @@ export const initializeServices = async ({
}: {
logger: ILogger;
envSettings: EnvConfig;
settingsService: ISettingsService;
settingsService: any;
settingsRepository: ISettingsRepository;
}): Promise<InitializedServices> => {
// Create DB
@@ -178,28 +144,22 @@ export const initializeServices = async ({
const teamsRepository = new MongoTeamsRepository();
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
// Network providers
const pingProvider = new PingProvider(ping);
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
const hardwareProvider = new HardwareProvider(httpProvider);
const dockerProvider = new DockerProvider(logger, Docker);
const portProvider = new PortProvider(net);
const gameProvider = new GameProvider(logger, GameDig);
const grpcProvider = new GrpcProvider(grpc, protoLoader);
const webSocketProvider = new WebSocketProvider(WebSocket);
const networkService = new NetworkService(axios, logger, [
pingProvider,
httpProvider,
pageSpeedProvider,
hardwareProvider,
dockerProvider,
portProvider,
gameProvider,
grpcProvider,
webSocketProvider,
]);
const networkService = new NetworkService(
axios,
got,
https,
jmespath,
GameDig as unknown as {
query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }>;
},
ping,
logger,
Docker,
net,
settingsService,
grpc,
protoLoader
);
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
const notificationMessageBuilder = new NotificationMessageBuilder();
@@ -221,7 +181,6 @@ export const initializeServices = async ({
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
// Notification providers
const webhookProvider = new WebhookProvider(logger);
const slackProvider = new SlackProvider(logger);
const emailProvider = new EmailProvider(emailService, logger);
@@ -305,6 +264,7 @@ export const initializeServices = async ({
const statusPageService = new StatusPageService(statusPagesRepository);
const services = {
//v1
settingsService,
db,
networkService,
-14
View File
@@ -180,20 +180,6 @@ class AuthController {
}
};
deleteUserById = async (req: Request, res: Response, next: NextFunction) => {
try {
getUserByIdParamValidation.parse(req.params);
const targetUserId = req.params.userId;
await this.userService.deleteUserById(req.user, targetUserId);
return res.status(200).json({
success: true,
msg: "User removed successfully",
});
} catch (error) {
next(error);
}
};
getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
const allUsers = await this.userService.getAllUsers();
-4
View File
@@ -1,4 +0,0 @@
export interface IDb {
connect(): Promise<void>;
disconnect(): Promise<void>;
}
+9 -11
View File
@@ -1,12 +1,10 @@
import mongoose from "mongoose";
import AppSettings from "@/db/models/AppSettings.js";
import { runMigrations } from "@/db/migration/index.js";
import AppSettings from "./models/AppSettings.js";
import { runMigrations } from "./migration/index.js";
import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
const SERVICE_NAME = "MongoDB";
import { IDb } from "@/db/IDb.js";
class MongoDB implements IDb {
class MongoDB {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -48,12 +46,12 @@ class MongoDB implements IDb {
});
await runMigrations();
} catch (error: unknown) {
} catch (error: any) {
this.logger.error({
message: error instanceof Error ? error.message : "Unknown error",
message: error.message,
service: SERVICE_NAME,
method: "connect",
stack: error instanceof Error ? error.stack : undefined,
stack: error.stack,
});
throw error;
}
@@ -65,12 +63,12 @@ class MongoDB implements IDb {
await mongoose.disconnect();
this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" });
return;
} catch (error: unknown) {
} catch (error: any) {
this.logger.error({
message: error instanceof Error ? error.message : "Unknown error",
message: error.message,
service: SERVICE_NAME,
method: "disconnect",
stack: error instanceof Error ? error.stack : undefined,
stack: error.stack,
});
}
};
@@ -1,60 +0,0 @@
import { MonitorModel } from "../models/Monitor.js";
import { logger } from "@/utils/logger.js";
/**
* Fix infrastructure monitors that had thresholds incorrectly set to 0 or 5
* due to the old default value. Updates them to 100 (disabled).
*/
export async function fixInfrastructureThresholds(): Promise<void> {
const SERVICE_NAME = "Migration:FixInfrastructureThresholds";
try {
logger.info({ service: SERVICE_NAME, message: "Starting infrastructure threshold fix" });
const thresholdFields = ["cpuAlertThreshold", "memoryAlertThreshold", "diskAlertThreshold", "tempAlertThreshold"];
// Find hardware monitors with any threshold set to 0 or 5
const filter = {
type: "hardware",
$or: thresholdFields.flatMap((field) => [{ [field]: 0 }, { [field]: 5 }]),
};
const monitors = await MonitorModel.find(filter);
if (monitors.length === 0) {
logger.info({ service: SERVICE_NAME, message: "No monitors with threshold value of 0 or 5 found" });
return;
}
logger.info({
service: SERVICE_NAME,
message: `Found ${monitors.length} monitors with threshold value of 0 or 5`,
});
let updatedCount = 0;
for (const monitor of monitors) {
const update: Record<string, number> = {};
for (const field of thresholdFields) {
if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) {
update[field] = 100;
}
}
if (Object.keys(update).length > 0) {
await MonitorModel.updateOne({ _id: monitor._id }, { $set: update });
updatedCount++;
}
}
logger.info({
service: SERVICE_NAME,
message: `Fixed ${updatedCount} monitors — thresholds updated from 0/5 to 100`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error({ service: SERVICE_NAME, message: `Error fixing thresholds: ${errorMessage}` });
throw error;
}
}
-2
View File
@@ -1,7 +1,6 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
import MigrationModel from "../models/Migration.js";
type MigrationEntry = {
@@ -13,7 +12,6 @@ const migrations: MigrationEntry[] = [
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
];
const runMigrations = async (logger?: { info: Function; error: Function }) => {
+6
View File
@@ -93,6 +93,12 @@ const MonitorSchema = new Schema<MonitorDocument>(
type: String,
enum: ["equal", "include", "regex", ""],
},
basicAuthUser: {
type: String,
},
basicAuthPassword: {
type: String,
},
url: {
type: String,
required: true,
+4 -3
View File
@@ -9,7 +9,7 @@ import fs from "fs";
import { runMigrations } from "./db/migration/index.js";
import Logger, { ILogger } from "@/utils/logger.js";
import { SettingsService } from "@/service/index.js";
import SettingsService from "@/service/system/settingsService.js";
import { MongoSettingsRepository } from "./repositories/index.js";
const SERVICE_NAME = "Server";
@@ -50,8 +50,9 @@ const startApp = async () => {
openApiSpec,
});
const server = app.listen(env.PORT, () => {
logger.info({ message: `Server started on port:${env.PORT}` });
const port = 52345;
const server = app.listen(port, () => {
logger.info({ message: `Server started on port:${port}` });
});
initShutdownListener(server, services);
@@ -20,19 +20,12 @@ export interface IChecksRepository {
monitorId: string,
sortOrder: string,
dateRange: string,
filter: string | undefined,
filter: string,
page: number,
rowsPerPage: number,
status: boolean | undefined
): Promise<ChecksQueryResult>;
findByTeamId(
sortOrder: string,
dateRange: string,
filter: string | undefined,
page: number,
rowsPerPage: number,
teamId: string
): Promise<ChecksQueryResult>;
findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise<ChecksQueryResult>;
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findByDateRangeAndMonitorId(
monitorId: string,
@@ -231,7 +231,7 @@ class MongoChecksRepository implements IChecksRepository {
monitorId: string,
sortOrder: string,
dateRange: string,
filter: string | undefined,
filter: string,
page: number,
rowsPerPage: number,
status: boolean | undefined
@@ -1,18 +1,17 @@
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
import type { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose";
import { getDateForRange } from "@/utils/dataUtils.js";
import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "GeoChecksRepository";
class MongoGeoChecksRepository implements IGeoChecksRepository {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
constructor(logger: ILogger) {
private logger: any;
constructor(logger: any) {
this.logger = logger;
}
+1 -2
View File
@@ -26,10 +26,9 @@ class AuthRoutes {
this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers);
this.router.post("/users", verifyJWT, isAllowed(["superadmin"]), upload.single("profileImage"), this.authController.createUser);
this.router.get("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getUserById);
this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById);
this.router.patch("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById);
this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById);
this.router.delete("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.deleteUserById);
this.router.patch("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
this.router.delete("/user", verifyJWT, this.authController.deleteUser);
+6 -9
View File
@@ -19,7 +19,7 @@ export interface ICheckService {
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
}
export class CheckService implements ICheckService {
class CheckService implements ICheckService {
static SERVICE_NAME = SERVICE_NAME;
private monitorsRepository: IMonitorsRepository;
@@ -124,13 +124,8 @@ export class CheckService implements ICheckService {
await this.monitorsRepository.findById(monitorId, teamId);
const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
if (!sortOrder || !dateRange || !page || !rowsPerPage) {
throw new AppError({
message: `Missing required query parameters sortOrder: ${sortOrder}, dateRange: ${dateRange}, filter: ${filter}, page: ${page}, rowsPerPage: ${rowsPerPage}`,
service: SERVICE_NAME,
method: "getChecksByMonitor",
status: 400,
});
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
}
const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status);
const parsedPage = page ? parseInt(page) : 0;
@@ -143,7 +138,7 @@ export class CheckService implements ICheckService {
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record<string, string> }) => {
const { sortOrder, dateRange, filter, page, rowsPerPage } = query;
if (!sortOrder || !dateRange || !page || !rowsPerPage) {
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
}
@@ -194,3 +189,5 @@ export class CheckService implements ICheckService {
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
};
}
export default CheckService;
@@ -3,19 +3,15 @@ import os from "os";
const SERVICE_NAME = "diagnosticService";
export interface IDiagnosticService {
getCPUUsage(): Promise<{ userUsageMs: number; systemUsageMs: number; usagePercentage: number }>;
getSystemStats(): Promise<{
osStats: { freeMemoryBytes: number; totalMemoryBytes: number };
memoryUsage: Record<keyof NodeJS.MemoryUsage, number>;
cpuUsage: { userUsageMs: number; systemUsageMs: number; usagePercentage: number };
v8HeapStats: { totalHeapSizeBytes: number; usedHeapSizeBytes: number; heapSizeLimitBytes: number };
eventLoopDelayMs: number;
uptimeMs: number;
}>;
interface MemoryUsageMetrics {
rss: number;
heapTotal: number;
heapUsed: number;
external: number;
arrayBuffers: number;
}
export class DiagnosticService implements IDiagnosticService {
class DiagnosticService {
static SERVICE_NAME = SERVICE_NAME;
constructor() {
@@ -107,3 +103,5 @@ export class DiagnosticService implements IDiagnosticService {
return diagnostics;
};
}
export default DiagnosticService;
@@ -16,7 +16,7 @@ export interface IGeoChecksService {
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
}
export class GeoChecksService implements IGeoChecksService {
class GeoChecksService implements IGeoChecksService {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -68,7 +68,7 @@ export class GeoChecksService implements IGeoChecksService {
}
// Step 1: Create measurement request
const measurementId = await this.globalPingService.createMeasurement(monitor.type, monitor.url, monitor.geoCheckLocations);
const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations);
if (!measurementId) {
// GlobalPing API is down, skip this check
@@ -76,6 +76,7 @@ export class GeoChecksService implements IGeoChecksService {
message: "Skipping geo check due to API unavailability",
service: SERVICE_NAME,
method: "buildGeoCheck",
details: { monitorId: monitor.id },
});
return null;
}
@@ -89,6 +90,7 @@ export class GeoChecksService implements IGeoChecksService {
message: "No successful geo check results",
service: SERVICE_NAME,
method: "buildGeoCheck",
details: { monitorId: monitor.id, measurementId },
});
return null;
}
@@ -100,16 +102,17 @@ export class GeoChecksService implements IGeoChecksService {
message: `Geo check completed for monitor ${monitor.id}`,
service: SERVICE_NAME,
method: "buildGeoCheck",
details: { monitorId: monitor.id, resultsCount: results.length },
});
return geoCheck;
} catch (error: unknown) {
} catch (error: any) {
this.logger.error({
message: "Error executing geo check",
service: SERVICE_NAME,
method: "buildGeoCheck",
details: { monitorId: monitor.id, error: error instanceof Error ? error.message : "Unknown error" },
stack: error instanceof Error ? error.stack : undefined,
details: { monitorId: monitor.id, error: error.message },
stack: error.stack,
});
return null;
}
@@ -169,9 +172,21 @@ export class GeoChecksService implements IGeoChecksService {
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
this.logger.debug({
message: "getGeoChecksByMonitor query params",
service: SERVICE_NAME,
method: "getGeoChecksByMonitor",
details: { continent, continents, query },
});
const parsedPage = page ? parseInt(page) : page;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
return result;
};
}
export default GeoChecksService;
+4 -24
View File
@@ -5,34 +5,12 @@ import { AppError } from "@/utils/AppError.js";
import { ParseBoolean } from "@/utils/utils.js";
import { getDateForRange } from "@/utils/dataUtils.js";
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
import type { Incident, IncidentSummary, User } from "@/types/index.js";
import type { Incident } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
import type { ILogger } from "@/utils/logger.js";
export interface IIncidentService {
handleIncident(
monitor: Monitor,
code: number,
decision: MonitorActionDecision,
monitorStatusResponse?: MonitorStatusResponse
): Promise<Incident | null>;
resolveIncident(incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string): Promise<Incident>;
getIncidentsByTeam(
teamId: string,
sortOrder: string,
dateRange: string,
page: string,
rowsPerPage: string,
status: string,
monitorId: string,
resolutionType: string
): Promise<{ incidents: Incident[]; count: number }>;
getIncidentSummary(teamId: string, limit?: string): Promise<IncidentSummary>;
getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>;
}
export class IncidentService implements IIncidentService {
class IncidentService {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -266,3 +244,5 @@ export class IncidentService implements IIncidentService {
}
};
}
export default IncidentService;
+3 -7
View File
@@ -5,13 +5,7 @@ import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "inviteService";
export interface IInviteService {
getInviteToken(params: { invite: Partial<Invite>; teamId: string; userRoles: UserRole[] }): Promise<Invite>;
sendInviteEmail(params: { invite: Partial<Invite>; firstName: string; userRoles: UserRole[] }): Promise<void>;
verifyInviteToken(params: { inviteToken: string }): Promise<Invite>;
}
export class InviteService implements IInviteService {
class InviteService {
static SERVICE_NAME = SERVICE_NAME;
private settingsService: any;
@@ -94,3 +88,5 @@ export class InviteService implements IInviteService {
return await this.invitesRepository.findByToken(inviteToken);
};
}
export default InviteService;
@@ -1,23 +1,10 @@
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { MaintenanceWindow } from "@/types/index.js";
import { ParseBoolean } from "@/utils/utils.js";
import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "maintenanceWindowService";
export interface IMaintenanceWindowService {
createMaintenanceWindow(params: { teamId: string; body: Partial<MaintenanceWindow> & { monitors: string[] } }): Promise<void>;
getMaintenanceWindowById(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
getMaintenanceWindowsByTeamId(params: {
teamId: string;
query: Record<string, string>;
}): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>;
getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise<MaintenanceWindow[]>;
deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise<MaintenanceWindow>;
editMaintenanceWindow(params: { id: string; teamId: string; body: Partial<MaintenanceWindow> }): Promise<MaintenanceWindow>;
}
export class MaintenanceWindowService implements IMaintenanceWindowService {
class MaintenanceWindowService {
static SERVICE_NAME = SERVICE_NAME;
private monitorsRepository: IMonitorsRepository;
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
@@ -96,3 +83,5 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
};
}
export default MaintenanceWindowService;
@@ -8,7 +8,6 @@ import type {
PageSpeedDetailsResult,
GamesMap,
} from "@/types/monitor.js";
import { supportsGeoCheck } from "@/types/monitor.js";
import type { GeoContinent } from "@/types/geoCheck.js";
import type {
IChecksRepository,
@@ -227,8 +226,7 @@ export class MonitorService implements IMonitorService {
checksData.monitorType !== "docker" &&
checksData.monitorType !== "port" &&
checksData.monitorType !== "game" &&
checksData.monitorType !== "grpc" &&
checksData.monitorType !== "websocket"
checksData.monitorType !== "grpc"
) {
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
}
@@ -342,7 +340,7 @@ export class MonitorService implements IMonitorService {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
}
if (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) {
if (monitor.type !== "http" || !monitor.geoCheckEnabled) {
return { groupedGeoChecks: [] };
}
+25 -97
View File
@@ -11,32 +11,10 @@ import { canManageRole, type UserRole } from "@/types/user.js";
import bcrypt from "bcryptjs";
import { AppError } from "@/utils/AppError.js";
import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import { IEmailService } from "@/service/infrastructure/emailService.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import jwt from "jsonwebtoken";
type JwtType = typeof jwt;
const SERVICE_NAME = "userService";
export interface IUserService {
issueToken(payload: Partial<User>, appSettings: { jwtTTL?: string; jwtSecret?: string }): string;
registerUser(user: Partial<User>, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>;
createUser(userData: Partial<User>, teamId: string, actorRoles: UserRole[], file: unknown): Promise<User>;
loginUser(email: string, password: string): Promise<{ user: User; token: string }>;
editUser(updates: Partial<User & { newPassword?: string }>, file: unknown, currentUser: User): Promise<User>;
checkSuperadminExists(): Promise<boolean>;
requestRecovery(email: string): Promise<string | false | undefined>;
validateRecovery(recoveryToken: string): Promise<void>;
resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>;
deleteUser(user: User): Promise<void>;
deleteUserById(actor: User, targetUserId: string): Promise<void>;
getAllUsers(): Promise<User[]>;
getUserById(roles: UserRole[], userId: string): Promise<User>;
editUserById(userId: string, patch: Partial<User>): Promise<void>;
setPasswordByUserId(userId: string, password: string): Promise<User>;
}
export class UserService implements IUserService {
class UserService {
static SERVICE_NAME = SERVICE_NAME;
private hashPassword = (password: string): string => {
@@ -44,10 +22,10 @@ export class UserService implements IUserService {
return bcrypt.hashSync(password, salt);
};
private emailService: IEmailService;
private settingsService: ISettingsService;
private logger: ILogger;
private jwt: JwtType;
private emailService: any;
private settingsService: any;
private logger: any;
private jwt: any;
private jobQueue: ISuperSimpleQueue;
private crypto: any;
private monitorsRepository: IMonitorsRepository;
@@ -72,10 +50,10 @@ export class UserService implements IUserService {
teamsRepository,
}: {
crypto: any;
emailService: IEmailService;
settingsService: ISettingsService;
logger: ILogger;
jwt: JwtType;
emailService: any;
settingsService: any;
logger: any;
jwt: any;
jobQueue: ISuperSimpleQueue;
monitorsRepository: IMonitorsRepository;
usersRepository: IUsersRepository;
@@ -142,7 +120,7 @@ export class UserService implements IUserService {
message: "New user created",
service: SERVICE_NAME,
method: "registerUser",
details: { userId: newUser.id },
details: newUser.id,
});
delete newUser.profileImage;
@@ -156,23 +134,20 @@ export class UserService implements IUserService {
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
name: newUser.firstName,
});
if (!html) {
throw new Error("Failed to build welcome email HTML");
}
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: unknown) => {
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => {
this.logger.warn({
message: error instanceof Error ? error.message : "Unknown error",
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error instanceof Error ? error.stack : undefined,
stack: error.stack,
});
});
} catch (error: unknown) {
} catch (error: any) {
this.logger.warn({
message: error instanceof Error ? error.message : "Unknown error",
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error instanceof Error ? error.stack : undefined,
stack: error.stack,
});
}
@@ -206,7 +181,7 @@ export class UserService implements IUserService {
message: "New user created by superadmin",
service: SERVICE_NAME,
method: "createUser",
details: { userId: newUser.id },
details: newUser.id,
});
newUser.profileImage = undefined;
@@ -281,14 +256,6 @@ export class UserService implements IUserService {
email,
url,
});
if (!html) {
throw new AppError({
message: "Failed to build password reset email HTML",
service: SERVICE_NAME,
method: "requestRecovery",
status: 500,
});
}
const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html);
return msgId;
};
@@ -346,65 +313,25 @@ export class UserService implements IUserService {
if (roles.includes("superadmin")) {
// 2. Remove all jobs, delete checks and alerts
if (res && res.length > 0) {
await Promise.all(
res &&
res?.length > 0 &&
(await Promise.all(
res.map(async (monitor) => {
await this.jobQueue.deleteJob(monitor.id);
})
);
}
));
}
// 6. Delete the user by id
await this.usersRepository.deleteById(userId);
};
deleteUserById = async (actor: User, targetUserId: string) => {
if (actor.id === targetUserId) {
throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
}
const targetUser = await this.usersRepository.findById(targetUserId);
if (targetUser.teamId !== actor.teamId) {
throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 });
}
if (targetUser.role.includes("demo")) {
throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 });
}
const actorRoles = actor.role;
const targetRoles = targetUser.role;
// Check actor can manage all of target's roles
for (const targetRole of targetRoles) {
const canManage = actorRoles.some((actorRole) => canManageRole(actorRole, targetRole));
if (!canManage) {
throw new AppError({
message: "You do not have permission to remove this user",
service: SERVICE_NAME,
method: "deleteUserById",
status: 403,
});
}
}
await this.usersRepository.deleteById(targetUserId);
this.logger.info({
message: `User ${targetUserId} deleted by ${actor.id}`,
service: SERVICE_NAME,
method: "deleteUserById",
});
};
getAllUsers = async () => {
return await this.usersRepository.findAll();
};
getUserById = async (roles: any, userId: any) => {
if (!roles.includes("superadmin") && !roles.includes("admin")) {
throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 });
if (!roles.includes("superadmin")) {
throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 });
}
const user = await this.usersRepository.findById(userId);
@@ -421,3 +348,4 @@ export class UserService implements IUserService {
return updatedUser;
};
}
export default UserService;
+6 -25
View File
@@ -1,33 +1,14 @@
// Business services
export * from "@/service/business/checkService.js";
export * from "@/service/business/diagnosticService.js";
export * from "@/service/business/geoChecksService.js";
export * from "@/service/business/incidentService.js";
export * from "@/service/business/inviteService.js";
export * from "@/service/business/maintenanceWindowService.js";
export * from "@/service/business/monitorService.js";
export * from "@/service/business/statusPageService.js";
export * from "@/service/business/userService.js";
// Infrastructure services
export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
export * from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
export * from "@/service/infrastructure/notificationMessageBuilder.js";
export * from "@/service/infrastructure/bufferService.js";
export * from "@/service/infrastructure/emailService.js";
export * from "@/service/infrastructure/globalPingService.js";
export * from "@/service/infrastructure/networkService.js";
export * from "@/service/infrastructure/notificationsService.js";
export * from "@/service/infrastructure/statusService.js";
// Notification providers
export * from "@/service/infrastructure/notificationProviders/discord.js";
export * from "@/service/infrastructure/notificationProviders/email.js";
export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js";
export * from "@/service/infrastructure/notificationProviders/matrix.js";
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
export * from "@/service/infrastructure/notificationProviders/matrix.js";
export * from "@/service/infrastructure/notificationProviders/slack.js";
export * from "@/service/infrastructure/notificationProviders/webhook.js";
// System services
export * from "@/service/system/settingsService.js";
export * from "@/service/infrastructure/notificationsService.js";
export * from "@/service/infrastructure/statusService.js";
export * from "@/service/infrastructure/notificationProviders/webhook.js";
export * from "@/service/infrastructure/notificationProviders/matrix.js";
export * from "@/service/business/statusPageService.js";
@@ -2,7 +2,6 @@ import { IMonitorsRepository } from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import Scheduler from "super-simple-scheduler";
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { supportsGeoCheck } from "@/types/monitor.js";
const SERVICE_NAME = "JobQueue";
type QueueJobFailure = {
@@ -54,7 +53,7 @@ export interface ISuperSimpleQueue {
obliterate(): Promise<void>;
}
export class SuperSimpleQueue implements ISuperSimpleQueue {
class SuperSimpleQueue implements ISuperSimpleQueue {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -126,13 +125,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
data: monitor,
});
// Return early if we don't need geo checks
if (!supportsGeoCheck(monitor.type)) {
return;
}
// Add geo check job if enabled for HTTP monitors
if (monitor.geoCheckEnabled) {
if (monitor.geoCheckEnabled && monitor.type === "http") {
this.scheduler.addJob({
id: `${monitorId}-geo`,
template: "geo-check-job",
@@ -181,7 +175,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
// Handle geo check job lifecycle
const geoJobId = `${monitor.id}-geo`;
if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) {
if (monitor.geoCheckEnabled && monitor.type === "http") {
// Check if geo job exists
const existingGeoJob = await this.scheduler.getJob(geoJobId);
if (existingGeoJob) {
@@ -295,3 +289,5 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
});
};
}
export default SuperSimpleQueue;
@@ -1,9 +1,10 @@
const SERVICE_NAME = "JobQueueHelper";
import type { Monitor } from "@/types/monitor.js";
import { supportsGeoCheck } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js";
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
import type { StatusChangeResult } from "@/types/index.js";
import IncidentService from "@/service/business/incidentService.js";
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
import {
IMaintenanceWindowsRepository,
IMonitorsRepository,
@@ -14,7 +15,7 @@ import {
IGeoChecksRepository,
} from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import { IBufferService } from "@/service/index.js";
import { IBufferService } from "../bufferService.js";
export interface ISuperSimpleQueueHelper {
readonly serviceName: string;
@@ -38,7 +39,7 @@ export interface MonitorActionDecision {
};
}
export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
static SERVICE_NAME = SERVICE_NAME;
private logger: any;
@@ -281,18 +282,19 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
}
if (!monitor.geoCheckEnabled) {
return;
}
if (!supportsGeoCheck(monitor.type)) {
if (monitor.type !== "http") {
this.logger.debug({
message: `Monitor ${monitorId} type does not support geo checks, skipping`,
message: `Monitor ${monitorId} is not HTTP type, skipping geo check`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
if (!monitor.geoCheckEnabled) {
return;
}
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
this.logger.warn({
message: `No geo check locations configured for monitor ${monitorId}`,
@@ -412,3 +414,5 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
return decision;
}
}
export default SuperSimpleQueueHelper;
@@ -15,7 +15,7 @@ export interface IBufferService {
flushGeoBuffer(): Promise<void>;
}
export class BufferService implements IBufferService {
class BufferService implements IBufferService {
static SERVICE_NAME = SERVICE_NAME;
private BUFFER_TIMEOUT: number;
private logger: ILogger;
@@ -177,3 +177,5 @@ export class BufferService implements IBufferService {
}
}
}
export default BufferService;
@@ -23,7 +23,7 @@ export interface IEmailService {
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
}
export class EmailService implements IEmailService {
class EmailService implements IEmailService {
static SERVICE_NAME = SERVICE_NAME;
private settingsService: ISettingsService;
@@ -94,7 +94,7 @@ export class EmailService implements IEmailService {
if (!mjml) {
throw new Error(`Template ${template} not found`);
}
const html = await this.mjml2html(mjml);
const html = this.mjml2html(mjml);
return html.html;
} catch (error: unknown) {
this.logger.error({
@@ -178,3 +178,5 @@ export class EmailService implements IEmailService {
}
};
}
export default EmailService;
@@ -1,6 +1,4 @@
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
import { supportsGeoCheck } from "@/types/monitor.js";
import { MonitorType } from "@/types/index.js";
import type { ILogger } from "@/utils/logger.js";
import got from "got";
@@ -10,7 +8,7 @@ const POLL_INTERVAL_MS = 2000;
const MAX_POLL_TIMEOUT_MS = 30000;
interface GlobalPingMeasurementRequest {
type: MonitorType;
type: "http";
target: string;
locations: Array<{ continent: GeoContinent }>;
limit: number;
@@ -46,26 +44,17 @@ interface GlobalPingProbeResult {
firstByte: number;
download: number;
};
stats?: {
min: number;
max: number;
avg: number;
total: number;
loss: number;
rcv: number;
drop: number;
};
rawOutput?: string;
};
}
export interface IGlobalPingService {
readonly serviceName: string;
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null>;
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
}
export class GlobalPingService implements IGlobalPingService {
class GlobalPingService implements IGlobalPingService {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
@@ -78,16 +67,13 @@ export class GlobalPingService implements IGlobalPingService {
return GlobalPingService.SERVICE_NAME;
}
async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> {
async createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null> {
try {
if (!supportsGeoCheck(monitorType)) {
throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`);
}
// GlobalPing API expects target without protocol (http:// or https://)
const cleanTarget = url.replace(/^https?:\/\//, "");
const requestBody: GlobalPingMeasurementRequest = {
type: monitorType,
type: "http",
target: cleanTarget,
locations: locations.map((continent) => ({ continent })),
limit: locations.length,
@@ -102,9 +88,10 @@ export class GlobalPingService implements IGlobalPingService {
const measurementId = response.body.id;
this.logger.debug({
message: `Created GlobalPing measurement: ${measurementId} for target: ${cleanTarget}`,
message: `Created GlobalPing measurement: ${measurementId}`,
service: SERVICE_NAME,
method: "createMeasurement",
details: { measurementId, url, locations },
});
return measurementId;
@@ -178,7 +165,8 @@ export class GlobalPingService implements IGlobalPingService {
const successfulResults: GeoCheckResult[] = [];
for (const probeResult of probeResults) {
if (probeResult.result.status !== "finished") {
// Skip failed or timeout results
if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) {
continue;
}
@@ -192,38 +180,23 @@ export class GlobalPingService implements IGlobalPingService {
latitude: probeResult.probe.latitude,
};
// HTTP results have statusCode and timings, ping results have stats
if (probeResult.result.statusCode && probeResult.result.timings) {
const timings: GeoCheckTimings = {
total: probeResult.result.timings.total,
dns: probeResult.result.timings.dns,
tcp: probeResult.result.timings.tcp,
tls: probeResult.result.timings.tls,
firstByte: probeResult.result.timings.firstByte,
download: probeResult.result.timings.download,
};
const timings: GeoCheckTimings = {
total: probeResult.result.timings.total,
dns: probeResult.result.timings.dns,
tcp: probeResult.result.timings.tcp,
tls: probeResult.result.timings.tls,
firstByte: probeResult.result.timings.firstByte,
download: probeResult.result.timings.download,
};
successfulResults.push({
location,
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
statusCode: probeResult.result.statusCode,
timings,
});
} else if (probeResult.result.stats) {
successfulResults.push({
location,
status: probeResult.result.stats.loss === 0,
statusCode: probeResult.result.stats.loss === 0 ? 200 : 5000,
timings: {
total: probeResult.result.stats.avg,
dns: 0,
tcp: 0,
tls: 0,
firstByte: 0,
download: 0,
},
});
}
const result: GeoCheckResult = {
location,
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
statusCode: probeResult.result.statusCode,
timings,
};
successfulResults.push(result);
}
return successfulResults;
@@ -233,3 +206,5 @@ export class GlobalPingService implements IGlobalPingService {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
export default GlobalPingService;
@@ -1,45 +0,0 @@
import { Monitor } from "@/types/monitor.js";
import jmespath from "jmespath";
type JmesPath = typeof jmespath;
export class AdvancedMatcher {
constructor(private jmespath: JmesPath) {}
private compare(actual: unknown, expected: string, method?: string): boolean {
if (method === "equal") return String(actual) === expected;
if (method === "include") return String(actual).includes(expected);
if (method === "regex") return new RegExp(expected).test(String(actual));
return String(actual) === expected; // Default
}
validate<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: any } {
const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
if (!useAdvancedMatching) return { ok: true, message: "Success" };
let dataToValidate = payload;
if (jsonPath) {
try {
dataToValidate = this.jmespath.search(payload, jsonPath);
} catch {
return { ok: false, message: "Error evaluating JSON path" };
}
}
if (expectedValue) {
const ok = this.compare(dataToValidate, expectedValue, matchMethod);
return {
ok,
message: ok ? "Success" : "Expected value did not match",
extracted: dataToValidate,
};
}
const isFalsey = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null;
return {
ok: !isFalsey,
message: !isFalsey ? "Success" : "Extracted value is falsey",
extracted: dataToValidate,
};
}
}
@@ -1,160 +0,0 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { DockerStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { ILogger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js";
import Dockerode from "dockerode";
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
type DockerodeType = typeof Dockerode;
const SERVICE_NAME = "DockerProvider";
export interface DockerError extends Error {
statusCode?: number;
reason?: string;
json?: { message?: string };
}
export class DockerProvider implements IStatusProvider<DockerStatusPayload> {
readonly type = "docker";
private docker: Dockerode;
constructor(
private logger: ILogger,
private DockerLib: DockerodeType
) {
this.docker = new this.DockerLib();
}
supports(type: MonitorType): boolean {
return type === "docker";
}
private isDockerError(error: unknown): error is DockerError {
return error instanceof Error && ("statusCode" in error || "reason" in error || "json" in error);
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<DockerStatusPayload>> {
const { url: containerInput } = monitor;
try {
if (!containerInput) {
throw new Error("Container name or ID is required for Docker monitor");
}
const containers = await this.docker.listContainers({ all: true });
const normalizedInput = containerInput.replace(/^\/+/, "").toLowerCase();
// Priority-based matching to avoid ambiguity:
// 1. Exact full ID match (64-char)
const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
// 2. Exact container name match (case-insensitive)
const exactNameMatch = containers.find((c) =>
c.Names.some((name: string) => {
const cleanName = name.replace(/^\/+/, "").toLowerCase();
return cleanName === normalizedInput;
})
);
// 3. Partial ID match (fallback for backwards compatibility)
const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
// Select container based on priority
const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
// Handle no match
if (!targetContainer) {
this.logger.warn({
message: `No container found for "${monitor.url}".`,
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url },
});
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: 404,
message: "Docker container not found",
responseTime: 0,
payload: null,
};
}
// 5. Handle Ambiguity Check
const matchTypes: string[] = [];
if (exactIdMatch) matchTypes.push("exact ID");
if (exactNameMatch) matchTypes.push("exact name");
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
if (matchTypes.length > 1) {
const message = `Ambiguous container match for "${containerInput}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
this.logger.warn({
message,
service: SERVICE_NAME,
method: "handle",
details: { url: containerInput },
});
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: NETWORK_ERROR,
message,
responseTime: 0,
payload: null,
};
}
// 6. Inspect Container Status
const container = this.docker.getContainer(targetContainer.Id);
const { response, responseTime, error } = await timeRequest(() => container.inspect());
if (error) {
let message = "Failed to fetch Docker container information";
let code = NETWORK_ERROR;
if (this.isDockerError(error)) {
code = error.statusCode ?? NETWORK_ERROR;
message = error.json?.message ?? error.reason ?? error.message;
} else if (error instanceof Error) {
message = error.message;
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: code,
message: message,
responseTime,
payload: null,
};
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: response?.State?.Status === "running",
code: 200,
message: "Docker container status fetched successfully",
responseTime,
payload: response as unknown as DockerStatusPayload,
};
} catch (err: unknown) {
throw new AppError({
message: err instanceof Error ? err.message : "Error performing Docker request",
service: SERVICE_NAME,
method: "handle",
details: { url: containerInput },
});
}
}
}
@@ -1,78 +0,0 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { GameStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { GameDig } from "gamedig";
import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
import { ILogger } from "@/utils/logger.js";
type GameDigType = typeof GameDig;
const SERVICE_NAME = "GameProvider";
export class GameProvider implements IStatusProvider<GameStatusPayload> {
readonly type = "game";
constructor(
private logger: ILogger,
private gameDig: GameDigType
) {}
supports(type: MonitorType): boolean {
return type === "game";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<GameStatusPayload>> {
const { url, port, gameId, id, teamId, type } = monitor;
// Clean the host in case a full URL was passed
const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0] || "";
try {
const state = await this.gameDig
.query({
type: gameId ?? "unknown",
host: host,
port: port ?? 0,
})
.catch((error: unknown) => {
this.logger.warn({
message: error instanceof Error ? error.message : String(error),
service: SERVICE_NAME,
method: "handle",
details: { host, port, gameId },
});
return undefined;
});
if (!state) {
return {
monitorId: id,
teamId: teamId,
type: type,
status: false,
code: NETWORK_ERROR,
message: "No response from game server",
responseTime: 0,
payload: null,
};
}
return {
monitorId: id,
teamId: teamId,
type: type,
status: true,
code: 200,
message: "Success",
responseTime: state.ping ?? 0,
payload: state,
};
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing game server check",
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId },
});
}
}
}
@@ -1,180 +0,0 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { GrpcStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { timeRequest } from "@/service/infrastructure/network/utils.js";
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import path from "path";
import { fileURLToPath } from "url";
type GrpcType = typeof grpc;
type ProtoLoaderType = typeof protoLoader;
const SERVICE_NAME = "GrpcProvider";
export class GrpcProvider implements IStatusProvider<GrpcStatusPayload> {
readonly type = "grpc";
constructor(
private grpc: GrpcType,
private protoLoader: ProtoLoaderType
) {}
supports(type: MonitorType): boolean {
return type === "grpc";
}
private getGrpcStatusName(code: number): string {
const statusNames: Record<number, string> = {
0: "OK",
1: "CANCELLED",
2: "UNKNOWN",
3: "INVALID_ARGUMENT",
4: "DEADLINE_EXCEEDED",
5: "NOT_FOUND",
6: "ALREADY_EXISTS",
7: "PERMISSION_DENIED",
8: "RESOURCE_EXHAUSTED",
9: "FAILED_PRECONDITION",
10: "ABORTED",
11: "OUT_OF_RANGE",
12: "UNIMPLEMENTED",
13: "INTERNAL",
14: "UNAVAILABLE",
15: "DATA_LOSS",
16: "UNAUTHENTICATED",
};
return statusNames[code] || "UNKNOWN";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<GrpcStatusPayload>> {
try {
const { id, teamId, type, url, port, ignoreTlsErrors } = monitor;
const grpcServiceName = monitor.grpcServiceName || "";
if (!url) {
throw new AppError({ message: "Monitor host is required", service: SERVICE_NAME, method: "handle" });
}
if (!port) {
throw new AppError({ message: "Monitor port is required", service: SERVICE_NAME, method: "handle" });
}
const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0];
const target = `${host}:${port}`;
const currentFilePath = fileURLToPath(import.meta.url);
const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto");
const packageDefinition = this.protoLoader.loadSync(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as {
grpc: {
health: {
v1: {
Health: new (
target: string,
credentials: unknown
) => {
Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void;
close: () => void;
};
};
};
};
};
const healthService = grpcObject.grpc.health.v1.Health;
let credentials;
if (ignoreTlsErrors) {
credentials = this.grpc.credentials.createSsl(null, null, null, {
checkServerIdentity: () => undefined,
});
} else {
credentials = this.grpc.credentials.createInsecure();
}
const client = new healthService(target, credentials);
const TIMEOUT_MS = 10000;
const deadline = new Date(Date.now() + TIMEOUT_MS);
const { response, responseTime, error } = await timeRequest<GrpcStatusPayload>(() => {
return new Promise<GrpcStatusPayload>((resolve, reject) => {
client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => {
client.close();
if (err) {
const grpcErr = err as { code?: number; details?: string; message?: string };
const payload: GrpcStatusPayload = {
grpcStatusCode: grpcErr.code ?? -1,
grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1),
serviceName: grpcServiceName,
servingStatus: "UNKNOWN",
};
const grpcError = new AppError({
message: grpcErr.details || grpcErr.message || "gRPC error",
service: SERVICE_NAME,
method: "handle",
}) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
grpcError.grpcPayload = payload;
grpcError.grpcCode = grpcErr.code;
reject(grpcError);
return;
}
const resp = response as { status?: string } | undefined;
const servingStatus = resp?.status ?? "UNKNOWN";
resolve({
grpcStatusCode: 0,
grpcStatusName: "OK",
serviceName: grpcServiceName,
servingStatus,
});
});
});
});
if (error) {
const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
const payload = grpcError.grpcPayload;
return {
monitorId: id,
teamId: teamId,
type: type,
status: false,
code: grpcError.grpcCode ?? 5000,
message: grpcError.message ?? "gRPC health check failed",
responseTime,
payload: payload ?? null,
};
}
const grpcPayload = response as GrpcStatusPayload;
const isServing = grpcPayload.servingStatus === "SERVING";
return {
monitorId: id,
teamId: teamId,
type: type,
status: isServing,
code: isServing ? 200 : 5000,
message: isServing ? `gRPC service healthy (${grpcPayload.servingStatus})` : `gRPC service unhealthy (${grpcPayload.servingStatus})`,
responseTime,
payload: grpcPayload,
};
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing gRPC health check",
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName },
});
}
}
}
@@ -1,29 +0,0 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { AppError } from "@/utils/AppError.js";
export class HardwareProvider implements IStatusProvider<HardwareStatusPayload> {
readonly type = "hardware";
constructor(private httpProvider: HttpProvider) {}
supports(type: MonitorType) {
return type === "hardware";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<HardwareStatusPayload>> {
const { url } = monitor;
try {
if (!url) throw new Error("URL is required for Hardware monitor");
return await this.httpProvider.handle<HardwareStatusPayload>(monitor);
} catch (err: unknown) {
throw new AppError({
message: err instanceof Error ? err.message : "Error performing Hardware request",
service: "HardwareProvider",
method: "handle",
details: { url: monitor.url },
});
}
}
}
@@ -1,121 +0,0 @@
import { type Got, HTTPError, RequestError } from "got";
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { HttpStatusPayload } from "@/types/network.js";
import { MonitorStatusResponse } from "@/types/network.js";
import { Agent as HttpsAgent } from "https";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
import CacheableLookup from "cacheable-lookup";
export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
readonly type = "http";
constructor(
private got: Got,
private advancedMatcher: AdvancedMatcher
) {
const cacheable = new CacheableLookup();
this.got = got.extend({
dnsCache: cacheable,
timeout: {
request: 30000,
},
retry: { limit: 1 },
});
}
supports(type: MonitorType) {
return type === "http";
}
private handleHttpError<T>(error: unknown, monitor: Monitor): MonitorStatusResponse<T> {
if (error instanceof HTTPError || error instanceof RequestError) {
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: error.response?.statusCode ?? NETWORK_ERROR,
message: error.message,
responseTime: error.timings?.phases?.total ?? 0,
timings: error.timings,
payload: null as T,
};
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: NETWORK_ERROR,
message: error instanceof Error ? error.message : String(error),
responseTime: 0,
payload: null as T,
};
}
async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
const { url, secret, jsonPath, ignoreTlsErrors } = monitor;
if (!url) {
throw new Error("URL is required for HTTP monitor");
}
const options: Record<string, unknown> = {
headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined,
};
options.agent = {
https: new HttpsAgent({ rejectUnauthorized: !ignoreTlsErrors }),
};
try {
const response = await this.got<string>(url, options);
const contentType = response.headers["content-type"] || "";
const isJson = contentType.includes("application/json");
if (jsonPath && !isJson) {
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: response.statusCode,
message: "Response is not JSON",
responseTime: response.timings.phases.total ?? 0,
timings: response.timings,
payload: response.body as unknown as T,
};
}
let payload: T;
if (isJson) {
try {
payload = JSON.parse(response.body) as T;
} catch {
payload = response.body as unknown as T;
}
} else {
payload = response.body as unknown as T;
}
const matchResult = this.advancedMatcher.validate<T>(payload, monitor);
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: response.ok && matchResult.ok,
code: response.statusCode,
message: matchResult.ok ? (response.statusMessage ?? "OK") : matchResult.message,
responseTime: response.timings.phases.total ?? 0,
timings: response.timings,
payload,
extracted: matchResult.extracted,
};
} catch (error: unknown) {
return this.handleHttpError(error, monitor);
}
}
}
@@ -1,8 +0,0 @@
import { Monitor, MonitorType } from "@/types/monitor.js";
import { MonitorStatusResponse } from "@/types/network.js";
export interface IStatusProvider<T> {
type: string;
supports: (type: MonitorType) => boolean;
handle(monitor: Monitor): Promise<MonitorStatusResponse<T>>;
}
@@ -1,55 +0,0 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js";
export class PageSpeedProvider implements IStatusProvider<PageSpeedStatusPayload> {
readonly type = "pagespeed";
constructor(
private httpProvider: HttpProvider,
private settingsService: ISettingsService,
private logger: ILogger
) {}
supports(type: MonitorType) {
return type === "pagespeed";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
const { url } = monitor;
try {
if (!url) throw new Error("URL is required for PageSpeed monitor");
const dbSettings = await this.settingsService.getDBSettings();
const apiKey = dbSettings?.pagespeedApiKey;
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(
url
)}&category=seo&category=accessibility&category=best-practices&category=performance`;
if (apiKey) {
pageSpeedUrl += `&key=${apiKey}`;
} else {
this.logger.warn({
message: "PageSpeed API key not found, performance may be throttled",
service: "PageSpeedProvider",
method: "handle",
details: { url },
});
}
return await this.httpProvider.handle<PageSpeedStatusPayload>({
...monitor,
url: pageSpeedUrl,
});
} catch (err: unknown) {
throw new AppError({
message: err instanceof Error ? err.message : "Error performing PageSpeed request",
service: "PageSpeedProvider",
method: "handle",
details: { url: monitor.url },
});
}
}
}
@@ -1,66 +0,0 @@
import { PingStatusPayload } from "@/types/network.js";
import { IStatusProvider } from "./IStatusProvider.js";
import { MonitorType, Monitor } from "@/types/monitor.js";
import { MonitorStatusResponse } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js";
import ping from "ping";
import { timeRequest } from "@/service/infrastructure/network/utils.js";
const SERVICE_NAME = "PingProvider";
type Ping = typeof ping;
export class PingProvider implements IStatusProvider<PingStatusPayload> {
readonly type = "ping";
constructor(private ping: Ping) {}
supports(type: MonitorType): boolean {
return type === "ping";
}
private sanitizeHost(url: string): string {
return url
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "")
.replace(/:.*/, "");
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PingStatusPayload>> {
try {
if (!monitor.url) {
throw new Error("URL is required for ping monitor");
}
const sanitizedHost = this.sanitizeHost(monitor.url);
const { response, error } = await timeRequest<PingStatusPayload>(() => this.ping.promise.probe(sanitizedHost));
if (error) {
throw error;
}
if (!response) {
throw new Error(`No response from ping for host: ${sanitizedHost}`);
}
const responseTime = typeof response.time === "number" ? response.time : parseFloat(String(response.time)) || 0;
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: response.alive ?? false,
code: response.alive ? 200 : 5000,
message: response.alive ? "Success" : "Ping failed",
responseTime,
payload: response,
};
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
throw new AppError({
message,
service: SERVICE_NAME,
method: "handle",
});
}
}
}
@@ -1,90 +0,0 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { PortStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
import * as net from "net";
type NetType = typeof net;
const SERVICE_NAME = "PortProvider";
export class PortProvider implements IStatusProvider<PortStatusPayload> {
readonly type = "port";
constructor(private net: NetType) {}
supports(type: MonitorType): boolean {
return type === "port";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PortStatusPayload>> {
try {
const { url, port } = monitor;
if (!url || !port) {
throw new Error("URL and port are required for port monitoring");
}
const { responseTime, error } = await timeRequest(async () => {
return new Promise((resolve, reject) => {
const socket = this.net.createConnection(
{
host: url,
port,
},
() => {
socket.end();
socket.destroy();
resolve({ success: true });
}
);
socket.setTimeout(5000);
socket.on("timeout", () => {
socket.destroy();
reject(new Error("Connection timeout"));
});
socket.on("error", (err: unknown) => {
socket.destroy();
reject(err);
});
});
});
if (error) {
const errorMessage = error instanceof Error ? error.message : "Port check failed";
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: NETWORK_ERROR,
message: errorMessage,
responseTime: responseTime,
timings: undefined,
payload: { success: false },
};
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: true,
code: 200,
message: "Port check successful",
responseTime: responseTime,
timings: undefined,
payload: { success: true },
};
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing port check",
status: 500,
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url, port: monitor.port },
});
}
}
}
@@ -1,94 +0,0 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
import type WebSocket from "ws";
type WebSocketConstructor = typeof WebSocket;
const SERVICE_NAME = "WebSocketProvider";
const TIMEOUT_MS = 10000;
export class WebSocketProvider implements IStatusProvider<WebSocketStatusPayload> {
readonly type = "websocket";
constructor(private WS: WebSocketConstructor) {}
supports(type: MonitorType): boolean {
return type === "websocket";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<WebSocketStatusPayload>> {
try {
const { url } = monitor;
if (!url) {
throw new Error("URL is required for WebSocket monitoring");
}
const { responseTime, error } = await timeRequest(async () => {
return new Promise<{ connected: boolean }>((resolve, reject) => {
const options: WebSocket.ClientOptions = {};
if (monitor.ignoreTlsErrors) {
options.rejectUnauthorized = false;
}
const ws = new this.WS(url, options);
const timeout = setTimeout(() => {
ws.close();
reject(new Error("WebSocket connection timeout"));
}, TIMEOUT_MS);
ws.on("open", () => {
clearTimeout(timeout);
ws.close();
resolve({ connected: true });
});
ws.on("error", (err: unknown) => {
clearTimeout(timeout);
ws.close();
reject(err);
});
});
});
if (error) {
const errorMessage = error instanceof Error ? error.message : "WebSocket check failed";
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: NETWORK_ERROR,
message: errorMessage,
responseTime: responseTime,
timings: undefined,
payload: { connected: false },
};
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: true,
code: 200,
message: "WebSocket check successful",
responseTime: responseTime,
timings: undefined,
payload: { connected: true },
};
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing WebSocket check",
status: 500,
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url },
});
}
}
}
@@ -1,14 +0,0 @@
export const timeRequest = async <T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> => {
const start = process.hrtime.bigint();
try {
const response = await operation();
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response, responseTime: elapsedMs, error: null };
} catch (error) {
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response: null, responseTime: elapsedMs, error };
}
};
export const NETWORK_ERROR = 5000;
export const PING_ERROR = 5001;
@@ -1,14 +1,57 @@
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
import { HTTPError, RequestError } from "got";
import type { Got, Response } from "got";
import type {
Monitor,
MonitorStatusResponse,
GrpcStatusPayload,
PageSpeedStatusPayload,
HttpStatusPayload,
HardwareStatusPayload,
PingStatusPayload,
DockerStatusPayload,
PortStatusPayload,
GameStatusPayload,
} from "@/types/index.js";
import type { AxiosStatic } from "axios";
import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
import path from "path";
import { fileURLToPath } from "url";
import CacheableLookup from "cacheable-lookup";
import { ISettingsService } from "../system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import { IStatusProvider } from "./network/IStatusProvider.js";
const SERVICE_NAME = "NetworkService";
type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
interface BuildStatusResponseArgs<T> {
monitor: Monitor;
response?: Response<T> | null;
error?: Error | RequestError | HTTPError | null;
payload?: T | null;
jsonPath?: string;
matchMethod?: MonitorStatusResponse["matchMethod"];
expectedValue?: string;
extracted?: unknown;
overrides?: MonitorStatusResponseOverrides<T>;
}
export interface INetworkService {
readonly serviceName: string;
requestStatus(monitor: Monitor): Promise<MonitorStatusResponse<unknown>>;
requestStatus(
monitor: Monitor
): Promise<
MonitorStatusResponse<
| PingStatusPayload
| HttpStatusPayload
| PageSpeedStatusPayload
| HardwareStatusPayload
| DockerStatusPayload
| PortStatusPayload
| GameStatusPayload
| GrpcStatusPayload
>
>;
requestWebhook(
type: string,
url: string,
@@ -23,32 +66,798 @@ export interface INetworkService {
}>;
}
export class NetworkService implements INetworkService {
class NetworkService implements INetworkService {
static SERVICE_NAME = SERVICE_NAME;
private TYPE_PING: string;
private TYPE_HTTP: string;
private TYPE_PAGESPEED: string;
private TYPE_HARDWARE: string;
private TYPE_DOCKER: string;
private TYPE_PORT: string;
private TYPE_GAME: string;
private TYPE_GRPC: string;
private SERVICE_NAME: string;
private NETWORK_ERROR: number;
private PING_ERROR: number;
private axios: AxiosStatic;
private got: Got;
private https: typeof import("https");
private jmespath: { search: (data: unknown, expression: string) => unknown };
private GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> };
private ping: typeof import("ping");
private logger: ILogger;
private Docker: typeof import("dockerode");
private net: typeof import("net");
private settingsService: ISettingsService;
private grpc: typeof import("@grpc/grpc-js");
private protoLoader: typeof import("@grpc/proto-loader");
private buildStatusResponse = <T>({
monitor,
response,
error,
payload,
jsonPath,
matchMethod,
expectedValue,
extracted,
overrides,
}: BuildStatusResponseArgs<T>): MonitorStatusResponse<T> => {
if (error) {
const statusResponse: MonitorStatusResponse<T> = {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: this.NETWORK_ERROR,
message: error.message ?? "Network error",
responseTime: 0,
timings: undefined,
jsonPath,
matchMethod,
expectedValue,
extracted,
payload,
};
if (error instanceof HTTPError || error instanceof RequestError) {
statusResponse.code = error?.response?.statusCode ?? this.NETWORK_ERROR;
statusResponse.message = error.message;
statusResponse.responseTime = error.timings?.phases?.total ?? 0;
statusResponse.timings = error.timings;
}
return { ...statusResponse, ...(overrides ?? {}) } as MonitorStatusResponse<T>;
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: response?.ok ?? false,
code: response?.statusCode ?? this.NETWORK_ERROR,
message: response?.statusMessage ?? "",
responseTime: response?.timings?.phases?.total ?? 0,
timings: response?.timings,
payload: payload ?? response?.body,
jsonPath,
matchMethod,
expectedValue,
extracted,
...(overrides ?? {}),
} as MonitorStatusResponse<T>;
};
constructor(
axios: AxiosStatic,
got: Got,
https: typeof import("https"),
jmespath: { search: (data: unknown, expression: string) => unknown },
GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> },
ping: typeof import("ping"),
logger: ILogger,
private providers: IStatusProvider<unknown>[]
Docker: typeof import("dockerode"),
net: typeof import("net"),
settingsService: ISettingsService,
grpc: typeof import("@grpc/grpc-js"),
protoLoader: typeof import("@grpc/proto-loader")
) {
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
this.TYPE_HARDWARE = "hardware";
this.TYPE_DOCKER = "docker";
this.TYPE_PORT = "port";
this.TYPE_GAME = "game";
this.TYPE_GRPC = "grpc";
this.SERVICE_NAME = SERVICE_NAME;
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
this.axios = axios;
this.https = https;
this.jmespath = jmespath;
this.GameDig = GameDig;
this.ping = ping;
this.logger = logger;
this.Docker = Docker;
this.net = net;
this.settingsService = settingsService;
this.grpc = grpc;
this.protoLoader = protoLoader;
const cacheable = new CacheableLookup();
this.got = got.extend({
dnsCache: cacheable,
timeout: {
request: 30000,
},
retry: { limit: 1 },
});
}
get serviceName(): string {
return NetworkService.SERVICE_NAME;
}
// Main entry point
async requestStatus(monitor: Monitor) {
const provider = this.providers.find((p) => p.supports(monitor.type));
if (!provider) {
return this.handleUnsupportedType(monitor.type);
// Helper functions
private async timeRequest<T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> {
const start = process.hrtime.bigint();
try {
const response = await operation();
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response, responseTime: elapsedMs, error: null };
} catch (error) {
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response: null, responseTime: elapsedMs, error };
}
return provider.handle(monitor);
}
// Main entry point
async requestStatus(
monitor: Monitor
): Promise<
MonitorStatusResponse<
| PingStatusPayload
| HttpStatusPayload
| PageSpeedStatusPayload
| HardwareStatusPayload
| DockerStatusPayload
| PortStatusPayload
| GameStatusPayload
| GrpcStatusPayload
>
> {
const type = monitor?.type || "unknown";
switch (type) {
case this.TYPE_PING:
return await this.requestPing(monitor);
case this.TYPE_HTTP:
return await this.requestHttp<HttpStatusPayload>(monitor);
case this.TYPE_PAGESPEED:
return await this.requestPageSpeed(monitor);
case this.TYPE_HARDWARE:
return await this.requestHardware(monitor);
case this.TYPE_DOCKER:
return await this.requestDocker(monitor);
case this.TYPE_PORT:
return await this.requestPort(monitor);
case this.TYPE_GAME:
return await this.requestGame(monitor);
case this.TYPE_GRPC:
return await this.requestGrpc(monitor);
default:
return this.handleUnsupportedType(type);
}
}
private async requestPing(monitor: Monitor): Promise<MonitorStatusResponse<PingStatusPayload>> {
try {
if (!monitor?.url) {
throw new Error("Monitor URL is required");
}
const rawUrl = monitor.url;
const sanitizedHost = rawUrl
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "")
.replace(/:.*/, "");
const { response, error } = await this.timeRequest(() => this.ping.promise.probe(sanitizedHost));
if (!response) {
if (error) {
throw error;
}
throw new Error("Ping failed - no result returned");
}
const pingResponse = this.buildStatusResponse<PingStatusPayload>({
monitor,
payload: response as PingStatusPayload,
overrides: {
status: (response as { alive?: boolean })?.alive ?? false,
code: 200,
message: "Success",
responseTime: (response as { time?: number })?.time ?? 0,
payload: response as PingStatusPayload,
} as MonitorStatusResponseOverrides<PingStatusPayload>,
});
return pingResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing ping",
service: this.SERVICE_NAME,
method: "requestPing",
details: { url: monitor.url },
});
}
}
private async requestHttp<T = unknown>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue, basicAuthUser, basicAuthPassword } = monitor;
const httpResponse = this.buildStatusResponse<T>({
monitor,
overrides: {
status: false,
code: this.NETWORK_ERROR,
message: "Request not executed",
},
});
try {
if (!url) {
throw new Error("Monitor URL is required");
}
const headers: Record<string, string> = {};
if (secret) {
headers["Authorization"] = `Bearer ${secret}`;
} else if (basicAuthUser) {
const credentials = Buffer.from(`${basicAuthUser}:${basicAuthPassword || ""}`).toString("base64");
headers["Authorization"] = `Basic ${credentials}`;
}
const config: Record<string, unknown> = {
headers: Object.keys(headers).length > 0 ? headers : undefined,
};
if (ignoreTlsErrors) {
config.agent = {
https: new this.https.Agent({
rejectUnauthorized: false,
}),
};
}
const response = await this.got(url, config);
let payload: T | string | undefined;
const contentType = response.headers["content-type"];
if (contentType && contentType.includes("application/json")) {
try {
payload = JSON.parse(response.body);
} catch {
payload = response.body;
}
} else {
payload = response.body;
}
Object.assign(httpResponse, {
code: response.statusCode,
status: response.ok,
message: response.statusMessage ?? "",
responseTime: response.timings.phases.total || 0,
payload,
timings: response.timings,
});
if (!useAdvancedMatching) {
return httpResponse;
}
if (expectedValue && !jsonPath) {
let ok = false;
if (matchMethod === "equal") ok = payload === expectedValue;
if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue);
if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload);
if (ok === true) {
return httpResponse;
} else {
httpResponse.code = 500;
httpResponse.status = false;
httpResponse.message = "Expected value did not match";
return httpResponse;
}
}
if (jsonPath) {
const contentType = response.headers["content-type"];
const isJson = contentType?.includes("application/json");
if (!isJson) {
httpResponse.status = false;
httpResponse.message = "Response is not JSON";
return httpResponse;
}
try {
const extracted = this.jmespath.search(payload, jsonPath);
if (expectedValue) {
let ok = false;
if (matchMethod === "equal") ok = extracted === expectedValue;
if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue);
if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted);
if (ok) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
httpResponse.message = "Expected value did not match";
httpResponse.extracted = extracted;
return httpResponse;
}
} else {
const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null;
if (!isFalsey) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
httpResponse.message = "Expected value did not match";
httpResponse.extracted = extracted;
return httpResponse;
}
}
} catch {
httpResponse.status = false;
httpResponse.message = "Error evaluating JSON path";
return httpResponse;
}
}
return httpResponse;
} catch (err: unknown) {
if (err instanceof HTTPError || err instanceof RequestError) {
httpResponse.code = err.response?.statusCode || this.NETWORK_ERROR;
httpResponse.status = false;
httpResponse.message = String(err.response?.statusCode) || err.message;
httpResponse.responseTime = err.timings?.phases?.total || 0;
httpResponse.payload = null;
httpResponse.timings = err.timings || undefined;
return httpResponse;
}
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing HTTP request",
service: this.SERVICE_NAME,
method: "requestHttp",
details: { url: monitor.url },
});
}
}
private async requestPageSpeed(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
try {
const url = monitor.url;
if (!url) {
throw new Error("Monitor URL is required");
}
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
const dbSettings = await this.settingsService.getDBSettings();
if (dbSettings?.pagespeedApiKey) {
pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
} else {
this.logger.warn({
message: "PageSpeed API key not found, job not executed",
service: this.SERVICE_NAME,
method: "requestPagespeed",
details: { url },
});
}
return await this.requestHttp<PageSpeedStatusPayload>({
...monitor,
url: pageSpeedUrl,
});
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing PageSpeed request",
service: this.SERVICE_NAME,
method: "requestPageSpeed",
details: { url: monitor.url },
});
}
}
private async requestHardware(monitor: Monitor): Promise<MonitorStatusResponse<HardwareStatusPayload>> {
try {
return await this.requestHttp<HardwareStatusPayload>(monitor);
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing hardware request",
service: this.SERVICE_NAME,
method: "requestHardware",
details: { url: monitor.url },
});
}
}
private async requestDocker(monitor: Monitor): Promise<MonitorStatusResponse<DockerStatusPayload>> {
try {
if (!monitor.url) {
throw new Error("Monitor URL is required");
}
const docker = new this.Docker({
socketPath: "/var/run/docker.sock",
});
const dockerResponse = this.buildStatusResponse<DockerStatusPayload>({
monitor,
overrides: {
status: false,
code: this.NETWORK_ERROR,
message: "No response",
},
});
const containers = await docker.listContainers({ all: true });
// Normalize input: strip leading slashes and convert to lowercase for comparison
const normalizedInput = monitor.url.replace(/^\/+/, "").toLowerCase();
// Priority-based matching to avoid ambiguity:
// 1. Exact full ID match (64-char)
const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
// 2. Exact container name match (case-insensitive)
const exactNameMatch = containers.find((c) =>
c.Names.some((name: string) => {
const cleanName = name.replace(/^\/+/, "").toLowerCase();
return cleanName === normalizedInput;
})
);
// 3. Partial ID match (fallback for backwards compatibility)
const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
// Select container based on priority
const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
// Return negative response if no container
if (!targetContainer) {
this.logger.warn({
message: `No container found for "${monitor.url}".`,
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
dockerResponse.code = 404;
dockerResponse.status = false;
dockerResponse.message = "Docker container not found";
return dockerResponse;
}
// Return negative response if ambiguous matches exist
const matchTypes = [];
if (exactIdMatch) matchTypes.push("exact ID");
if (exactNameMatch) matchTypes.push("exact name");
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
if (matchTypes.length > 1) {
this.logger.warn({
message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`,
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
dockerResponse.status = false;
dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
return dockerResponse;
}
const container = docker.getContainer(targetContainer.Id);
const { response, responseTime, error }: { response?: { State: { Status: string } } | null; responseTime: number; error: unknown } =
await this.timeRequest(() => container.inspect());
dockerResponse.responseTime = responseTime;
dockerResponse.status = response?.State?.Status === "running" ? true : false;
dockerResponse.code = 200;
dockerResponse.message = "Docker container status fetched successfully";
if (error) {
dockerResponse.status = false;
dockerResponse.code =
(error && typeof error === "object" && "statusCode" in error ? (error as { statusCode?: number }).statusCode : undefined) ||
this.NETWORK_ERROR;
dockerResponse.message =
(error && typeof error === "object" && "reason" in error ? (error as { reason?: string }).reason : undefined) ||
"Failed to fetch Docker container information";
return dockerResponse;
}
return dockerResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing Docker request",
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
}
}
private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse<PortStatusPayload>> {
try {
const { url, port } = monitor;
if (!port) {
throw new Error("Port is required for port monitor");
}
const { response, responseTime, error } = await this.timeRequest(async () => {
return new Promise((resolve, reject) => {
const socket = this.net.createConnection(
{
host: url,
port,
},
() => {
socket.end();
socket.destroy();
resolve({ success: true });
}
);
socket.setTimeout(5000);
socket.on("timeout", () => {
socket.destroy();
reject(new Error("Connection timeout"));
});
socket.on("error", (err: unknown) => {
socket.destroy();
reject(err);
});
});
});
const portResponse = this.buildStatusResponse<PortStatusPayload>({
monitor,
overrides: {
code: 200,
status: (response as { success?: boolean })?.success ?? false,
message: "Port check successful",
responseTime,
},
});
if (error) {
portResponse.code = this.NETWORK_ERROR;
portResponse.status = false;
portResponse.message = "Port check failed";
return portResponse;
}
return portResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing port check",
service: this.SERVICE_NAME,
method: "requestPort",
details: { url: monitor.url, port: monitor.port },
});
}
}
private async requestGame(monitor: Monitor): Promise<MonitorStatusResponse<GameStatusPayload>> {
try {
const { url, port, gameId } = monitor;
const gameResponse = this.buildStatusResponse<GameStatusPayload>({
monitor,
overrides: {
code: 200,
status: true,
message: "Success",
},
});
const state = await this.GameDig.query({
type: gameId ?? "unknown",
host: url,
port: port ?? 0,
}).catch((error: unknown): undefined => {
this.logger.warn({
message: error instanceof Error ? error.message : String(error),
service: this.SERVICE_NAME,
method: "requestGame",
details: { url, port, gameId },
});
return undefined;
});
if (!state) {
gameResponse.code = this.NETWORK_ERROR;
gameResponse.status = false;
gameResponse.message = "No response";
return gameResponse;
}
gameResponse.responseTime = state.ping ?? 0;
gameResponse.payload = state as GameStatusPayload;
return gameResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing game server check",
service: this.SERVICE_NAME,
method: "requestGame",
details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId },
});
}
}
private async requestGrpc(monitor: Monitor): Promise<MonitorStatusResponse<GrpcStatusPayload>> {
try {
const { url, port, ignoreTlsErrors } = monitor;
const grpcServiceName = monitor.grpcServiceName || "";
if (!url) {
throw new AppError({ message: "Monitor host is required", service: this.SERVICE_NAME, method: "requestGrpc" });
}
if (!port) {
throw new AppError({ message: "Monitor port is required", service: this.SERVICE_NAME, method: "requestGrpc" });
}
const target = `${url}:${port}`;
const currentFilePath = fileURLToPath(import.meta.url);
const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto");
const packageDefinition = this.protoLoader.loadSync(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as {
grpc: {
health: {
v1: {
Health: new (
target: string,
credentials: unknown
) => {
Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void;
close: () => void;
};
};
};
};
};
const healthService = grpcObject.grpc.health.v1.Health;
let credentials;
if (ignoreTlsErrors) {
credentials = this.grpc.credentials.createSsl(null, null, null, {
checkServerIdentity: () => undefined,
});
} else {
credentials = this.grpc.credentials.createInsecure();
}
const client = new healthService(target, credentials);
const TIMEOUT_MS = 10000;
const deadline = new Date(Date.now() + TIMEOUT_MS);
const grpcResponse = this.buildStatusResponse<GrpcStatusPayload>({
monitor,
overrides: {
status: false,
code: this.NETWORK_ERROR,
message: "gRPC health check not executed",
},
});
const { response, responseTime, error } = await this.timeRequest<GrpcStatusPayload>(() => {
return new Promise<GrpcStatusPayload>((resolve, reject) => {
client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => {
client.close();
if (err) {
const grpcErr = err as { code?: number; details?: string; message?: string };
const payload: GrpcStatusPayload = {
grpcStatusCode: grpcErr.code ?? -1,
grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1),
serviceName: grpcServiceName,
servingStatus: "UNKNOWN",
};
const grpcError = new AppError({
message: grpcErr.details || grpcErr.message || "gRPC error",
service: this.SERVICE_NAME,
method: "requestGrpc",
}) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
grpcError.grpcPayload = payload;
grpcError.grpcCode = grpcErr.code;
reject(grpcError);
return;
}
const resp = response as { status?: string } | undefined;
const servingStatus = resp?.status ?? "UNKNOWN";
resolve({
grpcStatusCode: 0,
grpcStatusName: "OK",
serviceName: grpcServiceName,
servingStatus,
});
});
});
});
if (error) {
const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
const payload = grpcError.grpcPayload;
grpcResponse.status = false;
grpcResponse.code = grpcError.grpcCode ?? this.NETWORK_ERROR;
grpcResponse.message = grpcError.message ?? "gRPC health check failed";
grpcResponse.responseTime = responseTime;
grpcResponse.payload = payload ?? null;
return grpcResponse;
}
const grpcPayload = response as GrpcStatusPayload;
const isServing = grpcPayload.servingStatus === "SERVING";
grpcResponse.status = isServing;
grpcResponse.code = isServing ? 200 : this.NETWORK_ERROR;
grpcResponse.message = isServing
? `gRPC service healthy (${grpcPayload.servingStatus})`
: `gRPC service unhealthy (${grpcPayload.servingStatus})`;
grpcResponse.responseTime = responseTime;
grpcResponse.payload = grpcPayload;
return grpcResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing gRPC health check",
service: this.SERVICE_NAME,
method: "requestGrpc",
details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName },
});
}
}
private getGrpcStatusName(code: number): string {
const statusNames: Record<number, string> = {
0: "OK",
1: "CANCELLED",
2: "UNKNOWN",
3: "INVALID_ARGUMENT",
4: "DEADLINE_EXCEEDED",
5: "NOT_FOUND",
6: "ALREADY_EXISTS",
7: "PERMISSION_DENIED",
8: "RESOURCE_EXHAUSTED",
9: "FAILED_PRECONDITION",
10: "ABORTED",
11: "OUT_OF_RANGE",
12: "UNIMPLEMENTED",
13: "INTERNAL",
14: "UNAVAILABLE",
15: "DATA_LOSS",
16: "UNAUTHENTICATED",
};
return statusNames[code] || "UNKNOWN";
}
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
@@ -57,7 +866,7 @@ export class NetworkService implements INetworkService {
teamId: "unknown",
type: "unknown",
status: false,
code: NETWORK_ERROR,
code: this.NETWORK_ERROR,
message: `Unsupported type: ${type}`,
};
}
@@ -81,7 +890,7 @@ export class NetworkService implements INetworkService {
} catch (err: unknown) {
this.logger.warn({
message: err instanceof Error ? err.message : String(err),
service: SERVICE_NAME,
service: this.SERVICE_NAME,
method: "requestWebhook",
});
@@ -90,7 +899,7 @@ export class NetworkService implements INetworkService {
return {
type: "webhook",
status: false,
code: axiosError.response?.status ?? NETWORK_ERROR,
code: axiosError.response?.status ?? this.NETWORK_ERROR,
message: `Failed to send ${type} notification`,
payload: axiosError.response?.data,
};
@@ -99,7 +908,7 @@ export class NetworkService implements INetworkService {
return {
type: "webhook",
status: false,
code: NETWORK_ERROR,
code: this.NETWORK_ERROR,
message: `Failed to send ${type} notification`,
};
}
@@ -125,7 +934,7 @@ export class NetworkService implements INetworkService {
throw new AppError({
message: originalMessage || "Error sending PagerDuty notification",
service: SERVICE_NAME,
service: this.SERVICE_NAME,
method: "requestPagerDuty",
details: {
responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined,
@@ -168,7 +977,7 @@ export class NetworkService implements INetworkService {
if (err instanceof Error) {
this.logger.warn({
message: err.message,
service: SERVICE_NAME,
service: this.SERVICE_NAME,
method: "requestMatrix",
});
@@ -176,7 +985,7 @@ export class NetworkService implements INetworkService {
const axiosError = err as { response?: { status?: number; data?: unknown } };
return {
status: false,
code: axiosError.response?.status || NETWORK_ERROR,
code: axiosError.response?.status || this.NETWORK_ERROR,
message: "Failed to send Matrix notification",
payload: axiosError.response?.data,
};
@@ -185,15 +994,17 @@ export class NetworkService implements INetworkService {
this.logger.warn({
message: String(err),
service: SERVICE_NAME,
service: this.SERVICE_NAME,
method: "requestMatrix",
});
return {
status: false,
code: NETWORK_ERROR,
code: this.NETWORK_ERROR,
message: "Failed to send Matrix notification",
};
}
}
}
export default NetworkService;
+3 -1
View File
@@ -21,7 +21,7 @@ export interface ISettingsService {
getDBSettings(): Promise<Settings>;
}
export class SettingsService implements ISettingsService {
class SettingsService implements ISettingsService {
static SERVICE_NAME = SERVICE_NAME;
private settings: EnvConfig;
private settingsRepository: ISettingsRepository;
@@ -74,3 +74,5 @@ export class SettingsService implements ISettingsService {
return settings;
};
}
export default SettingsService;
+3 -4
View File
@@ -3,12 +3,9 @@ export type { CheckSnapshot } from "@/types/check.js";
import type { GeoContinent } from "@/types/geoCheck.js";
export type { GeoContinent } from "@/types/geoCheck.js";
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const;
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type);
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
export type MonitorStatus = (typeof MonitorStatuses)[number];
@@ -30,6 +27,8 @@ export interface Monitor {
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
basicAuthUser?: string;
basicAuthPassword?: string;
url: string;
port?: number;
isActive: boolean;
+3 -13
View File
@@ -14,8 +14,6 @@ import type {
MonitorStatus,
} from "@/types/index.js";
import type { QueryResult } from "gamedig";
export interface MonitorStatusResponse<
T =
| HttpStatusPayload
@@ -24,8 +22,7 @@ export interface MonitorStatusResponse<
| HardwareStatusPayload
| DockerStatusPayload
| GameStatusPayload
| GrpcStatusPayload
| WebSocketStatusPayload,
| GrpcStatusPayload,
> {
monitorId: string;
teamId: string;
@@ -52,7 +49,7 @@ export interface PingStatusPayload {
host: string;
numeric_host?: string;
alive: boolean;
time: number | unknown;
time: number;
times?: number[];
output?: string;
min?: string;
@@ -103,7 +100,7 @@ export interface PortStatusPayload {
success: boolean;
}
export type GameStatusPayload = QueryResult;
export type GameStatusPayload = Record<string, unknown>;
export interface GrpcStatusPayload {
grpcStatusCode: number;
@@ -112,10 +109,6 @@ export interface GrpcStatusPayload {
servingStatus: string;
}
export interface WebSocketStatusPayload {
connected: boolean;
}
export interface MonitorPayloadMap {
ping: PingStatusPayload;
http: HttpStatusPayload;
@@ -125,7 +118,6 @@ export interface MonitorPayloadMap {
port: PortStatusPayload;
game: GameStatusPayload;
grpc: GrpcStatusPayload;
websocket: WebSocketStatusPayload;
default: unknown;
}
@@ -142,5 +134,3 @@ export type StatusChangeResult = {
temp: boolean;
};
};
export type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
+1 -1
View File
@@ -11,7 +11,7 @@ export const getChecksParamValidation = z.object({
});
export const getChecksQueryValidation = z.object({
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]).optional(),
type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"]).optional(),
sortOrder: z.enum(["asc", "desc"]).optional(),
limit: z.coerce.number().optional(),
dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(),
-1
View File
@@ -2,7 +2,6 @@ import { z } from "zod";
const envSchema = z.object({
// Server Configuration
PORT: z.string().default("52345"),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"),
+8 -4
View File
@@ -21,8 +21,8 @@ export const getMonitorsByTeamIdParamValidation = z.object({});
export const getMonitorsByTeamIdQueryValidation = z.object({
type: z
.union([
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]),
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]),
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])),
])
.optional(),
filter: z.union([z.string(), z.literal(""), z.null()]).optional(),
@@ -37,8 +37,8 @@ export const getMonitorsWithChecksQueryValidation = z.object({
order: z.enum(["asc", "desc"]).optional(),
type: z
.union([
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]),
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])),
z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"]),
z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"])),
])
.optional(),
explain: booleanCoercion.optional(),
@@ -70,6 +70,8 @@ export const createMonitorBodyValidation = z.object({
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
basicAuthUser: z.union([z.string(), z.literal("")]).optional(),
basicAuthPassword: z.union([z.string(), z.literal("")]).optional(),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
selectedDisks: z.array(z.string()).optional(),
@@ -94,6 +96,8 @@ export const editMonitorBodyValidation = z.object({
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
basicAuthUser: z.union([z.string(), z.literal("")]).optional(),
basicAuthPassword: z.union([z.string(), z.literal("")]).optional(),
port: z.number().min(1).max(65535).optional(),
cpuAlertThreshold: z.number().optional(),
memoryAlertThreshold: z.number().optional(),