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
157 changed files with 3287 additions and 5392 deletions

No files matched your search

+14 -7
View File
@@ -46,11 +46,11 @@ You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.
## User's guide ## User's guide
Usage instructions can be found [here](https://checkmate.so/docs). Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
## Installation ## Installation
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs). See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
@@ -157,11 +157,12 @@ Here's how you can contribute:
0. Star this repo :) 0. Star this repo :)
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag. 1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
2. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. 2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Open an issue if you believe you've encountered a bug. 3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
4. Check for good-first-issue's if you are a newcomer. 4. Open an issue if you believe you've encountered a bug.
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs. 5. Check for good-first-issue's if you are a newcomer.
6. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). 6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors"> <a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" /> <img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
@@ -174,3 +175,9 @@ Here's how you can contribute:
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features). If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
Also check other developer and contributor-friendly projects of BlueWave:
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
+168 -119
View File
@@ -44,7 +44,7 @@
"redux-persist": "6.0.0", "redux-persist": "6.0.0",
"swr": "^2.3.6", "swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"zod": "^4.3.6" "zod": "^4.1.11"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "24.5.2", "@types/node": "24.5.2",
@@ -58,7 +58,7 @@
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "5.9.3", "typescript": "5.9.3",
"vite": "6.4.1" "vite": "6.3.6"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -434,6 +434,7 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -450,6 +451,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -466,6 +468,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -482,6 +485,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -498,6 +502,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -514,6 +519,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -530,6 +536,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -546,6 +553,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -562,6 +570,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -578,6 +587,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -594,6 +604,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -610,6 +621,7 @@
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -626,6 +638,7 @@
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -642,6 +655,7 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -658,6 +672,7 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -674,6 +689,7 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -688,6 +704,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -704,6 +721,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -720,6 +738,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -736,6 +755,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -752,6 +772,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -768,6 +789,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -784,6 +806,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -800,6 +823,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -816,6 +840,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -832,6 +857,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1634,12 +1660,13 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1647,12 +1674,13 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1660,12 +1688,13 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1673,12 +1702,13 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1686,12 +1716,13 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1699,12 +1730,13 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1712,12 +1744,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1725,12 +1758,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1738,12 +1772,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1751,12 +1786,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1764,12 +1800,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1777,12 +1814,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-musl": { "node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1790,12 +1828,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1803,12 +1842,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-musl": { "node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1816,12 +1856,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1829,12 +1870,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1842,12 +1884,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1855,12 +1898,11 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1868,12 +1910,13 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1881,12 +1924,13 @@
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": { "node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1894,12 +1938,13 @@
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1907,12 +1952,13 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1920,12 +1966,13 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1933,12 +1980,13 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1946,12 +1994,13 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.59.0", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2301,7 +2350,7 @@
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.5.2", "version": "24.5.2",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.12.0" "undici-types": "~7.12.0"
@@ -2317,6 +2366,7 @@
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.27", "version": "18.3.27",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
@@ -2455,9 +2505,7 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.14.0", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -3518,6 +3566,7 @@
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.12", "version": "0.25.12",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -3835,6 +3884,7 @@
}, },
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@@ -3968,6 +4018,7 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -5200,9 +5251,7 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.5", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -5268,6 +5317,7 @@
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -5600,6 +5650,7 @@
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -6107,8 +6158,7 @@
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.57.1", "version": "4.57.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "dev": true,
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
@@ -6121,31 +6171,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm-eabi": "4.57.1",
"@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-android-arm64": "4.57.1",
"@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.57.1",
"@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-darwin-x64": "4.57.1",
"@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.57.1",
"@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.57.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
"@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.57.1",
"@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.57.1",
"@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.57.1",
"@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.57.1",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
"@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.57.1",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
"@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.57.1",
"@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.57.1",
"@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.57.1",
"@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.57.1",
"@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openbsd-x64": "4.57.1",
"@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.57.1",
"@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.57.1",
"@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.57.1",
"@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.57.1",
"@rollup/rollup-win32-x64-msvc": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.57.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -6480,6 +6530,7 @@
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -6767,6 +6818,7 @@
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -6896,7 +6948,7 @@
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"devOptional": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -6940,7 +6992,7 @@
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.12.0", "version": "7.12.0",
"devOptional": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/union-value": { "node_modules/union-value": {
@@ -7029,9 +7081,8 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.4.1", "version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "dev": true,
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
@@ -7315,7 +7366,7 @@
"version": "2.8.2", "version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"devOptional": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
@@ -7340,8 +7391,6 @@
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
+3 -3
View File
@@ -13,7 +13,7 @@
"format-check": "prettier --check ." "format-check": "prettier --check ."
}, },
"lint-staged": { "lint-staged": {
"**/*": "prettier --write" "**/*": "npm run format"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
@@ -52,7 +52,7 @@
"redux-persist": "6.0.0", "redux-persist": "6.0.0",
"swr": "^2.3.6", "swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"zod": "^4.3.6" "zod": "^4.1.11"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "24.5.2", "@types/node": "24.5.2",
@@ -66,6 +66,6 @@
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "5.9.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; if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-25); const latestChecks = checks.slice(-25).reverse();
let data: Array<CheckSnapshot | PlaceholderCheck>; let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) { if (latestChecks.length !== 25) {
@@ -57,8 +57,7 @@ export const HeatmapResponseTimeTooltip = ({
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} {formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
</Typography> </Typography>
<Typography> <Typography>
{t("common.labels.responseTime")}:{" "} {t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms
{check?.originalResponseTime?.toFixed() ?? "N/A"} ms
</Typography> </Typography>
<Typography textTransform={"capitalize"}> <Typography textTransform={"capitalize"}>
Status:{" "} Status:{" "}
+3 -19
View File
@@ -11,26 +11,20 @@ import type { ResponsiveStyleValue } from "@mui/system";
interface SliderInputProps extends SliderProps { interface SliderInputProps extends SliderProps {
sx?: SliderProps["sx"]; sx?: SliderProps["sx"];
showValue?: boolean; showValue?: boolean;
formatDisplayValue?: (value: number) => string;
} }
export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>( export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
({ sx, showValue = false, formatDisplayValue, ...props }, ref) => { ({ sx, showValue = false, ...props }, ref) => {
const theme = useTheme(); const theme = useTheme();
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
const displayValue =
showValue && formatDisplayValue && typeof props.value === "number"
? formatDisplayValue(props.value)
: props.value;
return ( return (
<Stack <Stack
gap={theme.spacing(8)} gap={theme.spacing(8)}
direction={"row"} direction={"row"}
alignItems={"center"} alignItems={"center"}
> >
{showValue && <Typography>{displayValue}</Typography>} {showValue && <Typography>{props.value}</Typography>}
<Slider <Slider
{...props} {...props}
ref={ref} ref={ref}
@@ -69,21 +63,12 @@ interface SliderWithLabelProps extends SliderProps {
fieldLabel?: string; fieldLabel?: string;
required?: boolean; required?: boolean;
showValue?: boolean; showValue?: boolean;
formatDisplayValue?: (value: number) => string;
sliderMaxWidth?: ResponsiveStyleValue<number | string>; sliderMaxWidth?: ResponsiveStyleValue<number | string>;
} }
export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>( export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>(
( (
{ { fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props },
fieldLabel,
required,
showValue = true,
formatDisplayValue,
value,
sliderMaxWidth = "100%",
...props
},
ref ref
) => { ) => {
const theme = useTheme(); const theme = useTheme();
@@ -97,7 +82,6 @@ export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>
<SliderInput <SliderInput
{...props} {...props}
showValue={showValue} showValue={showValue}
formatDisplayValue={formatDisplayValue}
value={value} value={value}
ref={ref} ref={ref}
/> />
@@ -1,6 +1,9 @@
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useTheme } from "@mui/material/styles"; 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 { OfflineBanner } from "@/Components/design-elements";
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient"; import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
@@ -10,6 +13,7 @@ interface AppLayoutProps {
const AppLayout = ({ children }: AppLayoutProps) => { const AppLayout = ({ children }: AppLayoutProps) => {
const theme = useTheme(); const theme = useTheme();
const mode = useSelector((state: RootState) => state.ui.mode);
const [serverUnreachable, setServerUnreachable] = useState(false); const [serverUnreachable, setServerUnreachable] = useState(false);
const retryIntervalRef = useRef<number | null>(null); const retryIntervalRef = useRef<number | null>(null);
@@ -43,6 +47,10 @@ const AppLayout = ({ children }: AppLayoutProps) => {
sx={{ sx={{
minHeight: "100vh", minHeight: "100vh",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}} }}
> >
<OfflineBanner visible={serverUnreachable} /> <OfflineBanner visible={serverUnreachable} />
@@ -7,7 +7,7 @@ import type { MonitorType } from "@/Types/Monitor";
import { Typography, useTheme } from "@mui/material"; import { Typography, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next"; 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> = { const typeDisplayNames: Record<string, string> = {
http: "HTTP", http: "HTTP",
ping: "Ping", ping: "Ping",
@@ -15,7 +15,6 @@ const typeDisplayNames: Record<string, string> = {
docker: "Docker", docker: "Docker",
game: "Game", game: "Game",
grpc: "gRPC", grpc: "gRPC",
websocket: "WebSocket",
}; };
const statuses = ["up", "down"]; const statuses = ["up", "down"];
const states = ["active", "paused"]; const states = ["active", "paused"];
+4 -8
View File
@@ -40,6 +40,8 @@ export const useMonitorForm = ({
matchMethod: data?.matchMethod || "", matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "", expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "", jsonPath: data?.jsonPath || "",
basicAuthUser: data?.basicAuthUser || "",
basicAuthPassword: data?.basicAuthPassword || "",
}; };
break; break;
case "ping": case "ping":
@@ -103,14 +105,6 @@ export const useMonitorForm = ({
selectedDisks: data?.selectedDisks || [], selectedDisks: data?.selectedDisks || [],
}; };
break; break;
case "websocket":
defaults = {
...base,
type: "websocket",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
default: default:
defaults = { defaults = {
...base, ...base,
@@ -121,6 +115,8 @@ export const useMonitorForm = ({
matchMethod: "", matchMethod: "",
expectedValue: "", expectedValue: "",
jsonPath: "", jsonPath: "",
basicAuthUser: "",
basicAuthPassword: "",
}; };
} }
+2 -2
View File
@@ -1,14 +1,14 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { settingsSchema } from "@/Validation/settings"; import { settingsSchema } from "@/Validation/settings";
import type { Settings } from "@/Types/Settings"; import type { Settings } from "@/Types/Settings";
import type { SettingsFormInput } from "@/Validation/settings"; import type { SettingsFormData } from "@/Validation/settings";
interface UseSettingsFormOptions { interface UseSettingsFormOptions {
data?: Settings | null; data?: Settings | null;
} }
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => { export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
return useMemo(() => { return useMemo(() => {
const defaults: SettingsFormInput = { const defaults: SettingsFormData = {
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false, systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false, systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true, systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
-2
View File
@@ -27,14 +27,12 @@ export const useStatusPageForm = ({
companyName: data?.companyName || "", companyName: data?.companyName || "",
url: data?.url || generateDefaultUrl(), url: data?.url || generateDefaultUrl(),
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
type: data?.type || ["uptime"],
color: data?.color || "#4169E1", color: data?.color || "#4169E1",
monitors: data?.monitors || [], monitors: data?.monitors || [],
isPublished: data?.isPublished ?? false, isPublished: data?.isPublished ?? false,
showCharts: data?.showCharts ?? true, showCharts: data?.showCharts ?? true,
showUptimePercentage: data?.showUptimePercentage ?? true, showUptimePercentage: data?.showUptimePercentage ?? true,
showAdminLoginLink: data?.showAdminLoginLink ?? false, showAdminLoginLink: data?.showAdminLoginLink ?? false,
showInfrastructure: data?.showInfrastructure ?? false,
customCSS: data?.customCSS || "", customCSS: data?.customCSS || "",
logo: transformLogo(data?.logo), logo: transformLogo(data?.logo),
}; };
+11 -64
View File
@@ -11,17 +11,12 @@ import { ConfigBox, BasePage } from "@/Components/design-elements";
import { UserRoles } from "@/Types/User"; import { UserRoles } from "@/Types/User";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { UserRole, User } from "@/Types/User"; 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 { useTheme } from "@mui/material";
import { useEditUserForm } from "@/Hooks/useEditUserForm"; 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 type { EditUserFormData } from "@/Validation/editUser";
import { useEffect, useState } from "react"; import { useEffect } 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";
interface RoleOption { interface RoleOption {
id: UserRole; id: UserRole;
@@ -34,12 +29,6 @@ const EditUserPage = () => {
const { userId } = useParams<{ userId: string }>(); const { userId } = useParams<{ userId: string }>();
const { resolver, defaults } = useEditUserForm(); const { resolver, defaults } = useEditUserForm();
const { patch, loading: isSaving } = usePatch(); 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}`); const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
@@ -71,20 +60,6 @@ const EditUserPage = () => {
setValue("role", newRoles, { shouldValidate: true }); 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) => { const onSubmit = async (data: EditUserFormData) => {
await patch(`/auth/users/${userId}`, data); await patch(`/auth/users/${userId}`, data);
}; };
@@ -190,45 +165,17 @@ const EditUserPage = () => {
</Stack> </Stack>
} }
/> />
<Stack <Button
gap={LAYOUT.XS} type="submit"
direction="row" variant="contained"
justifyContent={"flex-end"} color="primary"
width="100%" loading={isSaving}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
> >
{canDeleteUser && ( {t("common.buttons.save")}
<Button </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>
</Stack> </Stack>
</form> </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> </BasePage>
); );
}; };
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Table } from "@/Components/design-elements"; import { Table } from "@/Components/design-elements";
import type { Header } from "@/Components/design-elements/Table"; import type { Header } from "@/Components/design-elements/Table";
import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useIsSuperAdmin } from "@/Hooks/useIsAdmin";
import type { User } from "@/Types/User"; import type { User } from "@/Types/User";
interface TeamTableProps { interface TeamTableProps {
@@ -13,7 +13,7 @@ interface TeamTableProps {
export const TeamTable = ({ users }: TeamTableProps) => { export const TeamTable = ({ users }: TeamTableProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const isAdmin = useIsAdmin(); const isSuperAdmin = useIsSuperAdmin();
const headers: Header<User>[] = [ const headers: Header<User>[] = [
{ {
@@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
]; ];
const handleRowClick = (row: User) => { const handleRowClick = (row: User) => {
if (isAdmin) { if (isSuperAdmin) {
navigate(`/account/team/${row.id}`); navigate(`/account/team/${row.id}`);
} }
}; };
@@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
<Table <Table
headers={headers} headers={headers}
data={users} data={users}
onRowClick={isAdmin ? handleRowClick : undefined} onRowClick={isSuperAdmin ? handleRowClick : undefined}
/> />
); );
}; };
+53 -34
View File
@@ -32,12 +32,7 @@ import {
import { SPACING, LAYOUT } from "@/Utils/Theme/constants"; import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi"; import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
import { useMonitorForm } from "@/Hooks/useMonitorForm"; import { useMonitorForm } from "@/Hooks/useMonitorForm";
import { import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
type Monitor,
type MonitorType,
type GamesMap,
supportsGeoCheck,
} from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification"; import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor"; import type { MonitorFormData } from "@/Validation/monitor";
@@ -146,17 +141,6 @@ const getGeneralSettingsConfig = (
showGrpcServiceName: false, showGrpcServiceName: false,
showIgnoreTls: 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; return configs[type] || configs.http;
}; };
@@ -302,53 +286,46 @@ const CreateMonitorPage = () => {
> >
<RadioWithDescription <RadioWithDescription
value="http" value="http"
label={t("pages.common.monitors.monitorTypes.optionHttp")} label={t("pages.createMonitor.form.type.optionHttp")}
description={t( description={t(
"pages.createMonitor.form.type.optionHttpDescription" "pages.createMonitor.form.type.optionHttpDescription"
)} )}
/> />
<RadioWithDescription <RadioWithDescription
value="ping" value="ping"
label={t("pages.common.monitors.monitorTypes.optionPing")} label={t("pages.createMonitor.form.type.optionPing")}
description={t( description={t(
"pages.createMonitor.form.type.optionPingDescription" "pages.createMonitor.form.type.optionPingDescription"
)} )}
/> />
<RadioWithDescription <RadioWithDescription
value="docker" value="docker"
label={t("pages.common.monitors.monitorTypes.optionDocker")} label={t("pages.createMonitor.form.type.optionDocker")}
description={t( description={t(
"pages.createMonitor.form.type.optionDockerDescription" "pages.createMonitor.form.type.optionDockerDescription"
)} )}
/> />
<RadioWithDescription <RadioWithDescription
value="port" value="port"
label={t("pages.common.monitors.monitorTypes.optionPort")} label={t("pages.createMonitor.form.type.optionPort")}
description={t( description={t(
"pages.createMonitor.form.type.optionPortDescription" "pages.createMonitor.form.type.optionPortDescription"
)} )}
/> />
<RadioWithDescription <RadioWithDescription
value="game" value="game"
label={t("pages.common.monitors.monitorTypes.optionGame")} label={t("pages.createMonitor.form.type.optionGame")}
description={t( description={t(
"pages.createMonitor.form.type.optionGameDescription" "pages.createMonitor.form.type.optionGameDescription"
)} )}
/> />
<RadioWithDescription <RadioWithDescription
value="grpc" value="grpc"
label={t("pages.common.monitors.monitorTypes.optionGrpc")} label={t("pages.createMonitor.form.type.optionGrpc")}
description={t( description={t(
"pages.createMonitor.form.type.optionGrpcDescription" "pages.createMonitor.form.type.optionGrpcDescription"
)} )}
/> />
<RadioWithDescription
value="websocket"
label={t("pages.common.monitors.monitorTypes.optionWebSocket")}
description={t(
"pages.createMonitor.form.type.optionWebSocketDescription"
)}
/>
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
)} )}
@@ -765,9 +742,7 @@ const CreateMonitorPage = () => {
} }
/> />
{(watchedType === "http" || {(watchedType === "http" || watchedType === "grpc") && (
watchedType === "grpc" ||
watchedType === "websocket") && (
<ConfigBox <ConfigBox
title={t("pages.createMonitor.form.ignoreTls.title")} title={t("pages.createMonitor.form.ignoreTls.title")}
subtitle={t("pages.createMonitor.form.ignoreTls.description")} 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" && ( {watchedType === "http" && (
<ConfigBox <ConfigBox
title={t("pages.createMonitor.form.advanced.title")} title={t("pages.createMonitor.form.advanced.title")}
@@ -910,7 +929,7 @@ const CreateMonitorPage = () => {
/> />
)} )}
{supportsGeoCheck(watchedType) && ( {watchedType === "http" && (
<ConfigBox <ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")} title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")} subtitle={t("pages.createMonitor.form.geoChecks.description")}
+24 -41
View File
@@ -20,7 +20,7 @@ import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
import { useToast } from "@/Hooks/UseToast"; import { useToast } from "@/Hooks/UseToast";
import { useSettingsForm } from "@/Hooks/useSettingsForm"; import { useSettingsForm } from "@/Hooks/useSettingsForm";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import type { SettingsFormData, SettingsFormInput } from "@/Validation/settings"; import type { SettingsFormData } from "@/Validation/settings";
import { useState } from "react"; import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs"; import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
@@ -37,7 +37,6 @@ import {
} from "@/Features/UI/uiSlice.js"; } from "@/Features/UI/uiSlice.js";
import timezones from "@/Utils/timezones.json"; import timezones from "@/Utils/timezones.json";
import type { RootState } from "@/Types/state"; import type { RootState } from "@/Types/state";
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
interface Timezone { interface Timezone {
id: string; id: string;
@@ -89,7 +88,7 @@ export const SettingsPage = () => {
// Initialize form with schema and defaults // Initialize form with schema and defaults
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings }); const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
const form = useForm<SettingsFormInput, unknown, SettingsFormData>({ const form = useForm<SettingsFormData>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: defaults, defaultValues: defaults,
mode: "onChange", mode: "onChange",
@@ -256,7 +255,28 @@ export const SettingsPage = () => {
delete (dataToSend as any).systemEmailPassword; delete (dataToSend as any).systemEmailPassword;
} }
const result = await patch("/settings", dataToSend as SettingsFormData); // Convert undefined to empty string for backend unsetting
const processedData = Object.entries(dataToSend).reduce((acc, [key, value]) => {
const typedKey = key as keyof SettingsFormData;
if (value === undefined) {
(acc as any)[typedKey] = "";
} else if (typeof value === "object" && value !== null) {
// Handle nested objects like globalThresholds
// Convert 0 to "" for unsetting thresholds
(acc as any)[typedKey] = Object.entries(value).reduce(
(nested, [nestedKey, nestedValue]) => ({
...nested,
[nestedKey]: nestedValue === undefined ? "" : nestedValue,
}),
{}
);
} else {
(acc as any)[typedKey] = value;
}
return acc;
}, {} as Partial<SettingsFormData>);
const result = await patch("/settings", processedData as SettingsFormData);
if (result?.success) { if (result?.success) {
// Update API key state from response // Update API key state from response
@@ -442,43 +462,6 @@ export const SettingsPage = () => {
/> />
)} )}
{/* Check Retention */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.retention.title")}
subtitle={t("pages.settings.form.retention.description")}
rightContent={
<Controller
name="checkTTL"
control={form.control}
defaultValue={defaults.checkTTL}
render={({ field }) => (
<SliderWithLabel
{...field}
fieldLabel={t("pages.settings.form.retention.option.days.label")}
min={1}
max={CHECK_TTL_SENTINEL}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
value={field.value || 30}
onChange={(_, value) => field.onChange(value)}
valueLabelDisplay="auto"
valueLabelFormat={(value: number) =>
value >= CHECK_TTL_SENTINEL
? t("pages.settings.form.retention.option.days.unlimited")
: `${value}`
}
formatDisplayValue={(value: number) =>
value >= CHECK_TTL_SENTINEL
? t("pages.settings.form.retention.option.days.unlimited")
: `${value}`
}
/>
)}
/>
}
/>
)}
{/* Global Thresholds */} {/* Global Thresholds */}
{isAdmin && ( {isAdmin && (
<ConfigBox <ConfigBox
+8 -58
View File
@@ -21,28 +21,19 @@ import {
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useStatusPageForm } from "@/Hooks/useStatusPageForm"; import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
import type { StatusPageFormData } from "@/Validation/statusPage"; import type { StatusPageFormData } from "@/Validation/statusPage";
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi"; import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor"; import type { Monitor } from "@/Types/Monitor";
import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage"; import type { StatusPageResponse } from "@/Types/StatusPage";
import { getMonitorTypeLabel } from "@/Types/StatusPage";
import timezones from "@/Utils/timezones.json"; import timezones from "@/Utils/timezones.json";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import axios from "axios"; import axios from "axios";
import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls"; import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls";
const monitorsUrl = (() => {
const params = new URLSearchParams();
["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach(
(type) => params.append("type", type)
);
return `/monitors/team?${params.toString()}`;
})();
interface TimezoneOption { interface TimezoneOption {
_id: string; _id: string;
name: string; name: string;
@@ -58,11 +49,11 @@ const CreateStatusPage = () => {
// Fetch existing status page data when configuring // Fetch existing status page data when configuring
const { data: statusPageData, isLoading: isLoadingStatusPage } = const { data: statusPageData, isLoading: isLoadingStatusPage } =
useGet<StatusPageResponse>( useGet<StatusPageResponse>(isCreate ? null : `/status-page/${url}?type=uptime`);
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
);
const { data: monitorsResponse } = useGet<Monitor[]>(monitorsUrl); const { data: monitorsResponse } = useGet<Monitor[]>(
"/monitors/team?type=http&type=ping&type=port&type=docker"
);
const monitors = monitorsResponse ?? []; const monitors = monitorsResponse ?? [];
const { post, loading: isSubmittingPost } = usePost(); const { post, loading: isSubmittingPost } = usePost();
@@ -90,24 +81,6 @@ const CreateStatusPage = () => {
reset(defaults); reset(defaults);
}, [defaults, reset]); }, [defaults, reset]);
const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
const computedTypes: MonitorDisplayType[] = useMemo(() => {
const selectedMonitors = (watchedMonitorIds ?? [])
.map((id) => monitors.find((m) => m.id === id))
.filter((m): m is Monitor => m !== undefined);
const typesSet = new Set<MonitorDisplayType>();
selectedMonitors.forEach((m) => {
typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime");
});
return typesSet.size ? Array.from(typesSet) : ["uptime"];
}, [watchedMonitorIds, monitors]);
useEffect(() => {
form.setValue("type", computedTypes);
}, [computedTypes]);
const onError = (errors: any) => { const onError = (errors: any) => {
logger.debug("Status page validation errors", errors); logger.debug("Status page validation errors", errors);
}; };
@@ -130,6 +103,7 @@ const CreateStatusPage = () => {
const onSubmit = async (data: StatusPageFormData) => { const onSubmit = async (data: StatusPageFormData) => {
const fd = new FormData(); const fd = new FormData();
fd.append("type", "uptime");
fd.append("isPublished", String(data.isPublished)); fd.append("isPublished", String(data.isPublished));
if (data.companyName) fd.append("companyName", data.companyName); if (data.companyName) fd.append("companyName", data.companyName);
if (data.url) fd.append("url", data.url); if (data.url) fd.append("url", data.url);
@@ -138,16 +112,11 @@ const CreateStatusPage = () => {
fd.append("showCharts", String(data.showCharts)); fd.append("showCharts", String(data.showCharts));
fd.append("showUptimePercentage", String(data.showUptimePercentage)); fd.append("showUptimePercentage", String(data.showUptimePercentage));
fd.append("showAdminLoginLink", String(data.showAdminLoginLink)); fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
fd.append("showInfrastructure", String(data.showInfrastructure));
data.monitors.forEach((monitorId) => { data.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId); fd.append("monitors[]", monitorId);
}); });
data.type.forEach((type) => {
fd.append("type[]", type);
});
// Handle logo upload // Handle logo upload
if (data.logo === null) { if (data.logo === null) {
// Signal to remove the logo // Signal to remove the logo
@@ -334,9 +303,7 @@ const CreateStatusPage = () => {
}} }}
> >
<GripVertical size={20} /> <GripVertical size={20} />
<Typography <Typography flexGrow={1}>{monitor.name}</Typography>
flexGrow={1}
>{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}</Typography>
<IconButton <IconButton
size="small" size="small"
onClick={() => { onClick={() => {
@@ -458,23 +425,6 @@ const CreateStatusPage = () => {
/> />
)} )}
/> />
<Controller
name="showInfrastructure"
control={control}
render={({ field }) => (
<FormControlLabel
control={
<Checkbox
checked={field.value}
onChange={field.onChange}
/>
}
label={t(
"pages.statusPages.form.features.option.showInfrastructure.label"
)}
/>
)}
/>
{/* <Controller {/* <Controller
name="showUptimePercentage" name="showUptimePercentage"
control={control} control={control}
@@ -1,246 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Gauge } from "@/Components/design-elements";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import prettyBytes from "pretty-bytes";
import type { Monitor } from "@/Types/Monitor";
import Grid from "@mui/material/Grid";
import useMediaQuery from "@mui/material/useMediaQuery";
import Box from "@mui/material/Box";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
const GAUGE_RADIUS = 60;
const GAUGE_STROKE_WIDTH = 12;
const PERCENTAGE_MULTIPLIER = 100;
interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"];
}
interface MetricDetail {
label: string;
value: string;
}
interface MetricConfig {
key: string;
label: string;
hasData: boolean;
progress: number;
details: MetricDetail[];
}
const MetricDetailRow = ({ label, value }: MetricDetail) => {
const theme = useTheme();
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{label}
</Typography>
<Typography variant="body2">{value}</Typography>
</Stack>
);
};
interface MetricItemProps {
label: string;
progress: number;
details?: MetricDetail[];
}
const MetricItem = ({ label, progress, details }: MetricItemProps) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Grid
size={isSmall ? 12 : 4}
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
gap: theme.spacing(SPACING.LG),
padding: theme.spacing(LAYOUT.XS),
borderRight: isSmall ? "none" : `1px solid ${theme.palette.divider}`,
borderBottom: isSmall ? `1px solid ${theme.palette.divider}` : "none",
"&:last-child": {
borderRight: "none",
borderBottom: "none",
paddingBottom: theme.spacing(SPACING.LG),
},
}}
>
<Box
display="flex"
flexDirection="column"
alignItems="center"
>
<Gauge
progress={progress}
radius={GAUGE_RADIUS}
strokeWidth={GAUGE_STROKE_WIDTH}
/>
<Typography variant="body2">{label}</Typography>
</Box>
{details && details.length > 0 && (
<Box
width="100%"
paddingX={theme.spacing(LAYOUT.LG)}
>
{details.map((detail) => (
<MetricDetailRow
key={detail.label}
label={detail.label}
value={detail.value}
/>
))}
</Box>
)}
</Grid>
);
};
type LatestCheck = NonNullable<Monitor["recentChecks"]>[number];
const buildCpuMetric = (
check: LatestCheck,
t: (key: string) => string
): MetricConfig | null => {
if (!check.cpu || typeof check.cpu.usage_percent !== "number") {
return null;
}
const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER;
return {
key: "cpu",
label: t("pages.statusPages.monitorsList.infrastructure.cpu"),
hasData: true,
progress: usagePercent,
details: [
{
label: t("pages.statusPages.monitorsList.infrastructure.usage"),
value: `${usagePercent.toFixed(2)}%`,
},
],
};
};
const buildMemoryMetric = (
check: LatestCheck,
t: (key: string) => string
): MetricConfig | null => {
if (
!check.memory ||
typeof check.memory.usage_percent !== "number" ||
typeof check.memory.used_bytes !== "number" ||
typeof check.memory.total_bytes !== "number"
) {
return null;
}
return {
key: "memory",
label: t("pages.statusPages.monitorsList.infrastructure.memory"),
hasData: true,
progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER,
details: [
{
label: t("pages.statusPages.monitorsList.infrastructure.used"),
value: prettyBytes(check.memory.used_bytes ?? 0),
},
{
label: t("pages.statusPages.monitorsList.infrastructure.total"),
value: prettyBytes(check.memory.total_bytes ?? 0),
},
],
};
};
const buildDiskMetric = (
check: LatestCheck,
t: (key: string) => string
): MetricConfig | null => {
if (!check.disk || check.disk.length === 0) {
return null;
}
const disks = check.disk;
const avgUsagePercent =
(disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) *
PERCENTAGE_MULTIPLIER;
const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0);
const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0);
return {
key: "disk",
label: t("pages.statusPages.monitorsList.infrastructure.disk"),
hasData: true,
progress: avgUsagePercent,
details: [
{
label: t("pages.statusPages.monitorsList.infrastructure.used"),
value: prettyBytes(totalUsedBytes),
},
{
label: t("pages.statusPages.monitorsList.infrastructure.total"),
value: prettyBytes(totalTotalBytes),
},
],
};
};
export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => {
const theme = useTheme();
const { t } = useTranslation();
const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
if (!latestCheck) {
return (
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{t("pages.statusPages.monitorsList.noData")}
</Typography>
);
}
const metrics: MetricConfig[] = [
buildCpuMetric(latestCheck, t),
buildMemoryMetric(latestCheck, t),
buildDiskMetric(latestCheck, t),
].filter((m): m is MetricConfig => m !== null);
if (metrics.length === 0) {
return (
<Typography
variant="body2"
color={theme.palette.text.secondary}
>
{t("pages.statusPages.monitorsList.noData")}
</Typography>
);
}
return (
<Grid
container
alignItems="center"
padding={theme.spacing(LAYOUT.XS)}
>
{metrics.map(({ key, label, progress, details }) => (
<MetricItem
key={key}
label={label}
progress={progress}
details={details}
/>
))}
</Grid>
);
};
@@ -5,16 +5,13 @@ import Typography from "@mui/material/Typography";
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common"; import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
import { StatusLabel, BaseBox } from "@/Components/design-elements"; import { StatusLabel, BaseBox } from "@/Components/design-elements";
import { SwitchComponent } from "@/Components/inputs"; import { SwitchComponent } from "@/Components/inputs";
import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics";
import { useTheme, type Theme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useState } from "react"; import { useState } from "react";
import type { Monitor } from "@/Types/Monitor"; import type { Monitor } from "@/Types/Monitor";
import type { StatusPage } from "@/Types/StatusPage"; import type { StatusPage } from "@/Types/StatusPage";
import { getMonitorTypeLabel } from "@/Types/StatusPage";
import type { RootState } from "@/Types/state"; import type { RootState } from "@/Types/state";
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
interface StatusPageMonitor extends Monitor { interface StatusPageMonitor extends Monitor {
checks?: Monitor["recentChecks"]; checks?: Monitor["recentChecks"];
@@ -25,124 +22,29 @@ interface MonitorsListProps {
monitors: StatusPageMonitor[]; monitors: StatusPageMonitor[];
} }
const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
const bg =
monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light;
return {
backgroundColor: bg,
color: theme.palette.background.paper,
padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`,
borderRadius: theme.shape.borderRadius,
};
};
const MonitorHeader = ({
monitor,
showURL,
}: {
monitor: StatusPageMonitor;
showURL: boolean;
}) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={theme.spacing(LAYOUT.XS)}
mb={theme.spacing(LAYOUT.XS)}
>
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(SPACING.LG)}
mb={theme.spacing(SPACING.SM)}
>
<Typography
variant="h6"
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
>
{monitor.name}
</Typography>
<Typography
variant="caption"
sx={getMonitorBadgeStyles(monitor.type ?? "", theme)}
>
{getMonitorTypeLabel(monitor.type, t)}
</Typography>
</Stack>
{showURL && monitor.url && (
<Typography
variant="body2"
color={theme.palette.text.secondary}
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
>
{monitor.url}
</Typography>
)}
</Box>
<StatusLabel status={monitor.status} />
</Stack>
);
};
const MonitorContent = ({
monitor,
statusPage,
chartType,
}: {
monitor: StatusPageMonitor;
statusPage: StatusPage;
chartType: string;
}) => {
const theme = useTheme();
if (monitor.type === "hardware") {
if (statusPage.showInfrastructure === false) return null;
return <InfrastructureMetrics monitor={monitor} />;
}
if (statusPage.showCharts === false) return null;
const checks = monitor.checks?.slice().reverse() ?? [];
return (
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}>
{chartType === "histogram" ? (
<HistogramResponseTime
height={{ xs: 50, md: 100 }}
gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }}
checks={checks}
/>
) : (
<HeatmapResponseTime checks={checks} />
)}
</Box>
);
};
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const showURL = useSelector((state: RootState) => state.ui?.showURL); const showURL = useSelector((state: RootState) => state.ui?.showURL);
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram"); const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
return ( return (
<Stack gap={theme.spacing(LAYOUT.MD)}> <Stack gap={theme.spacing(8)}>
{statusPage.showCharts && ( {statusPage.showCharts !== false && (
<Stack <Stack
direction="row" direction={"row"}
alignItems="center" alignItems={"center"}
gap={theme.spacing(LAYOUT.SM)}
> >
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography> <Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
<SwitchComponent <SwitchComponent
dualOption dualOption
value={chartType} value={chartType}
checked={chartType === "histogram"} checked={chartType === "histogram"}
onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")} onChange={(e) => {
setChartType(e.target.checked ? "histogram" : "heatmap");
}}
/> />
<Typography> <Typography>
{t("pages.statusPages.monitorsList.chartTypeHistogram")} {t("pages.statusPages.monitorsList.chartTypeHistogram")}
@@ -150,22 +52,59 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
</Stack> </Stack>
)} )}
{monitors.map((monitor) => ( {monitors?.map((monitor) => {
<BaseBox return (
key={monitor.id} <BaseBox
padding={theme.spacing(LAYOUT.MD)} key={monitor.id}
> padding={theme.spacing(4)}
<MonitorHeader >
monitor={monitor} <Stack
showURL={showURL} direction="row"
/> alignItems="center"
<MonitorContent justifyContent="space-between"
monitor={monitor} gap={theme.spacing(4)}
statusPage={statusPage} mb={statusPage.showCharts !== false ? theme.spacing(4) : 0}
chartType={chartType} >
/> <Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
</BaseBox> <Typography
))} variant="h6"
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{monitor.name}
</Typography>
{showURL && (
<Typography
variant="body2"
color="text.secondary"
>
{monitor.url}
</Typography>
)}
</Box>
<StatusLabel status={monitor.status} />
</Stack>
{statusPage.showCharts !== false && (
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
{chartType === "histogram" ? (
<HistogramResponseTime
height={{ xs: 50, md: 100 }}
gap={{ xs: theme.spacing(0.5), md: theme.spacing(5) }}
checks={monitor?.checks?.slice().reverse() ?? []}
/>
) : (
<HeatmapResponseTime
checks={monitor?.checks?.slice().reverse() ?? []}
/>
)}
</Box>
)}
</BaseBox>
);
})}
</Stack> </Stack>
); );
}; };
+3 -9
View File
@@ -23,22 +23,16 @@ const StatusPageView = () => {
const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const isPublic = location.pathname.startsWith("/status/public"); const isPublic = location.pathname.startsWith("/status/public");
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null; const apiUrl = url ? `/status-page/${url}?type=uptime` : null;
const { data, isLoading, error } = useGet<StatusPageResponse>( const { data, isLoading, error } = useGet<StatusPageResponse>(apiUrl);
apiUrl,
{},
{
refreshInterval: 10000,
}
);
const statusPage = data?.statusPage; const statusPage = data?.statusPage;
const monitors = data?.monitors ?? []; const monitors = data?.monitors ?? [];
if (!statusPage) return null; if (!statusPage) return null;
if (monitors.length === 0) { if (monitors?.length === 0) {
return ( return (
<BasePage <BasePage
loading={isLoading} loading={isLoading}
@@ -97,7 +97,7 @@ export const StatusPagesTable = ({
{ {
id: "type", id: "type",
content: t("common.table.headers.type"), content: t("common.table.headers.type"),
render: (row) => row.type.join(", "), render: (row) => row.type,
}, },
{ {
id: "status", id: "status",
+3 -3
View File
@@ -20,7 +20,7 @@ import { useState, useMemo } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useGet } from "@/Hooks/UseApi"; 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 { ChecksResponse } from "@/Types/Check";
import type { import type {
GeoChecksResult, GeoChecksResult,
@@ -122,7 +122,7 @@ const UptimeDetailsPage = () => {
); );
const geoChecksUrl = useMemo(() => { const geoChecksUrl = useMemo(() => {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) { if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
return null; return null;
} }
const params = new URLSearchParams(); const params = new URLSearchParams();
@@ -141,7 +141,7 @@ const UptimeDetailsPage = () => {
// Fetch paginated geo checks for the table // Fetch paginated geo checks for the table
const geoChecksTableUrl = useMemo(() => { const geoChecksTableUrl = useMemo(() => {
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) { if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
return null; return null;
} }
const params = new URLSearchParams(); const params = new URLSearchParams();
+1 -1
View File
@@ -70,7 +70,7 @@ const UptimeMonitorsPage = () => {
const effectiveTypes = const effectiveTypes =
selectedTypes.length > 0 selectedTypes.length > 0
? selectedTypes ? selectedTypes
: ["http", "ping", "docker", "port", "game", "grpc", "websocket"]; : ["http", "ping", "docker", "port", "game", "grpc"];
// Build URL for monitors with checks // Build URL for monitors with checks
const monitorsWithChecksUrl = useMemo(() => { const monitorsWithChecksUrl = useMemo(() => {
+8 -3
View File
@@ -1,5 +1,3 @@
export const CHECK_TTL_SENTINEL = 366;
export interface CheckMetadata { export interface CheckMetadata {
monitorId: string; monitorId: string;
teamId: string; teamId: string;
@@ -127,6 +125,9 @@ export interface Check {
timings?: CheckTimings; timings?: CheckTimings;
statusCode: number; statusCode: number;
message: string; message: string;
ack: boolean;
ackAt?: string | null;
expiry: string;
cpu?: CheckCpuInfo; cpu?: CheckCpuInfo;
memory?: CheckMemoryInfo; memory?: CheckMemoryInfo;
disk?: CheckDiskInfo[]; disk?: CheckDiskInfo[];
@@ -139,6 +140,7 @@ export interface Check {
seo?: number; seo?: number;
performance?: number; performance?: number;
audits?: CheckAudits; audits?: CheckAudits;
__v: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
@@ -241,7 +243,10 @@ export interface ChecksSummary {
downChecks: number; downChecks: number;
} }
export type CheckSnapshot = Omit<Check, "metadata" | "__v" | "updatedAt"> & { export type CheckSnapshot = Omit<
Check,
"metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt"
> & {
originalResponseTime: number; originalResponseTime: number;
}; };
+2 -10
View File
@@ -12,20 +12,10 @@ export const MonitorTypes = [
"port", "port",
"game", "game",
"grpc", "grpc",
"websocket",
"unknown", "unknown",
] as const; ] as const;
export type MonitorType = (typeof MonitorTypes)[number]; 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 = [ export const MonitorStatuses = [
"up", "up",
"down", "down",
@@ -54,6 +44,8 @@ export interface Monitor {
jsonPath?: string; jsonPath?: string;
expectedValue?: string; expectedValue?: string;
matchMethod?: MonitorMatchMethod; matchMethod?: MonitorMatchMethod;
basicAuthUser?: string;
basicAuthPassword?: string;
url: string; url: string;
port?: number; port?: number;
isActive: boolean; isActive: boolean;
-1
View File
@@ -5,7 +5,6 @@ export const NotificationChannels = [
"webhook", "webhook",
"pager_duty", "pager_duty",
"matrix", "matrix",
"teams",
] as const; ] as const;
export type NotificationChannel = (typeof NotificationChannels)[number]; export type NotificationChannel = (typeof NotificationChannels)[number];
+2 -24
View File
@@ -1,31 +1,10 @@
import type { Monitor, MonitorType } from "@/Types/Monitor"; import type { Monitor } from "@/Types/Monitor";
export type MonitorDisplayType = "uptime" | "infrastructure";
export const MONITOR_TYPE_KEYS: Partial<Record<MonitorType, string>> = {
http: "pages.common.monitors.monitorTypes.optionHttp",
ping: "pages.common.monitors.monitorTypes.optionPing",
docker: "pages.common.monitors.monitorTypes.optionDocker",
port: "pages.common.monitors.monitorTypes.optionPort",
game: "pages.common.monitors.monitorTypes.optionGame",
grpc: "pages.common.monitors.monitorTypes.optionGrpc",
websocket: "pages.common.monitors.monitorTypes.optionWebSocket",
hardware: "pages.common.monitors.monitorTypes.optionHardware",
pagespeed: "pages.common.monitors.monitorTypes.optionPagespeed",
};
export const getMonitorTypeLabel = (
type: MonitorType,
t: (key: string) => string
): string => {
const key = MONITOR_TYPE_KEYS[type];
return key ? t(key) : type;
};
export interface StatusPage { export interface StatusPage {
id: string; id: string;
userId: string; userId: string;
teamId: string; teamId: string;
type: MonitorDisplayType[]; type: string;
companyName: string; companyName: string;
url: string; url: string;
timezone?: string; timezone?: string;
@@ -41,7 +20,6 @@ export interface StatusPage {
showCharts: boolean; showCharts: boolean;
showUptimePercentage: boolean; showUptimePercentage: boolean;
showAdminLoginLink: boolean; showAdminLoginLink: boolean;
showInfrastructure: boolean;
customCSS: string; customCSS: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
-1
View File
@@ -9,7 +9,6 @@ export const getMonitorPath = (type: MonitorType): string => {
ping: "uptime", ping: "uptime",
game: "uptime", game: "uptime",
grpc: "uptime", grpc: "uptime",
websocket: "uptime",
unknown: "uptime", unknown: "uptime",
docker: "uptime", docker: "uptime",
hardware: "infrastructure", hardware: "infrastructure",
-6
View File
@@ -12,8 +12,6 @@ export const typographyLevels = {
export const colors = { export const colors = {
gray200: "#EFEFEF", gray200: "#EFEFEF",
gray700: "#313131", gray700: "#313131",
gray900: "#151518",
gray850: "#1c1c21",
blueBlueWave: "#1570EF", blueBlueWave: "#1570EF",
}; };
@@ -33,8 +31,4 @@ export const darkPalette = {
secondary: { secondary: {
main: colors.gray700, 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(), matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(), expectedValue: z.string().optional(),
jsonPath: z.string().optional(), jsonPath: z.string().optional(),
basicAuthUser: z.string().optional(),
basicAuthPassword: z.string().optional(),
}); });
// Ping monitor schema // Ping monitor schema
@@ -115,13 +117,6 @@ const hardwareSchema = baseSchema.extend({
selectedDisks: z.array(z.string()), 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 // Discriminated union of all monitor types
export const monitorSchema = z.discriminatedUnion("type", [ export const monitorSchema = z.discriminatedUnion("type", [
httpSchema, httpSchema,
@@ -132,7 +127,6 @@ export const monitorSchema = z.discriminatedUnion("type", [
grpcSchema, grpcSchema,
pagespeedSchema, pagespeedSchema,
hardwareSchema, hardwareSchema,
websocketSchema,
]); ]);
export type MonitorFormData = z.infer<typeof monitorSchema>; export type MonitorFormData = z.infer<typeof monitorSchema>;
@@ -147,5 +141,4 @@ export {
grpcSchema, grpcSchema,
pagespeedSchema, pagespeedSchema,
hardwareSchema, hardwareSchema,
websocketSchema,
}; };
-6
View File
@@ -45,11 +45,6 @@ const matrixSchema = baseSchema.extend({
accessToken: z.string().min(1, "Access token is required"), accessToken: z.string().min(1, "Access token is required"),
}); });
const teamsSchema = baseSchema.extend({
type: z.literal("teams"),
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
});
export const notificationSchema = z.discriminatedUnion("type", [ export const notificationSchema = z.discriminatedUnion("type", [
emailSchema, emailSchema,
slackSchema, slackSchema,
@@ -57,7 +52,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
webhookSchema, webhookSchema,
pagerDutySchema, pagerDutySchema,
matrixSchema, matrixSchema,
teamsSchema,
]); ]);
export type NotificationFormData = z.infer<typeof notificationSchema>; export type NotificationFormData = z.infer<typeof notificationSchema>;
+9 -14
View File
@@ -1,4 +1,3 @@
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
import { z } from "zod"; import { z } from "zod";
export const settingsSchema = z.object({ export const settingsSchema = z.object({
@@ -7,42 +6,39 @@ export const settingsSchema = z.object({
systemEmailRejectUnauthorized: z.boolean(), systemEmailRejectUnauthorized: z.boolean(),
systemEmailConnectionHost: z systemEmailConnectionHost: z
.string() .string()
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailSecure: z.boolean().optional(), systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(), systemEmailPool: z.boolean().optional(),
showURL: z.boolean().optional(), showURL: z.boolean().optional(),
checkTTL: z checkTTL: z.number().int().min(1, "Please enter a value").max(365, "Maximum 365 days"),
.number()
.int()
.min(1, "Please enter a value")
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
pagespeedApiKey: z pagespeedApiKey: z
.string() .string()
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailHost: z systemEmailHost: z
.string() .string()
.regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address") .regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address")
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailPort: z.number().int().min(1).max(65535).optional(), systemEmailPort: z.number().int().min(1).max(65535).optional(),
systemEmailAddress: z systemEmailAddress: z
.string()
.email("Please enter a valid email address") .email("Please enter a valid email address")
.or(z.literal("")) .or(z.literal(""))
.transform((val) => (val === "" ? null : val.toLowerCase().trim())) .transform((val) => (val === "" ? undefined : val.toLowerCase().trim()))
.optional(), .optional(),
systemEmailUser: z systemEmailUser: z
.string() .string()
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailPassword: z systemEmailPassword: z
.string() .string()
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
systemEmailTLSServername: z systemEmailTLSServername: z
.string() .string()
.transform((val) => (val.trim() === "" ? null : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
.optional(), .optional(),
globalThresholds: z.object({ globalThresholds: z.object({
cpu: z.number().int().min(1).max(100), cpu: z.number().int().min(1).max(100),
@@ -52,5 +48,4 @@ export const settingsSchema = z.object({
}), }),
}); });
export type SettingsFormInput = z.input<typeof settingsSchema>;
export type SettingsFormData = z.infer<typeof settingsSchema>; export type SettingsFormData = z.infer<typeof settingsSchema>;
-4
View File
@@ -14,16 +14,12 @@ export const statusPageSchema = z.object({
"URL can only contain lowercase letters, numbers, and hyphens" "URL can only contain lowercase letters, numbers, and hyphens"
), ),
timezone: z.string().optional(), timezone: z.string().optional(),
type: z
.array(z.enum(["uptime", "infrastructure"]))
.min(1, "At least one type is required"),
color: z.string().min(1, "Color is required"), color: z.string().min(1, "Color is required"),
monitors: z.array(z.string()).min(1, "At least one monitor is required"), monitors: z.array(z.string()).min(1, "At least one monitor is required"),
isPublished: z.boolean(), isPublished: z.boolean(),
showCharts: z.boolean(), showCharts: z.boolean(),
showUptimePercentage: z.boolean(), showUptimePercentage: z.boolean(),
showAdminLoginLink: z.boolean(), showAdminLoginLink: z.boolean(),
showInfrastructure: z.boolean(),
customCSS: z.string().optional(), customCSS: z.string().optional(),
logo: z logo: z
.object({ .object({
+1 -8
View File
@@ -288,13 +288,7 @@ const logoImageValidation = joi
.optional(); // Make entire object optional .optional(); // Make entire object optional
const statusPageValidation = joi.object({ const statusPageValidation = joi.object({
type: joi type: joi.string().valid("uptime").required(),
.alternatives()
.try(
joi.string().valid("uptime", "infrastructure"),
joi.array().items(joi.string().valid("uptime", "infrastructure"))
)
.required(),
isPublished: joi.bool(), isPublished: joi.bool(),
companyName: joi companyName: joi
.string() .string()
@@ -323,7 +317,6 @@ const statusPageValidation = joi.object({
showUptimePercentage: joi.boolean(), showUptimePercentage: joi.boolean(),
showCharts: joi.boolean(), showCharts: joi.boolean(),
showAdminLoginLink: joi.boolean(), showAdminLoginLink: joi.boolean(),
showInfrastructure: joi.boolean(),
}); });
const settingsValidation = joi.object({ const settingsValidation = joi.object({
+22 -56
View File
@@ -46,8 +46,7 @@
"sendTestEmail": "Send test email", "sendTestEmail": "Send test email",
"exportToJSON": "Export to JSON", "exportToJSON": "Export to JSON",
"importFromJSON": "Import from JSON", "importFromJSON": "Import from JSON",
"clearFilters": "Clear filters", "clearFilters": "Clear filters"
"removeUser": "Remove user"
}, },
"charts": { "charts": {
"labels": { "labels": {
@@ -401,23 +400,12 @@
}, },
"status": { "status": {
"down": "down", "down": "down",
"breached": "threshold exceeded", "breached": "breached",
"initializing": "initializing", "initializing": "initializing",
"maintenance": "maintenance", "maintenance": "maintenance",
"paused": "paused", "paused": "paused",
"total": "total", "total": "total",
"up": "up" "up": "up"
},
"monitorTypes": {
"optionDocker": "Docker",
"optionGame": "Game",
"optionHttp": "HTTP(S)",
"optionPing": "Ping",
"optionPort": "Port",
"optionPagespeed": "PageSpeed",
"optionHardware": "Infrastructure",
"optionGrpc": "gRPC",
"optionWebSocket": "WebSocket"
} }
} }
}, },
@@ -499,10 +487,6 @@
"grpcServiceName": { "grpcServiceName": {
"label": "Service name", "label": "Service name",
"placeholder": "e.g. my.service.v1 (leave empty for overall health)" "placeholder": "e.g. my.service.v1 (leave empty for overall health)"
},
"wsUrl": {
"label": "WebSocket URL",
"placeholder": "wss://example.com/socket"
} }
}, },
"title": "General settings", "title": "General settings",
@@ -514,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.", "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.", "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.", "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." "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": { "ignoreTls": {
"description": "Configure TLS/SSL certificate validation for HTTPS connections.", "description": "Configure TLS/SSL certificate validation for HTTPS connections.",
"option": { "option": {
@@ -545,12 +540,17 @@
}, },
"type": { "type": {
"description": "Select the type of check to perform", "description": "Select the type of check to perform",
"optionDocker": "Docker",
"optionDockerDescription": "Use Docker to monitor if a container is running.", "optionDockerDescription": "Use Docker to monitor if a container is running.",
"optionGame": "Game",
"optionGameDescription": "Monitor if a specific game server is online.", "optionGameDescription": "Monitor if a specific game server is online.",
"optionGrpc": "gRPC",
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.", "optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.", "optionHttp": "HTTP(S)",
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.", "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
"optionPing": "Ping",
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.", "optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
"optionPort": "Port",
"optionPortDescription": "Monitor if a specific port on a server is open.", "optionPortDescription": "Monitor if a specific port on a server is open.",
"title": "Type" "title": "Type"
}, },
@@ -631,12 +631,6 @@
"title": "Roles", "title": "Roles",
"description": "Assign roles to the user. Multiple roles can be selected." "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": { "incidents": {
@@ -1045,7 +1039,7 @@
}, },
"stats": { "stats": {
"title": "Monitor history", "title": "Monitor history",
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.", "description": "Define how long you want to retain historical data. You can also clear all existing data.",
"option": { "option": {
"clear": { "clear": {
"label": "Clear all stats. This is irreversible." "label": "Clear all stats. This is irreversible."
@@ -1056,16 +1050,6 @@
"description": "This cannot be undone" "description": "This cannot be undone"
} }
}, },
"retention": {
"title": "Check retention",
"description": "Set how long check data is retained before being automatically cleaned up.",
"option": {
"days": {
"label": "Retention period (days)",
"unlimited": "Unlimited"
}
}
},
"thresholds": { "thresholds": {
"title": "Global Thresholds", "title": "Global Thresholds",
"description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.", "description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.",
@@ -1182,22 +1166,7 @@
}, },
"monitorsList": { "monitorsList": {
"chartTypeHeatmap": "Heatmap", "chartTypeHeatmap": "Heatmap",
"chartTypeHistogram": "Histogram", "chartTypeHistogram": "Histogram"
"noData": "No data available",
"infrastructure": {
"title": "Infrastructure",
"cpu": "CPU",
"memory": "RAM",
"memoryText": "Memory",
"disk": "Disk",
"usage": "Usage",
"used": "Used",
"total": "Total"
},
"uptime": {
"title": "Uptime",
"responseTime": "Response time"
}
}, },
"statusBar": { "statusBar": {
"allDown": "All systems are down", "allDown": "All systems are down",
@@ -1289,9 +1258,6 @@
}, },
"showAdminLoginLink": { "showAdminLoginLink": {
"label": "Show admin login link" "label": "Show admin login link"
},
"showInfrastructure": {
"label": "Show infrastructure metrics"
} }
} }
} }
+1 -1
View File
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
export default defineConfig(({}) => { export default defineConfig(({}) => {
let version = "3.5.1"; let version = "3.5.0";
return { return {
base: "/", base: "/",
+1 -4
View File
@@ -32,14 +32,11 @@ export default [
languageOptions: { languageOptions: {
parser: tseslint.parser, parser: tseslint.parser,
parserOptions: { parserOptions: {
projectService: { projectService: true,
allowDefaultProject: ["jest.config.ts"],
},
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
}, },
rules: { rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types "@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing "mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
}, },
+135 -172
View File
@@ -510,24 +510,42 @@
"summary": "Get monitors by team", "summary": "Get monitors by team",
"description": "Get monitors filtered by team with optional parameters", "description": "Get monitors filtered by team with optional parameters",
"parameters": [ "parameters": [
{
"name": "status",
"in": "query",
"description": "Filter by monitor status (up/down)",
"schema": {
"type": "boolean"
}
},
{ {
"name": "type", "name": "type",
"in": "query", "in": "query",
"description": "Filter by monitor type", "description": "Filter by monitor type",
"schema": { "schema": {
"oneOf": [ "type": "string",
{ "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"]
"type": "string", }
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"] },
}, {
{ "name": "page",
"type": "array", "in": "query",
"items": { "description": "Page number for pagination",
"type": "string", "schema": {
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"] "type": "integer",
} "minimum": 1,
} "default": 1
] }
},
{
"name": "rowsPerPage",
"in": "query",
"description": "Number of monitors per page",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 10
} }
}, },
{ {
@@ -537,6 +555,15 @@
"schema": { "schema": {
"type": "string" "type": "string"
} }
},
{
"name": "field",
"in": "query",
"description": "Field to filter on",
"schema": {
"type": "string",
"enum": ["name", "url", "description"]
}
} }
], ],
"responses": { "responses": {
@@ -729,7 +756,7 @@
} }
] ]
}, },
"patch": { "put": {
"tags": ["monitors"], "tags": ["monitors"],
"summary": "Update monitor", "summary": "Update monitor",
"description": "Update an existing monitor", "description": "Update an existing monitor",
@@ -3541,19 +3568,22 @@
}, },
"CreateMonitorRequest": { "CreateMonitorRequest": {
"type": "object", "type": "object",
"required": ["name", "type", "url"], "required": ["name", "description", "type", "url", "interval"],
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"minLength": 1,
"maxLength": 100,
"example": "My Website Monitor" "example": "My Website Monitor"
}, },
"description": { "description": {
"type": "string", "type": "string",
"maxLength": 500,
"example": "Monitors the main website homepage" "example": "Monitors the main website homepage"
}, },
"type": { "type": {
"type": "string", "type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"], "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"],
"example": "http" "example": "http"
}, },
"url": { "url": {
@@ -3563,34 +3593,14 @@
}, },
"interval": { "interval": {
"type": "integer", "type": "integer",
"minimum": 30,
"maximum": 3600,
"example": 300, "example": 300,
"description": "Check interval in seconds" "description": "Check interval in seconds"
}, },
"statusWindowSize": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
},
"statusWindowThreshold": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 60
},
"isActive": { "isActive": {
"type": "boolean"
},
"ignoreTlsErrors": {
"type": "boolean", "type": "boolean",
"default": false "default": true
},
"useAdvancedMatching": {
"type": "boolean",
"default": false
},
"port": {
"type": "integer"
}, },
"notifications": { "notifications": {
"type": "array", "type": "array",
@@ -3599,62 +3609,51 @@
}, },
"description": "Array of notification IDs to associate with this monitor" "description": "Array of notification IDs to associate with this monitor"
}, },
"secret": { "httpOptions": {
"type": "string" "type": "object",
}, "properties": {
"jsonPath": { "method": {
"type": "string" "type": "string",
}, "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"],
"expectedValue": { "default": "GET"
"type": "string" },
}, "headers": {
"matchMethod": { "type": "object",
"type": "string", "additionalProperties": {
"enum": ["equal", "include", "regex", ""] "type": "string"
}, }
"cpuAlertThreshold": { },
"type": "number" "body": {
}, "type": "string",
"memoryAlertThreshold": { "description": "Request body for POST/PUT requests"
"type": "number" },
}, "timeout": {
"diskAlertThreshold": { "type": "integer",
"type": "number" "minimum": 1000,
}, "maximum": 30000,
"tempAlertThreshold": { "default": 5000,
"type": "number" "description": "Request timeout in milliseconds"
}, }
"gameId": {
"type": "string"
},
"grpcServiceName": {
"type": "string",
"default": ""
},
"selectedDisks": {
"type": "array",
"items": {
"type": "string"
} }
}, },
"group": { "assertions": {
"type": "string",
"nullable": true,
"maxLength": 50
},
"geoCheckEnabled": {
"type": "boolean"
},
"geoCheckLocations": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "object",
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"] "properties": {
"type": {
"type": "string",
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
},
"comparison": {
"type": "string",
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
},
"value": {
"type": "string"
}
}
} }
},
"geoCheckInterval": {
"type": "integer",
"minimum": 300000
} }
} }
}, },
@@ -3662,32 +3661,21 @@
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"type": "string"
},
"type": {
"type": "string", "type": "string",
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] "minLength": 1,
}, "maxLength": 100
"url": {
"type": "string"
},
"statusWindowSize": {
"type": "integer",
"minimum": 1,
"maximum": 20,
"default": 5
},
"statusWindowThreshold": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 60
}, },
"description": { "description": {
"type": "string" "type": "string",
"maxLength": 500
}, },
"interval": { "interval": {
"type": "integer" "type": "integer",
"minimum": 30,
"maximum": 3600
},
"isActive": {
"type": "boolean"
}, },
"notifications": { "notifications": {
"type": "array", "type": "array",
@@ -3695,72 +3683,47 @@
"type": "string" "type": "string"
} }
}, },
"secret": { "httpOptions": {
"type": "string" "type": "object",
}, "properties": {
"ignoreTlsErrors": { "method": {
"type": "boolean" "type": "string",
}, "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"]
"useAdvancedMatching": { },
"type": "boolean" "headers": {
}, "type": "object",
"jsonPath": { "additionalProperties": {
"type": "string" "type": "string"
}, }
"expectedValue": { },
"type": "string" "body": {
}, "type": "string"
"matchMethod": { },
"type": "string", "timeout": {
"enum": ["equal", "include", "regex", ""] "type": "integer",
}, "minimum": 1000,
"port": { "maximum": 30000
"type": "integer", }
"minimum": 1,
"maximum": 65535
},
"cpuAlertThreshold": {
"type": "number"
},
"memoryAlertThreshold": {
"type": "number"
},
"diskAlertThreshold": {
"type": "number"
},
"tempAlertThreshold": {
"type": "number"
},
"gameId": {
"type": "string"
},
"grpcServiceName": {
"type": "string"
},
"selectedDisks": {
"type": "array",
"items": {
"type": "string"
} }
}, },
"group": { "assertions": {
"type": "string",
"nullable": true,
"maxLength": 50
},
"geoCheckEnabled": {
"type": "boolean"
},
"geoCheckLocations": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string", "type": "object",
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"] "properties": {
"type": {
"type": "string",
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
},
"comparison": {
"type": "string",
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
},
"value": {
"type": "string"
}
}
} }
},
"geoCheckInterval": {
"type": "integer",
"minimum": 300000
} }
} }
}, },
+705 -657
View File
File diff suppressed because it is too large. Load diff
+4 -6
View File
@@ -8,14 +8,14 @@
"test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand", "test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand",
"dev": "nodemon --exec tsx src/index.js", "dev": "nodemon --exec tsx src/index.js",
"start": "node --watch ./dist/index.js", "start": "node --watch ./dist/index.js",
"build": "tsc && tsc-alias && cp -r src/templates dist/templates", "build": "tsc && tsc-alias",
"lint": "eslint .", "lint": "eslint .",
"lint-fix": "eslint --fix .", "lint-fix": "eslint --fix .",
"format": "prettier --write .", "format": "prettier --write .",
"format-check": "prettier --check ." "format-check": "prettier --check ."
}, },
"lint-staged": { "lint-staged": {
"**/*": "prettier --write" "**/*": "npm run format"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -38,7 +38,7 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-rate-limit": "8.0.1", "express-rate-limit": "8.0.1",
"gamedig": "5.2.0", "gamedig": "^5.3.1",
"got": "14.4.7", "got": "14.4.7",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"helmet": "^8.0.0", "helmet": "^8.0.0",
@@ -51,14 +51,13 @@
"mjml": "^5.0.0-alpha.4", "mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3", "mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "8.0.1", "nodemailer": "^6.9.14",
"ping": "0.4.4", "ping": "0.4.4",
"sharp": "0.33.5", "sharp": "0.33.5",
"ssl-checker": "2.0.10", "ssl-checker": "2.0.10",
"super-simple-scheduler": "1.4.5", "super-simple-scheduler": "1.4.5",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"winston": "^3.13.0", "winston": "^3.13.0",
"ws": "^8.19.0",
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
@@ -79,7 +78,6 @@
"@types/papaparse": "^5.5.2", "@types/papaparse": "^5.5.2",
"@types/ping": "0.4.4", "@types/ping": "0.4.4",
"@types/swagger-ui-express": "4.1.8", "@types/swagger-ui-express": "4.1.8",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1", "@typescript-eslint/parser": "^8.56.1",
"c8": "10.1.3", "c8": "10.1.3",
+4 -18
View File
@@ -4,7 +4,7 @@ import cors from "cors";
import helmet from "helmet"; import helmet from "helmet";
import compression from "compression"; import compression from "compression";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import swaggerUi, { type JsonObject } from "swagger-ui-express"; import swaggerUi from "swagger-ui-express";
import { handleErrors } from "@/middleware/handleErrors.js"; import { handleErrors } from "@/middleware/handleErrors.js";
import { generalApiLimiter } from "@/middleware/rateLimiter.js"; import { generalApiLimiter } from "@/middleware/rateLimiter.js";
import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js"; import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js";
@@ -24,7 +24,7 @@ export const createApp = ({
controllers: InitializedControllers; controllers: InitializedControllers;
envSettings: EnvConfig; envSettings: EnvConfig;
frontendPath: string; frontendPath: string;
openApiSpec: JsonObject; openApiSpec: any;
}) => { }) => {
const allowedOrigin = envSettings.clientHost; const allowedOrigin = envSettings.clientHost;
const app = express(); const app = express();
@@ -74,22 +74,8 @@ export const createApp = ({
}, },
}) })
); );
// Swagger UI — dynamically set server URL from request // Swagger UI
app.use("/api-docs", swaggerUi.serve, (req: express.Request, res: express.Response, next: express.NextFunction) => { app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
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);
});
app.use("/api/v1/health", (req, res) => { app.use("/api/v1/health", (req, res) => {
res.json({ res.json({
+1 -1
View File
@@ -31,7 +31,7 @@ export interface InitializedControllers {
export const initializeControllers = (services: InitializedServices): InitializedControllers => { export const initializeControllers = (services: InitializedServices): InitializedControllers => {
return { return {
authController: new AuthController(services.userService), authController: new AuthController(services.userService),
monitorController: new MonitorController(services.monitorService, services.notificationsService), monitorController: new MonitorController(services.monitorService),
settingsController: new SettingsController(services.settingsService, services.emailService), settingsController: new SettingsController(services.settingsService, services.emailService),
checkController: new CheckController(services.checkService), checkController: new CheckController(services.checkService),
geoCheckController: new GeoCheckController(services.geoChecksService), geoCheckController: new GeoCheckController(services.geoChecksService),
+1 -4
View File
@@ -1,9 +1,6 @@
import type { Application } from "express";
import { createVerifyJWT } from "../middleware/verifyJWT.js"; import { createVerifyJWT } from "../middleware/verifyJWT.js";
import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js"; import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js";
import { authApiLimiter } from "../middleware/rateLimiter.js"; import { authApiLimiter } from "../middleware/rateLimiter.js";
import type { InitializedControllers } from "./controllers.js";
import type { InitializedServices } from "./services.js";
import AuthRoutes from "../routes/authRoute.js"; import AuthRoutes from "../routes/authRoute.js";
import InviteRoutes from "../routes/inviteRoute.js"; import InviteRoutes from "../routes/inviteRoute.js";
@@ -20,7 +17,7 @@ import NotificationRoutes from "../routes/notificationRoute.js";
import IncidentRoutes from "../routes/incidentRoute.js"; import IncidentRoutes from "../routes/incidentRoute.js";
export const setupRoutes = (app: Application, controllers: InitializedControllers, services: InitializedServices) => { export const setupRoutes = (app: any, controllers: Record<string, any>, services: Record<string, any>) => {
const verifyJWT = createVerifyJWT(services.settingsService); const verifyJWT = createVerifyJWT(services.settingsService);
const authRoutes = new AuthRoutes(controllers.authController, verifyJWT); const authRoutes = new AuthRoutes(controllers.authController, verifyJWT);
const monitorRoutes = new MonitorRoutes(controllers.monitorController); const monitorRoutes = new MonitorRoutes(controllers.monitorController);
+52 -96
View File
@@ -1,70 +1,35 @@
import MongoDB from "../db/MongoDB.js"; 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 { import {
// Service classes
NetworkService,
EmailService,
BufferService,
GlobalPingService,
SuperSimpleQueue,
SuperSimpleQueueHelper,
NotificationsService, NotificationsService,
StatusService, StatusService,
NotificationMessageBuilder,
MonitorService,
StatusPageService,
UserService,
CheckService,
GeoChecksService,
DiagnosticService,
InviteService,
MaintenanceWindowService,
IncidentService,
// Notification providers
WebhookProvider, WebhookProvider,
SlackProvider, SlackProvider,
EmailProvider, EmailProvider,
DiscordProvider, DiscordProvider,
PagerDutyProvider, PagerDutyProvider,
MatrixProvider, MatrixProvider,
TeamsProvider,
// Interfaces
INetworkService,
IEmailService,
IBufferService,
ISuperSimpleQueue,
INotificationsService, INotificationsService,
IStatusService,
IMonitorService,
IUserService,
ICheckService,
IGeoChecksService,
IDiagnosticService,
IInviteService,
IMaintenanceWindowService,
IStatusPageService,
IIncidentService,
INotificationMessageBuilder,
ISettingsService,
EnvConfig,
} from "@/service/index.js"; } from "@/service/index.js";
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
// Network providers import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js"; import UserService from "../service/business/userService.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; import CheckService from "../service/business/checkService.js";
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; import GeoChecksService from "../service/business/geoChecksService.js";
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js"; import GlobalPingService from "../service/infrastructure/globalPingService.js";
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js"; import DiagnosticService from "../service/business/diagnosticService.js";
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js"; import InviteService from "../service/business/inviteService.js";
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js"; import { MonitorService } from "@/service/index.js";
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js"; import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js"; import IncidentService from "../service/business/incidentService.js";
import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js";
// Third-party
import axios from "axios"; import axios from "axios";
import got from "got"; import got from "got";
import ping from "ping"; import ping from "ping";
import https from "https";
import Docker from "dockerode"; import Docker from "dockerode";
import net from "net"; import net from "net";
import fs from "fs"; import fs from "fs";
@@ -79,9 +44,8 @@ import { games, GameDig } from "gamedig";
import jmespath from "jmespath"; import jmespath from "jmespath";
import * as grpc from "@grpc/grpc-js"; import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader"; import * as protoLoader from "@grpc/proto-loader";
import WebSocket from "ws";
// Repositories // repositories
import { import {
MongoMonitorsRepository, MongoMonitorsRepository,
MongoChecksRepository, MongoChecksRepository,
@@ -110,23 +74,24 @@ import {
IMaintenanceWindowsRepository, IMaintenanceWindowsRepository,
} from "@/repositories/index.js"; } from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
export type InitializedServices = { export type InitializedServices = {
settingsService: ISettingsService; settingsService: any;
db: IDb; db: any;
networkService: INetworkService; networkService: any;
emailService: IEmailService; emailService: any;
bufferService: IBufferService; bufferService: any;
statusService: IStatusService; statusService: any;
jobQueue: ISuperSimpleQueue; jobQueue: any;
userService: IUserService; userService: any;
checkService: ICheckService; checkService: any;
geoChecksService: IGeoChecksService; geoChecksService: any;
diagnosticService: IDiagnosticService; diagnosticService: any;
inviteService: IInviteService; inviteService: any;
maintenanceWindowService: IMaintenanceWindowService; maintenanceWindowService: any;
monitorService: IMonitorService; monitorService: any;
incidentService: IIncidentService; incidentService: any;
logger: ILogger; logger: ILogger;
notificationsService: INotificationsService; notificationsService: INotificationsService;
statusPageService: IStatusPageService; statusPageService: IStatusPageService;
@@ -156,7 +121,7 @@ export const initializeServices = async ({
}: { }: {
logger: ILogger; logger: ILogger;
envSettings: EnvConfig; envSettings: EnvConfig;
settingsService: ISettingsService; settingsService: any;
settingsRepository: ISettingsRepository; settingsRepository: ISettingsRepository;
}): Promise<InitializedServices> => { }): Promise<InitializedServices> => {
// Create DB // Create DB
@@ -179,28 +144,22 @@ export const initializeServices = async ({
const teamsRepository = new MongoTeamsRepository(); const teamsRepository = new MongoTeamsRepository();
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
// Network providers const networkService = new NetworkService(
const pingProvider = new PingProvider(ping); axios,
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath)); got,
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger); https,
const hardwareProvider = new HardwareProvider(httpProvider); jmespath,
const dockerProvider = new DockerProvider(logger, Docker); GameDig as unknown as {
const portProvider = new PortProvider(net); query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }>;
const gameProvider = new GameProvider(logger, GameDig); },
const grpcProvider = new GrpcProvider(grpc, protoLoader); ping,
const webSocketProvider = new WebSocketProvider(WebSocket); logger,
Docker,
const networkService = new NetworkService(axios, logger, [ net,
pingProvider, settingsService,
httpProvider, grpc,
pageSpeedProvider, protoLoader
hardwareProvider, );
dockerProvider,
portProvider,
gameProvider,
grpcProvider,
webSocketProvider,
]);
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
const notificationMessageBuilder = new NotificationMessageBuilder(); const notificationMessageBuilder = new NotificationMessageBuilder();
@@ -222,14 +181,12 @@ export const initializeServices = async ({
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository); const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
// Notification providers
const webhookProvider = new WebhookProvider(logger); const webhookProvider = new WebhookProvider(logger);
const slackProvider = new SlackProvider(logger); const slackProvider = new SlackProvider(logger);
const emailProvider = new EmailProvider(emailService, logger); const emailProvider = new EmailProvider(emailService, logger);
const discordProvider = new DiscordProvider(logger); const discordProvider = new DiscordProvider(logger);
const pagerDutyProvider = new PagerDutyProvider(logger); const pagerDutyProvider = new PagerDutyProvider(logger);
const matrixProvider = new MatrixProvider(logger); const matrixProvider = new MatrixProvider(logger);
const teamsProvider = new TeamsProvider(logger);
const notificationsService = new NotificationsService( const notificationsService = new NotificationsService(
notificationsRepository, notificationsRepository,
@@ -240,7 +197,6 @@ export const initializeServices = async ({
discordProvider, discordProvider,
pagerDutyProvider, pagerDutyProvider,
matrixProvider, matrixProvider,
teamsProvider,
settingsService, settingsService,
logger, logger,
notificationMessageBuilder notificationMessageBuilder
@@ -252,7 +208,6 @@ export const initializeServices = async ({
statusService, statusService,
notificationsService, notificationsService,
checkService, checkService,
settingsService,
bufferService, bufferService,
incidentService, incidentService,
maintenanceWindowsRepository, maintenanceWindowsRepository,
@@ -309,6 +264,7 @@ export const initializeServices = async ({
const statusPageService = new StatusPageService(statusPagesRepository); const statusPageService = new StatusPageService(statusPagesRepository);
const services = { const services = {
//v1
settingsService, settingsService,
db, db,
networkService, networkService,
+47 -75
View File
@@ -1,6 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js"; import { requireTeamId, requireUserRoles } from "./controllerUtils.js";
import type { UserRole } from "@/types/user.js";
import { import {
registrationBodyValidation, registrationBodyValidation,
@@ -19,33 +20,15 @@ import {
editSuperadminUserByIdBodyValidation, editSuperadminUserByIdBodyValidation,
editUserPasswordByIdBodyValidation, editUserPasswordByIdBodyValidation,
} from "@/validation/userValidation.js"; } from "@/validation/userValidation.js";
import { IUserService } from "@/service/index.js";
const SERVICE_NAME = "authController"; const SERVICE_NAME = "authController";
export interface IAuthController { class AuthController {
registerUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
createUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
loginUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
editUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
checkSuperadminExists(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
requestRecovery(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
validateRecovery(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
resetPassword(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
deleteUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
deleteUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getAllUsers(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
editUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
editUserPasswordById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
}
class AuthController implements IAuthController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private userService: IUserService; private userService: any;
constructor(userService: IUserService) { constructor(userService: any) {
this.userService = userService; this.userService = userService;
} }
@@ -58,12 +41,11 @@ class AuthController implements IAuthController {
const newUser = req.body.user; const newUser = req.body.user;
const newUserToken = req.body.token; const newUserToken = req.body.token;
if (newUser?.email) { if (newUser?.email) {
const newUserEmail = requireUserEmail(newUser.email); newUser.email = newUser.email.toLowerCase();
newUser.email = newUserEmail.toLowerCase();
} }
const validatedBody = registrationBodyValidation.parse(newUser); registrationBodyValidation.parse(newUser);
const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null); const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file);
res.status(200).json({ res.status(200).json({
success: true, success: true,
msg: "User registered successfully", msg: "User registered successfully",
@@ -80,11 +62,11 @@ class AuthController implements IAuthController {
if (userData?.email) { if (userData?.email) {
userData.email = userData.email.toLowerCase(); userData.email = userData.email.toLowerCase();
} }
const validatedBody = createUserBodyValidation.parse(userData); createUserBodyValidation.parse(userData);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const actorRoles = requireUserRoles(req.user?.role); const actorRoles = requireUserRoles(req.user?.role) as UserRole[];
const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null); const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file);
res.status(201).json({ res.status(201).json({
success: true, success: true,
msg: "User created successfully", msg: "User created successfully",
@@ -118,10 +100,8 @@ class AuthController implements IAuthController {
editUser = async (req: Request, res: Response, next: NextFunction) => { editUser = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = editUserBodyValidation.parse(req.body); editUserBodyValidation.parse(req.body);
const userId = requireUserId(req.user?.id); const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
const userEmail = requireUserEmail(req.user?.email);
const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -190,15 +170,7 @@ class AuthController implements IAuthController {
deleteUser = async (req: Request, res: Response, next: NextFunction) => { deleteUser = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const userId = requireUserId(req.user?.id); await this.userService.deleteUser(req.user);
const teamId = requireTeamId(req.user?.teamId);
const roles = requireUserRoles(req.user?.role);
await this.userService.deleteUser({
userId,
teamId,
roles,
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "User deleted successfully", msg: "User deleted successfully",
@@ -208,23 +180,6 @@ class AuthController implements IAuthController {
} }
}; };
deleteUserById = async (req: Request, res: Response, next: NextFunction) => {
try {
const validatedParams = getUserByIdParamValidation.parse(req.params);
const targetUserId = validatedParams.userId;
const actorId = requireUserId(req.user?.id);
const actorTeamId = requireTeamId(req.user?.teamId);
const actorRoles = requireUserRoles(req.user?.role);
await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId });
return res.status(200).json({
success: true,
msg: "User removed successfully",
});
} catch (error) {
next(error);
}
};
getAllUsers = async (req: Request, res: Response, next: NextFunction) => { getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const allUsers = await this.userService.getAllUsers(); const allUsers = await this.userService.getAllUsers();
@@ -240,9 +195,19 @@ class AuthController implements IAuthController {
getUserById = async (req: Request, res: Response, next: NextFunction) => { getUserById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getUserByIdParamValidation.parse(req.params); getUserByIdParamValidation.parse(req.params);
const actorRoles = requireUserRoles(req.user?.role); const userId = req?.params?.userId;
const user = await this.userService.getUserById(actorRoles, validatedParams.userId); const roles = req?.user?.role;
if (!userId) {
throw new Error("No user ID in request");
}
if (!roles || roles.length === 0) {
throw new Error("No roles in request");
}
const user = await this.userService.getUserById(roles, userId);
return res.status(200).json({ success: true, msg: "ok", data: user }); return res.status(200).json({ success: true, msg: "ok", data: user });
} catch (error) { } catch (error) {
@@ -252,19 +217,24 @@ class AuthController implements IAuthController {
editUserById = async (req: Request, res: Response, next: NextFunction) => { editUserById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const actorRoles = requireUserRoles(req.user?.role); const roles = req?.user?.role;
const actorId = requireUserId(req.user?.id);
if (!actorRoles.includes("superadmin")) { if (!roles || !roles.includes("superadmin")) {
throw new AppError({ message: "Unauthorized", status: 403 }); throw new AppError({ message: "Unauthorized", status: 403 });
} }
const validatedParams = editUserByIdParamValidation.parse(req.params); const userId = req.params.userId as string;
// If this is superadmin self edit, allow "superadmin" role const user = { ...req.body };
const validatedBody =
validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body);
await this.userService.editUserById(validatedParams.userId, validatedBody); editUserByIdParamValidation.parse(req.params);
// If this is superadmin self edit, allow "superadmin" role
if (userId === req.user?.id) {
editSuperadminUserByIdBodyValidation.parse(req.body);
} else {
editUserByIdBodyValidation.parse(req.body);
}
await this.userService.editUserById(userId, user);
return res.status(200).json({ success: true, msg: "ok" }); return res.status(200).json({ success: true, msg: "ok" });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -273,14 +243,16 @@ class AuthController implements IAuthController {
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const actorRoles = requireUserRoles(req.user?.role); const roles = req?.user?.role;
if (!actorRoles.includes("superadmin")) { if (!roles || !roles.includes("superadmin")) {
throw new AppError({ message: "Unauthorized", status: 403 }); throw new AppError({ message: "Unauthorized", status: 403 });
} }
const validatedParams = editUserByIdParamValidation.parse(req.params); const userId = req.params.userId as string;
const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body); editUserByIdParamValidation.parse(req.params);
await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password); editUserPasswordByIdBodyValidation.parse(req.body);
const updatedPassword = req.body.password;
await this.userService.setPasswordByUserId(userId, updatedPassword);
return res.status(200).json({ success: true, msg: "Password reset successfully" }); return res.status(200).json({ success: true, msg: "Password reset successfully" });
} catch (error) { } catch (error) {
next(error); next(error);
+38 -47
View File
@@ -4,28 +4,18 @@ import {
getChecksParamValidation, getChecksParamValidation,
getChecksQueryValidation, getChecksQueryValidation,
getTeamChecksQueryValidation, getTeamChecksQueryValidation,
getChecksSummaryByTeamIdQueryValidation,
deleteChecksParamValidation, deleteChecksParamValidation,
deleteChecksByTeamIdParamValidation,
updateChecksTTLBodyValidation,
} from "@/validation/checkValidation.js"; } from "@/validation/checkValidation.js";
import { ICheckService } from "@/service/index.js";
import { requireTeamId } from "@/controllers/controllerUtils.js";
const SERVICE_NAME = "checkController"; const SERVICE_NAME = "checkController";
export interface ICheckController { class CheckController {
serviceName: string;
getChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getChecksByTeam: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getChecksSummaryByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteChecks: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class CheckController implements ICheckController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private checkService: ICheckService; private checkService: any;
constructor(checkService: ICheckService) { constructor(checkService: any) {
this.checkService = checkService; this.checkService = checkService;
} }
@@ -35,20 +25,13 @@ class CheckController implements ICheckController {
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getChecksParamValidation.parse(req.params); getChecksParamValidation.parse(req.params);
const validatedQuery = getChecksQueryValidation.parse(req.query); getChecksQueryValidation.parse(req.query);
const teamId = requireTeamId(req.user?.teamId);
const result = await this.checkService.getChecksByMonitor({ const result = await this.checkService.getChecksByMonitor({
monitorId: validatedParams.monitorId, monitorId: req?.params?.monitorId,
teamId, query: req?.query,
sortOrder: validatedQuery.sortOrder, teamId: req?.user?.teamId,
dateRange: validatedQuery.dateRange,
filter: validatedQuery.filter,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
status: validatedQuery.status,
}); });
return res.status(200).json({ return res.status(200).json({
@@ -63,16 +46,10 @@ class CheckController implements ICheckController {
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getTeamChecksQueryValidation.parse(req.query); getTeamChecksQueryValidation.parse(req.query);
const teamId = requireTeamId(req.user?.teamId);
const checkData = await this.checkService.getChecksByTeam({ const checkData = await this.checkService.getChecksByTeam({
teamId, teamId: req?.user?.teamId,
sortOrder: validatedQuery.sortOrder, query: req?.query,
dateRange: validatedQuery.dateRange,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
filter: validatedQuery.filter,
}); });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -86,11 +63,8 @@ class CheckController implements ICheckController {
getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query); const dateRange = req.query?.dateRange || "hour";
const teamId = requireTeamId(req.user?.teamId); const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange });
const dateRange = validatedQuery.dateRange ?? "hour";
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Checks summary retrieved successfully", msg: "Checks summary retrieved successfully",
@@ -103,12 +77,11 @@ class CheckController implements ICheckController {
deleteChecks = async (req: Request, res: Response, next: NextFunction) => { deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = deleteChecksParamValidation.parse(req.params); deleteChecksParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId);
const deletedCount = await this.checkService.deleteChecks({ const deletedCount = await this.checkService.deleteChecks({
monitorId: validatedParams.monitorId, monitorId: req.params.monitorId as string,
teamId, teamId: req?.user?.teamId,
}); });
return res.status(200).json({ return res.status(200).json({
@@ -123,9 +96,9 @@ class CheckController implements ICheckController {
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); deleteChecksByTeamIdParamValidation.parse(req.params);
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId }); const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -136,6 +109,24 @@ class CheckController implements ICheckController {
next(error); next(error);
} }
}; };
updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => {
try {
updateChecksTTLBodyValidation.parse(req.body);
await this.checkService.updateChecksTTL({
teamId: req?.user?.teamId,
ttl: req?.body?.ttl,
});
return res.status(200).json({
success: true,
msg: "Checks TTL updated successfully",
});
} catch (error) {
next(error);
}
};
} }
export default CheckController; export default CheckController;
+27 -22
View File
@@ -1,25 +1,24 @@
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; import { type MonitorType, MonitorTypes } from "@/types/index.js";
import sslChecker, { SSLDetails } from "ssl-checker";
type SSLCheckerType = typeof sslChecker;
export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => { const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
const monitorUrl = new URL(monitor.url); const monitorUrl = new URL(monitor.url);
const hostname = monitorUrl.hostname; const hostname = monitorUrl.hostname;
const cert = await checker(hostname); const cert = await sslChecker(hostname);
// Throw an error if no cert or if cert.validTo is not present
if (cert?.validTo === null || cert?.validTo === undefined) { if (cert?.validTo === null || cert?.validTo === undefined) {
throw new Error("Certificate not found"); throw new Error("Certificate not found");
} }
return cert; return cert;
}; };
export const requireString = (value: unknown, fieldName: string): string => { const requireString = (value: unknown, fieldName: string): string => {
if (typeof value === "string" && value.trim().length > 0) { if (typeof value === "string" && value.trim().length > 0) {
return value; return value;
} }
throw new AppError({ message: `${fieldName} is required`, status: 400 }); throw new AppError({ message: `${fieldName} is required`, status: 400 });
}; };
export const optionalString = (value: unknown, fieldName: string): string | undefined => { const optionalString = (value: unknown, fieldName: string): string | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -29,7 +28,7 @@ export const optionalString = (value: unknown, fieldName: string): string | unde
throw new AppError({ message: `${fieldName} must be a string`, status: 400 }); throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
}; };
export const optionalNumber = (value: unknown, fieldName: string): number | undefined => { const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -45,7 +44,7 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde
throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
}; };
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -63,7 +62,7 @@ export const optionalBoolean = (value: unknown, fieldName: string): boolean | un
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
}; };
export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
const parseSingle = (input: unknown): MonitorType => { const parseSingle = (input: unknown): MonitorType => {
if (typeof input !== "string") { if (typeof input !== "string") {
throw new AppError({ message: "Monitor type must be a string", status: 400 }); throw new AppError({ message: "Monitor type must be a string", status: 400 });
@@ -83,7 +82,7 @@ export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorTyp
return parseSingle(value); return parseSingle(value);
}; };
export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -93,36 +92,42 @@ export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 }); throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
}; };
export const requireTeamId = (teamId?: string): string => { const requireTeamId = (teamId?: string): string => {
if (!teamId) { if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 }); throw new AppError({ message: "Team ID is required", status: 400 });
} }
return teamId; return teamId;
}; };
export const requireUserId = (userId?: string): string => { const requireUserId = (userId?: string): string => {
if (!userId) { if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 }); throw new AppError({ message: "User ID is required", status: 400 });
} }
return userId; return userId;
}; };
export const requireUserEmail = (userEmail?: string): string => { const requireUserEmail = (userEmail?: string): string => {
if (!userEmail) { if (!userEmail) {
throw new AppError({ message: "User email is required", status: 400 }); throw new AppError({ message: "User email is required", status: 400 });
} }
return userEmail; return userEmail;
}; };
export const requireFirstName = (firstName?: string): string => { export const requireUserRoles = (userRoles?: string[]): string[] => {
if (!firstName) {
throw new AppError({ message: "First name is required", status: 400 });
}
return firstName;
};
export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => {
if (!userRoles || userRoles.length === 0) { if (!userRoles || userRoles.length === 0) {
throw new AppError({ message: "User roles are required", status: 400 }); throw new AppError({ message: "User roles are required", status: 400 });
} }
return userRoles; return userRoles;
}; };
export {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
requireUserId,
requireUserEmail,
};
@@ -1,18 +1,12 @@
import { IDiagnosticService } from "@/service/index.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
const SERVICE_NAME = "diagnosticController"; const SERVICE_NAME = "diagnosticController";
class DiagnosticController {
export interface IDiagnosticController {
getSystemStats: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class DiagnosticController implements IDiagnosticController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private diagnosticService: IDiagnosticService; private diagnosticService: any;
constructor(diagnosticService: IDiagnosticService) { constructor(diagnosticService: any) {
this.diagnosticService = diagnosticService; this.diagnosticService = diagnosticService;
} }
+6 -27
View File
@@ -1,15 +1,10 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js"; import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
import type { IGeoChecksService } from "@/service/business/geoChecksService.js"; import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
import { requireTeamId } from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js";
const SERVICE_NAME = "geoCheckController"; const SERVICE_NAME = "geoCheckController";
export interface IGeoCheckController { class GeoCheckController {
getGeoChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class GeoCheckController implements IGeoCheckController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private geoChecksService: IGeoChecksService; private geoChecksService: IGeoChecksService;
@@ -23,29 +18,13 @@ class GeoCheckController implements IGeoCheckController {
getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getChecksParamValidation.parse(req.params); getChecksParamValidation.parse(req.params);
const validatedQuery = getChecksQueryValidation.parse(req.query); getChecksQueryValidation.parse(req.query);
if (!validatedQuery.continent || validatedQuery.continent.length === 0) {
throw new AppError({
message: "At least one continent must be specified",
service: SERVICE_NAME,
method: "getGeoChecksByMonitor",
details: { monitorId: validatedParams.monitorId },
status: 400,
});
}
const teamId = requireTeamId(req.user?.teamId);
const result = await this.geoChecksService.getGeoChecksByMonitor({ const result = await this.geoChecksService.getGeoChecksByMonitor({
monitorId: validatedParams.monitorId, monitorId: req?.params?.monitorId as string,
teamId: teamId, query: req?.query,
sortOrder: validatedQuery.sortOrder, teamId: req?.user?.teamId as string,
dateRange: validatedQuery.dateRange,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
continent: validatedQuery.continent,
}); });
return res.status(200).json({ return res.status(200).json({
+21 -22
View File
@@ -1,37 +1,35 @@
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js"; import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js";
import { IIncidentService } from "@/service/index.js";
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
const SERVICE_NAME = "IncidentController"; const SERVICE_NAME = "incidentController";
export interface IIncidentController { class IncidentController {
getIncidentsByTeam: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; static SERVICE_NAME = SERVICE_NAME;
getIncidentSummary: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getIncidentById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; private incidentService: any;
resolveIncidentManually: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; constructor(incidentService: any) {
}
class IncidentController implements IIncidentController {
private incidentService: IIncidentService;
constructor(incidentService: IIncidentService) {
this.incidentService = incidentService; this.incidentService = incidentService;
} }
get serviceName() {
return IncidentController.SERVICE_NAME;
}
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query); const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const result = await this.incidentService.getIncidentsByTeam( const result = await this.incidentService.getIncidentsByTeam(
teamId, teamId,
validatedQuery.sortOrder, sortOrder,
validatedQuery.dateRange, dateRange,
validatedQuery.page, page,
validatedQuery.rowsPerPage, rowsPerPage,
validatedQuery.status, status,
validatedQuery.monitorId, monitorId,
validatedQuery.resolutionType resolutionType
); );
return res.status(200).json({ return res.status(200).json({
@@ -47,9 +45,10 @@ class IncidentController implements IIncidentController {
getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => { getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query);
const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit); const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
const summary = await this.incidentService.getIncidentSummary(teamId, limit);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Incident summary retrieved successfully", msg: "Incident summary retrieved successfully",
+7 -15
View File
@@ -1,19 +1,12 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js"; import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
import { IInviteService } from "@/service/index.js";
const SERVICE_NAME = "inviteController"; const SERVICE_NAME = "inviteController";
export interface IInviteController { class InviteController {
getInviteToken: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
sendInviteEmail: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
verifyInviteToken: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class InviteController implements IInviteController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private inviteService: IInviteService; private inviteService: any;
constructor(inviteService: IInviteService) { constructor(inviteService: any) {
this.inviteService = inviteService; this.inviteService = inviteService;
} }
@@ -41,9 +34,8 @@ class InviteController implements IInviteController {
sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const userRoles = requireUserRoles(req.user?.role); const userRoles = requireUserRoles(req?.user?.role);
const firstName = requireFirstName(req.user?.firstName);
const inviteRequest = req.body; const inviteRequest = req.body;
inviteRequest.teamId = teamId; inviteRequest.teamId = teamId;
@@ -51,7 +43,7 @@ class InviteController implements IInviteController {
const inviteToken = await this.inviteService.sendInviteEmail({ const inviteToken = await this.inviteService.sendInviteEmail({
invite: inviteRequest, invite: inviteRequest,
firstName, firstName: req?.user?.firstName,
userRoles, userRoles,
}); });
return res.status(200).json({ return res.status(200).json({
+3 -8
View File
@@ -1,16 +1,11 @@
import { ILogger } from "@/utils/logger.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
const SERVICE_NAME = "LogController"; const SERVICE_NAME = "LogController";
export interface ILogController {
getLogs: (req: Request, res: Response, next: NextFunction) => Promise<void>;
}
class LogController { class LogController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
get serviceName() { get serviceName() {
@@ -19,7 +14,7 @@ class LogController {
getLogs = async (req: Request, res: Response, next: NextFunction) => { getLogs = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const logs = this.logger.getLogs(); const logs = await this.logger.getLogs();
res.status(200).json({ res.status(200).json({
success: true, success: true,
msg: "Logs fetched successfully", msg: "Logs fetched successfully",
@@ -9,37 +9,26 @@ import {
deleteMaintenanceWindowByIdParamValidation, deleteMaintenanceWindowByIdParamValidation,
} from "@/validation/maintenanceWindowValidation.js"; } from "@/validation/maintenanceWindowValidation.js";
import { requireTeamId } from "@/controllers/controllerUtils.js"; import { requireTeamId } from "@/controllers/controllerUtils.js";
import { IMaintenanceWindowService } from "@/service/index.js";
export interface IMaintenanceWindowController { const SERVICE_NAME = "maintenanceWindowController";
createMaintenanceWindows: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getMaintenanceWindowById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; class MaintenanceWindowController {
getMaintenanceWindowsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; static SERVICE_NAME = SERVICE_NAME;
getMaintenanceWindowsByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; private maintenanceWindowService: any;
deleteMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; constructor(maintenanceWindowService: any) {
editMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class MaintenanceWindowController implements IMaintenanceWindowController {
private maintenanceWindowService: IMaintenanceWindowService;
constructor(maintenanceWindowService: IMaintenanceWindowService) {
this.maintenanceWindowService = maintenanceWindowService; this.maintenanceWindowService = maintenanceWindowService;
} }
get serviceName() {
return MaintenanceWindowController.SERVICE_NAME;
}
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body); createMaintenanceWindowBodyValidation.parse(req.body);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitorIDs = validatedBody.monitors; await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
const name = validatedBody.name;
const active = validatedBody.active ?? true;
const duration = validatedBody.duration;
const durationUnit = validatedBody.durationUnit;
const repeat = validatedBody.repeat;
const start = validatedBody.start;
const end = validatedBody.end;
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, monitorIDs, name, active, duration, durationUnit, repeat, start, end });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -51,11 +40,11 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
}; };
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params); getMaintenanceWindowByIdParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId }); const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -69,18 +58,11 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
teamId,
active: validatedQuery.active,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
field: validatedQuery.field,
order: validatedQuery.order,
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -94,12 +76,12 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({
monitorId: validatedParams.monitorId, monitorId: req.params.monitorId as string,
teamId, teamId,
}); });
@@ -114,11 +96,11 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
}; };
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params); deleteMaintenanceWindowByIdParamValidation.parse(req.params);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId }); await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -131,14 +113,14 @@ class MaintenanceWindowController implements IMaintenanceWindowController {
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params); editMaintenanceWindowByIdParamValidation.parse(req.params);
const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body); editMaintenanceByIdWindowBodyValidation.parse(req.body);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({
id: validatedParams.id, id: req.params.id as string,
body: validatedBody, body: req.body,
teamId, teamId,
}); });
return res.status(200).json({ return res.status(200).json({
+122 -136
View File
@@ -1,8 +1,9 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
import { import {
getMonitorByIdParamValidation, getMonitorByIdParamValidation,
getMonitorByIdQueryValidation, getMonitorByIdQueryValidation,
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation, getMonitorsByTeamIdQueryValidation,
getMonitorsWithChecksQueryValidation, getMonitorsWithChecksQueryValidation,
createMonitorBodyValidation, createMonitorBodyValidation,
@@ -11,48 +12,31 @@ import {
getCertificateParamValidation, getCertificateParamValidation,
getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation, getHardwareDetailsByIdQueryValidation,
getUptimeDetailsByIdParamValidation,
getUptimeDetailsByIdQueryValidation,
importMonitorsBodyValidation,
} from "@/validation/monitorValidation.js"; } from "@/validation/monitorValidation.js";
import sslChecker from "ssl-checker"; import sslChecker from "ssl-checker";
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js"; import {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
requireUserId,
} from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { IMonitorService, INotificationsService } from "@/service/index.js"; import { IMonitorService } from "@/service/index.js";
import { GeoContinent } from "@/types/geoCheck.js"; import { GeoContinent } from "@/types/geoCheck.js";
const SERVICE_NAME = "monitorController"; const SERVICE_NAME = "monitorController";
class MonitorController {
export interface IMonitorController {
getMonitorCertificate: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getUptimeDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getHardwareDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getPageSpeedDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getGeoChecksByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getMonitorById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
createMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
importMonitorsFromJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteAllMonitors: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
editMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
pauseMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
addDemoMonitors: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getMonitorsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getMonitorsWithChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
exportMonitorsToJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getAllGames: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getGroupsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
updateNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class MonitorController implements IMonitorController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private monitorService: IMonitorService; private monitorService: IMonitorService;
private notificationsService: INotificationsService;
constructor(monitorService: IMonitorService, notificationsService: INotificationsService) { constructor(monitorService: IMonitorService) {
this.monitorService = monitorService; this.monitorService = monitorService;
this.notificationsService = notificationsService;
} }
get serviceName() { get serviceName() {
@@ -61,9 +45,10 @@ class MonitorController implements IMonitorController {
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getCertificateParamValidation.parse(req.params); getCertificateParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const monitorId = requireString(req.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
const certificate = await fetchMonitorCertificate(sslChecker, monitor); const certificate = await fetchMonitorCertificate(sslChecker, monitor);
return res.status(200).json({ return res.status(200).json({
@@ -80,18 +65,16 @@ class MonitorController implements IMonitorController {
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => { getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params); const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query); const dateRange = requireString(req?.query?.dateRange, "dateRange");
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId);
const dateRange = validatedQuery.dateRange;
const teamId = requireTeamId(req.user?.teamId);
const data = await this.monitorService.getUptimeDetailsById({ const data = await this.monitorService.getUptimeDetailsById({
teamId, teamId,
monitorId, monitorId,
dateRange, dateRange,
normalize,
}); });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -105,12 +88,12 @@ class MonitorController implements IMonitorController {
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); getHardwareDetailsByIdParamValidation.parse(req.params);
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); getHardwareDetailsByIdQueryValidation.parse(req.query);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = validatedQuery.dateRange || "recent"; const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getHardwareDetailsById({ const data = await this.monitorService.getHardwareDetailsById({
teamId, teamId,
@@ -129,12 +112,12 @@ class MonitorController implements IMonitorController {
}; };
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); getHardwareDetailsByIdParamValidation.parse(req.params);
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); getHardwareDetailsByIdQueryValidation.parse(req.query);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = validatedQuery.dateRange || "recent"; const dateRange = requireString(req?.query?.dateRange, "dateRange");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getPageSpeedDetailsById({ const data = await this.monitorService.getPageSpeedDetailsById({
teamId, teamId,
@@ -154,14 +137,18 @@ class MonitorController implements IMonitorController {
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => { getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); getMonitorByIdParamValidation.parse(req.params);
const validatedQuery = getMonitorByIdQueryValidation.parse(req.query); getMonitorByIdQueryValidation.parse(req.query);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = validatedQuery.dateRange || "recent"; const dateRange = requireString(req?.query?.dateRange, "dateRange");
const continentParam = validatedQuery.continent; const continentParam = req?.query?.continent;
const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined; const continents = continentParam
const teamId = requireTeamId(req.user?.teamId); ? Array.isArray(continentParam)
? (continentParam as GeoContinent[])
: [continentParam as GeoContinent]
: undefined;
const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getGeoChecksByMonitorId({ const data = await this.monitorService.getGeoChecksByMonitorId({
teamId, teamId,
@@ -182,10 +169,11 @@ class MonitorController implements IMonitorController {
getMonitorById = async (req: Request, res: Response, next: NextFunction) => { getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); getMonitorByIdParamValidation.parse(req.params);
getMonitorByIdQueryValidation.parse(req.query);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
@@ -201,12 +189,12 @@ class MonitorController implements IMonitorController {
createMonitor = async (req: Request, res: Response, next: NextFunction) => { createMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createMonitorBodyValidation.parse(req.body); createMonitorBodyValidation.parse(req.body);
const userId = requireUserId(req.user?.id); const userId = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -220,10 +208,14 @@ class MonitorController implements IMonitorController {
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => { importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const userId = requireUserId(req.user?.id); const userId = requireUserId(req?.user?.id);
const validatedBody = importMonitorsBodyValidation.parse(req.body);
const monitors = validatedBody.monitors; const { monitors } = req.body;
if (!monitors || !Array.isArray(monitors)) {
throw new AppError({ message: "Invalid request: monitors array is required", status: 400 });
}
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors });
@@ -239,9 +231,9 @@ class MonitorController implements IMonitorController {
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); getMonitorByIdParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId);
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
@@ -257,7 +249,8 @@ class MonitorController implements IMonitorController {
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -271,12 +264,12 @@ class MonitorController implements IMonitorController {
editMonitor = async (req: Request, res: Response, next: NextFunction) => { editMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); getMonitorByIdParamValidation.parse(req.params);
const validatedBody = editMonitorBodyValidation.parse(req.body); editMonitorBodyValidation.parse(req.body);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -290,10 +283,10 @@ class MonitorController implements IMonitorController {
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = pauseMonitorParamValidation.parse(req.params); pauseMonitorParamValidation.parse(req.params);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
@@ -309,8 +302,8 @@ class MonitorController implements IMonitorController {
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const id = requireUserId(req.user?.id); const id = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId });
return res.status(200).json({ return res.status(200).json({
@@ -323,13 +316,32 @@ class MonitorController implements IMonitorController {
} }
}; };
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
try {
const { to } = req.body;
if (!to || typeof to !== "string") {
throw new AppError({ message: "Invalid 'to' email address", status: 400 });
}
const messageId = await this.monitorService.sendTestEmail({ to });
return res.status(200).json({
success: true,
msg: "Test email sent successfully",
data: { messageId },
});
} catch (error) {
next(error);
}
};
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); getMonitorsByTeamIdParamValidation.parse(req.params);
getMonitorsByTeamIdQueryValidation.parse(req.query);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const type = validatedQuery.type; const type = parseMonitorTypeFilter(req.query?.type);
const filter = validatedQuery.filter; const filter = optionalString(req.query?.filter, "filter");
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
@@ -345,16 +357,17 @@ class MonitorController implements IMonitorController {
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query); getMonitorsByTeamIdParamValidation.parse(req.params);
getMonitorsWithChecksQueryValidation.parse(req.query);
const limit = validatedQuery.limit; const explain = optionalBoolean(req?.query?.explain, "explain");
const page = validatedQuery.page; const limit = optionalNumber(req?.query?.limit, "limit");
const rowsPerPage = validatedQuery.rowsPerPage; const page = optionalNumber(req?.query?.page, "page");
const filter = validatedQuery.filter; const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
const field = validatedQuery.field; const filter = optionalString(req?.query?.filter, "filter");
const order = validatedQuery.order; const field = optionalString(req?.query?.field, "field");
const type = validatedQuery.type; const order = parseSortOrder(req?.query?.order);
const teamId = requireTeamId(req.user?.teamId); const type = parseMonitorTypeFilter(req?.query?.type);
const teamId = requireTeamId(req?.user?.teamId);
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
teamId, teamId,
@@ -365,6 +378,7 @@ class MonitorController implements IMonitorController {
filter, filter,
field, field,
order, order,
explain,
}); });
return res.status(200).json({ return res.status(200).json({
@@ -378,7 +392,11 @@ class MonitorController implements IMonitorController {
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const json = await this.monitorService.exportMonitorsToJSON({ teamId }); const json = await this.monitorService.exportMonitorsToJSON({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -406,7 +424,11 @@ class MonitorController implements IMonitorController {
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const groups = await this.monitorService.getGroupsByTeamId({ teamId }); const groups = await this.monitorService.getGroupsByTeamId({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -417,42 +439,6 @@ class MonitorController implements IMonitorController {
next(error); next(error);
} }
}; };
updateNotifications = async (req: Request, res: Response, next: NextFunction) => {
try {
updateNotificationsValidation.parse(req.body);
const teamId = requireTeamId(req.user?.teamId);
const { monitorIds, notificationIds, action } = req.body;
// Verify all requested notification IDs actually belong to this team
const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId);
const validNotificationIds = teamNotifications.map((n) => n.id);
const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id));
if (invalidIds.length > 0) {
throw new AppError({
message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`,
status: 403,
});
}
const modifiedCount = await this.monitorService.updateNotifications({
teamId,
monitorIds,
notificationIds,
action,
});
return res.status(200).json({
success: true,
msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`,
data: { modifiedCount },
});
} catch (error) {
next(error);
}
};
} }
export default MonitorController; export default MonitorController;
@@ -1,30 +1,16 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { import { Notification } from "@/types/index.js";
createNotificationBodyValidation, import { createNotificationBodyValidation } from "@/validation/notificationValidation.js";
deleteNotificationParamValidation,
getNotificationByIdParamValidation,
testNotificationBodyValidation,
editNotificationParamValidation,
testAllNotificationsBodyValidation,
} from "@/validation/notificationValidation.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { INotificationsService } from "@/service/index.js";
import { requireTeamId, requireUserId } from "./controllerUtils.js";
import { IMonitorsRepository } from "@/repositories/index.js"; import { IMonitorsRepository } from "@/repositories/index.js";
import { INotificationsService } from "@/service/index.js";
import { requireTeamId } from "./controllerUtils.js";
const SERVICE_NAME = "NotificationController"; const SERVICE_NAME = "NotificationController";
export interface INotificationController { class NotificationController {
testNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>; static SERVICE_NAME = SERVICE_NAME;
createNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getNotificationsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
deleteNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
getNotificationById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
editNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
testAllNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
}
class NotificationController implements INotificationController {
private notificationsService: INotificationsService; private notificationsService: INotificationsService;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
@@ -32,9 +18,13 @@ class NotificationController implements INotificationController {
this.monitorsRepository = monitorsRepository; this.monitorsRepository = monitorsRepository;
} }
get serviceName() {
return NotificationController.SERVICE_NAME;
}
testNotification = async (req: Request, res: Response, next: NextFunction) => { testNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const notification = testNotificationBodyValidation.parse(req.body); const notification: Notification = req.body;
const success = await this.notificationsService.sendTestNotification(notification); const success = await this.notificationsService.sendTestNotification(notification);
if (!success) { if (!success) {
@@ -44,7 +34,6 @@ class NotificationController implements INotificationController {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification sent successfully", msg: "Notification sent successfully",
details: { service: SERVICE_NAME },
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -53,12 +42,23 @@ class NotificationController implements INotificationController {
createNotification = async (req: Request, res: Response, next: NextFunction) => { createNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createNotificationBodyValidation.parse(req.body); createNotificationBodyValidation.parse(req.body);
const teamId = requireTeamId(req.user?.teamId); const body = req.body;
const userId = requireUserId(req.user?.id);
const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const userId = req?.user?.id;
if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 });
}
body.userId = userId;
body.teamId = teamId;
const notification = await this.notificationsService.createNotification(body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification created successfully", msg: "Notification created successfully",
@@ -71,7 +71,11 @@ class NotificationController implements INotificationController {
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); const notifications = await this.notificationsService.findNotificationsByTeamId(teamId);
return res.status(200).json({ return res.status(200).json({
@@ -86,10 +90,17 @@ class NotificationController implements INotificationController {
deleteNotification = async (req: Request, res: Response, next: NextFunction) => { deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
const validatedParams = deleteNotificationParamValidation.parse(req.params); if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
await this.notificationsService.deleteById(validatedParams.id, teamId); const notificationId = req.params.id as string;
if (!notificationId) {
throw new AppError({ message: "Notification ID is required", status: 400 });
}
await this.notificationsService.deleteById(notificationId, teamId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification deleted successfully", msg: "Notification deleted successfully",
@@ -102,9 +113,12 @@ class NotificationController implements INotificationController {
getNotificationById = async (req: Request, res: Response, next: NextFunction) => { getNotificationById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const validatedParams = getNotificationByIdParamValidation.parse(req.params); const notificationId = req.params.id as string;
if (!notificationId) {
throw new AppError({ message: "Notification ID is required", status: 400 });
}
const notification = await this.notificationsService.findById(validatedParams.id, teamId); const notification = await this.notificationsService.findById(notificationId, teamId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -118,13 +132,14 @@ class NotificationController implements INotificationController {
editNotification = async (req: Request, res: Response, next: NextFunction) => { editNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createNotificationBodyValidation.parse(req.body); createNotificationBodyValidation.parse(req.body);
const validatedParams = editNotificationParamValidation.parse(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const notificationId = validatedParams.id; const notificationId = req.params.id as string;
if (!notificationId) {
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); throw new AppError({ message: "Notification ID is required", status: 400 });
}
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification updated successfully", msg: "Notification updated successfully",
@@ -137,13 +152,15 @@ class NotificationController implements INotificationController {
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = testAllNotificationsBodyValidation.parse(req.body); const monitorId = req.body.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorsRepository.findById(monitorId, teamId);
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
const notifications = monitor.notifications || [];
const notifications = monitor.notifications;
if (notifications.length === 0) { if (notifications.length === 0) {
throw new AppError({ message: "No notifications", status: 400 }); throw new AppError({ message: "No notifications", status: 400 });
} }
+28 -11
View File
@@ -1,19 +1,11 @@
import { ISuperSimpleQueue } from "@/service/index.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
const SERVICE_NAME = "JobQueueController"; const SERVICE_NAME = "JobQueueController";
export interface IJobQueueController { class JobQueueController {
getMetrics(req: Request, res: Response, next: NextFunction): Promise<void>;
getJobs(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getAllMetrics(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
flushQueue(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
}
class JobQueueController implements IJobQueueController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private jobQueue: ISuperSimpleQueue; private jobQueue: any;
constructor(jobQueue: ISuperSimpleQueue) { constructor(jobQueue: any) {
this.jobQueue = jobQueue; this.jobQueue = jobQueue;
} }
@@ -61,6 +53,18 @@ class JobQueueController implements IJobQueueController {
} }
}; };
addJob = async (req: Request, res: Response, next: NextFunction) => {
try {
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.status(200).json({
success: true,
msg: "Job added to queue successfully",
});
} catch (error) {
next(error);
}
};
flushQueue = async (req: Request, res: Response, next: NextFunction) => { flushQueue = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const result = await this.jobQueue.flushQueues(); const result = await this.jobQueue.flushQueues();
@@ -73,5 +77,18 @@ class JobQueueController implements IJobQueueController {
next(error); next(error);
} }
}; };
checkQueueHealth = async (req: Request, res: Response, next: NextFunction) => {
try {
const stuckQueues = await this.jobQueue.checkQueueHealth();
return res.status(200).json({
success: true,
msg: "Queue health checked successfully",
data: stuckQueues,
});
} catch (error) {
next(error);
}
};
} }
export default JobQueueController; export default JobQueueController;
+9 -21
View File
@@ -2,23 +2,14 @@ import { Request, Response, NextFunction } from "express";
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js"; import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js"; import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { IEmailService, ISettingsService } from "@/service/index.js";
import { Settings } from "@/types/settings.js";
const SERVICE_NAME = "SettingsController"; const SERVICE_NAME = "SettingsController";
export interface ISettingsController { class SettingsController {
serviceName: string;
getAppSettings(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
updateAppSettings(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
sendTestEmail(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
}
class SettingsController implements ISettingsController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private settingsService: ISettingsService; private settingsService: any;
private emailService: IEmailService; private emailService: any;
constructor(settingsService: ISettingsService, emailService: IEmailService) { constructor(settingsService: any, emailService: any) {
this.settingsService = settingsService; this.settingsService = settingsService;
this.emailService = emailService; this.emailService = emailService;
} }
@@ -27,11 +18,11 @@ class SettingsController implements ISettingsController {
return SettingsController.SERVICE_NAME; return SettingsController.SERVICE_NAME;
} }
buildAppSettings = (dbSettings: Settings) => { buildAppSettings = (dbSettings: any) => {
const sanitizedSettings: Record<string, unknown> = { ...dbSettings }; const sanitizedSettings = { ...dbSettings };
delete sanitizedSettings.version; delete sanitizedSettings.version;
delete sanitizedSettings.jwtSecret; delete sanitizedSettings.jwtSecret;
const returnSettings: Record<string, unknown | null> = { const returnSettings = {
pagespeedKeySet: false, pagespeedKeySet: false,
emailPasswordSet: false, emailPasswordSet: false,
settings: null, settings: null,
@@ -66,9 +57,9 @@ class SettingsController implements ISettingsController {
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = updateAppSettingsBodyValidation.parse(req.body); updateAppSettingsBodyValidation.parse(req.body);
const updatedSettings = await this.settingsService.updateDbSettings(validatedBody); const updatedSettings = await this.settingsService.updateDbSettings(req.body);
const returnSettings = this.buildAppSettings(updatedSettings); const returnSettings = this.buildAppSettings(updatedSettings);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -104,9 +95,6 @@ class SettingsController implements ISettingsController {
const context = { testName: "Monitoring System" }; const context = { testName: "Monitoring System" };
const html = await this.emailService.buildEmail("testEmailTemplate", context); const html = await this.emailService.buildEmail("testEmailTemplate", context);
if (!html) {
throw new AppError({ message: "Failed to build email template.", status: 500 });
}
const messageId = await this.emailService.sendEmail(to, subject, html, { const messageId = await this.emailService.sendEmail(to, subject, html, {
systemEmailHost, systemEmailHost,
systemEmailPort, systemEmailPort,
+2 -11
View File
@@ -15,16 +15,7 @@ import { NormalizeData } from "@/utils/dataUtils.js";
const SERVICE_NAME = "statusPageController"; const SERVICE_NAME = "statusPageController";
export interface IStatusPageController { class StatusPageController {
readonly serviceName: string;
createStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
updateStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getStatusPageByUrl(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
getStatusPagesByTeamId(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
deleteStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
}
class StatusPageController implements IStatusPageController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private statusPageService: IStatusPageService; private statusPageService: IStatusPageService;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
@@ -75,7 +66,7 @@ class StatusPageController implements IStatusPageController {
if (statusPage === null) { if (statusPage === null) {
throw new AppError({ message: "Status page not found", status: 404 }); throw new AppError({ message: "Status page not found", status: 404 });
} }
res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Status page updated successfully", msg: "Status page updated successfully",
data: statusPage, data: statusPage,
-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 mongoose from "mongoose";
import AppSettings from "@/db/models/AppSettings.js"; import AppSettings from "./models/AppSettings.js";
import { runMigrations } from "@/db/migration/index.js"; import { runMigrations } from "./migration/index.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js"; import { EnvConfig } from "@/service/system/settingsService.js";
const SERVICE_NAME = "MongoDB"; const SERVICE_NAME = "MongoDB";
import { IDb } from "@/db/IDb.js"; class MongoDB {
class MongoDB implements IDb {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: ILogger;
@@ -48,12 +46,12 @@ class MongoDB implements IDb {
}); });
await runMigrations(); await runMigrations();
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "connect", method: "connect",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
@@ -65,12 +63,12 @@ class MongoDB implements IDb {
await mongoose.disconnect(); await mongoose.disconnect();
this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" }); this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" });
return; return;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "disconnect", method: "disconnect",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
} }
}; };
@@ -1,5 +1,4 @@
import mongoose from "mongoose"; import mongoose from "mongoose";
import type { Document, AnyBulkWriteOperation } from "mongodb";
const CHECKS_COLLECTION = "checks"; const CHECKS_COLLECTION = "checks";
const BACKUP_COLLECTION = "checks_backup"; const BACKUP_COLLECTION = "checks_backup";
@@ -86,7 +85,7 @@ const createTimeSeriesCollection = async (backedUp: boolean) => {
} }
}; };
const validateDocument = (doc: Document): boolean => { const validateDocument = (doc: any): boolean => {
if (!doc.createdAt) { if (!doc.createdAt) {
return false; return false;
} }
@@ -96,7 +95,7 @@ const validateDocument = (doc: Document): boolean => {
return true; return true;
}; };
const storeFailedDocument = async (doc: Document, reason: string) => { const storeFailedDocument = async (doc: any, reason: string) => {
const db = getDb(); const db = getDb();
await db.collection(FAILED_DOCS_COLLECTION).insertOne({ await db.collection(FAILED_DOCS_COLLECTION).insertOne({
originalDoc: doc, originalDoc: doc,
@@ -125,8 +124,8 @@ const migrateBackupData = async (backedUp: boolean): Promise<MigrationStats> =>
stats.totalSource = await source.countDocuments(); stats.totalSource = await source.countDocuments();
const cursor = source.find().addCursorFlag("noCursorTimeout", true); const cursor = source.find().addCursorFlag("noCursorTimeout", true);
const operations: AnyBulkWriteOperation<Document>[] = []; const operations: any[] = [];
const invalidDocs: Document[] = []; const invalidDocs: any[] = [];
while (await cursor.hasNext()) { while (await cursor.hasNext()) {
const doc = await cursor.next(); const doc = await cursor.next();
@@ -140,7 +139,6 @@ const migrateBackupData = async (backedUp: boolean): Promise<MigrationStats> =>
} }
const { _id, monitorId, teamId, type, ...rest } = doc; const { _id, monitorId, teamId, type, ...rest } = doc;
void _id;
const metadata = { monitorId, teamId, type }; const metadata = { monitorId, teamId, type };
operations.push({ insertOne: { document: { ...rest, metadata } } }); operations.push({ insertOne: { document: { ...rest, metadata } } });
@@ -42,10 +42,10 @@ export async function cleanupDuplicateMonitorStats(): Promise<void> {
const monitorId = duplicate._id; const monitorId = duplicate._id;
const { ids, updatedAts } = duplicate; const { ids, updatedAts } = duplicate;
type DocPair = { id: string; updatedAt: Date }; type DocPair = { id: any; updatedAt: Date };
// Create array of {id, updatedAt} pairs and sort by updatedAt descending // Create array of {id, updatedAt} pairs and sort by updatedAt descending
const docs: DocPair[] = ids.map((id: string, index: number) => ({ const docs: DocPair[] = ids.map((id: any, index: number) => ({
id, id,
updatedAt: updatedAts[index], updatedAt: updatedAts[index],
})); }));
@@ -1,61 +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) {
const value = monitor.get(field);
if (value === 0 || value === 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;
}
}
@@ -1,45 +0,0 @@
import mongoose from "mongoose";
import { logger } from "@/utils/logger.js";
import StatusPageModel from "../models/StatusPage.js";
export async function migrateStatusPageTypeToArray(): Promise<void> {
const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray";
try {
logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" });
const db = mongoose.connection.db;
if (!db) {
throw new Error("Database connection is not initialized");
}
const result = await StatusPageModel.updateMany(
{
$expr: {
$eq: [{ $type: "$type" }, "string"], // only scalar string, not existing array
},
},
[
{
$set: {
type: ["$type"],
},
},
]
);
if (result.modifiedCount === 0) {
logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" });
return;
}
logger.info({
service: SERVICE_NAME,
message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` });
throw error;
}
}
+2 -7
View File
@@ -1,10 +1,7 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js"; import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
import MigrationModel from "../models/Migration.js"; import MigrationModel from "../models/Migration.js";
import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
import type { ILogger } from "@/utils/logger.js";
type MigrationEntry = { type MigrationEntry = {
name: string; name: string;
@@ -15,11 +12,9 @@ const migrations: MigrationEntry[] = [
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold }, { name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats }, { name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
]; ];
const runMigrations = async (logger?: ILogger) => { const runMigrations = async (logger?: { info: Function; error: Function }) => {
try { try {
logger?.info({ message: "Running migrations", service: "Migrations" }); logger?.info({ message: "Running migrations", service: "Migrations" });
for (const migration of migrations) { for (const migration of migrations) {
@@ -46,7 +41,7 @@ const runMigrations = async (logger?: ILogger) => {
logger?.info({ message: "Migrations completed", service: "Migrations" }); logger?.info({ message: "Migrations completed", service: "Migrations" });
} catch (error) { } catch (error) {
const err = error as Error; const err = error as Error;
logger?.error({ message: "Migration failed", service: "Migrations", details: { error: err?.message }, stack: err?.stack }); logger?.error({ message: "Migration failed", service: "Migrations", details: err?.message, stack: err?.stack });
throw error; throw error;
} }
}; };
+16 -2
View File
@@ -21,10 +21,13 @@ type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
type: MonitorType; type: MonitorType;
}; };
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "createdAt" | "updatedAt"> & { type CheckDocumentBase = Omit<Check, "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt"> & {
metadata: CheckMetadataDocument; metadata: CheckMetadataDocument;
expiry: Date;
ackAt?: Date | null;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
__v: number;
}; };
interface CheckDocument extends CheckDocumentBase { interface CheckDocument extends CheckDocumentBase {
@@ -222,7 +225,18 @@ const CheckSchema = new Schema<CheckDocument>(
message: { message: {
type: String, type: String,
}, },
expiry: {
type: Date,
default: Date.now,
},
ack: {
type: Boolean,
default: false,
},
ackAt: {
type: Date,
default: undefined,
},
cpu: { cpu: {
type: cpuSchema, type: cpuSchema,
default: () => ({}), default: () => ({}),
+1 -1
View File
@@ -1,5 +1,5 @@
import { Schema, model, type Types } from "mongoose"; import { Schema, model, type Types } from "mongoose";
import type { MaintenanceWindow } from "@/types/maintenanceWindow.js"; import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js";
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & { type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
monitorId: Types.ObjectId; monitorId: Types.ObjectId;
+8 -168
View File
@@ -1,18 +1,6 @@
import { Schema, model, Types } from "mongoose"; import { Schema, model, Types, type UpdateQuery } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js"; import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
import type {
CheckAudits,
CheckCaptureInfo,
CheckCpuInfo,
CheckDiskInfo,
CheckErrorInfo,
CheckHostInfo,
CheckMemoryInfo,
CheckNetworkInterfaceInfo,
GotTimings,
ILighthouseAudit,
} from "@/types/check.js";
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date }; type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
@@ -35,167 +23,13 @@ interface MonitorDocument extends MonitorDocumentBase {
updatedAt: Date; updatedAt: Date;
} }
const snapshotTimingPhasesSchema = new Schema<GotTimings["phases"]>(
{
wait: { type: Number },
dns: { type: Number },
tcp: { type: Number },
tls: { type: Number },
request: { type: Number },
firstByte: { type: Number },
download: { type: Number },
total: { type: Number },
},
{ _id: false }
);
const snapshotTimingsSchema = new Schema<GotTimings>(
{
start: { type: Number },
socket: { type: Number },
lookup: { type: Number },
connect: { type: Number },
secureConnect: { type: Number },
upload: { type: Number },
response: { type: Number },
end: { type: Number },
phases: { type: snapshotTimingPhasesSchema },
},
{ _id: false }
);
const snapshotCpuSchema = new Schema<CheckCpuInfo>(
{
physical_core: { type: Number },
logical_core: { type: Number },
frequency: { type: Number },
current_frequency: { type: Number },
temperature: { type: [Number] },
free_percent: { type: Number },
usage_percent: { type: Number },
},
{ _id: false }
);
const snapshotMemorySchema = new Schema<CheckMemoryInfo>(
{
total_bytes: { type: Number },
available_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
},
{ _id: false }
);
const snapshotDiskSchema = new Schema<CheckDiskInfo>(
{
device: { type: String },
mountpoint: { type: String },
total_bytes: { type: Number },
free_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
total_inodes: { type: Number },
free_inodes: { type: Number },
used_inodes: { type: Number },
inodes_usage_percent: { type: Number },
read_bytes: { type: Number },
write_bytes: { type: Number },
read_time: { type: Number },
write_time: { type: Number },
},
{ _id: false }
);
const snapshotHostSchema = new Schema<CheckHostInfo>(
{
os: { type: String },
platform: { type: String },
kernel_version: { type: String },
pretty_name: { type: String },
},
{ _id: false }
);
const snapshotErrorSchema = new Schema<CheckErrorInfo>(
{
metric: { type: [String] },
err: { type: String },
},
{ _id: false }
);
const snapshotCaptureSchema = new Schema<CheckCaptureInfo>(
{
version: { type: String },
mode: { type: String },
},
{ _id: false }
);
const snapshotNetworkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
{
name: { type: String },
bytes_sent: { type: Number },
bytes_recv: { type: Number },
packets_sent: { type: Number },
packets_recv: { type: Number },
err_in: { type: Number },
err_out: { type: Number },
drop_in: { type: Number },
drop_out: { type: Number },
fifo_in: { type: Number },
fifo_out: { type: Number },
},
{ _id: false }
);
const snapshotLighthouseAuditSchema = new Schema<ILighthouseAudit>(
{
id: { type: String },
title: { type: String },
score: { type: Number },
displayValue: { type: String },
numericValue: { type: Number },
numericUnit: { type: String },
},
{ _id: false }
);
const snapshotAuditsSchema = new Schema<CheckAudits>(
{
cls: { type: snapshotLighthouseAuditSchema },
si: { type: snapshotLighthouseAuditSchema },
fcp: { type: snapshotLighthouseAuditSchema },
lcp: { type: snapshotLighthouseAuditSchema },
tbt: { type: snapshotLighthouseAuditSchema },
},
{ _id: false }
);
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>( const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
{ {
id: { type: String, required: true }, id: { type: String, required: true },
status: { type: Boolean, required: true }, status: { type: Boolean, required: true },
responseTime: { type: Number },
timings: { type: snapshotTimingsSchema },
statusCode: { type: Number },
message: { type: String },
cpu: { type: snapshotCpuSchema },
memory: { type: snapshotMemorySchema },
disk: { type: [snapshotDiskSchema] },
host: { type: snapshotHostSchema },
errors: { type: [snapshotErrorSchema] },
capture: { type: snapshotCaptureSchema },
net: { type: [snapshotNetworkInterfaceSchema] },
accessibility: { type: Number },
bestPractices: { type: Number },
seo: { type: Number },
performance: { type: Number },
audits: { type: snapshotAuditsSchema },
createdAt: { type: Date, required: true }, createdAt: { type: Date, required: true },
}, },
{ _id: false } { _id: false, strict: false }
); );
const MonitorSchema = new Schema<MonitorDocument>( const MonitorSchema = new Schema<MonitorDocument>(
@@ -259,6 +93,12 @@ const MonitorSchema = new Schema<MonitorDocument>(
type: String, type: String,
enum: ["equal", "include", "regex", ""], enum: ["equal", "include", "regex", ""],
}, },
basicAuthUser: {
type: String,
},
basicAuthPassword: {
type: String,
},
url: { url: {
type: String, type: String,
required: true, required: true,
+1 -1
View File
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
}, },
type: { type: {
type: String, type: String,
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[], enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[],
required: true, required: true,
}, },
notificationName: { notificationName: {
+2 -6
View File
@@ -43,9 +43,9 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
required: true, required: true,
}, },
type: { type: {
type: [String], type: String,
required: true, required: true,
default: ["uptime"], default: "uptime",
enum: StatusPageTypes, enum: StatusPageTypes,
}, },
companyName: { companyName: {
@@ -100,10 +100,6 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showInfrastructure: {
type: Boolean,
default: false,
},
customCSS: { customCSS: {
type: String, type: String,
default: "", default: "",
+4 -3
View File
@@ -9,7 +9,7 @@ import fs from "fs";
import { runMigrations } from "./db/migration/index.js"; import { runMigrations } from "./db/migration/index.js";
import Logger, { ILogger } from "@/utils/logger.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"; import { MongoSettingsRepository } from "./repositories/index.js";
const SERVICE_NAME = "Server"; const SERVICE_NAME = "Server";
@@ -50,8 +50,9 @@ const startApp = async () => {
openApiSpec, openApiSpec,
}); });
const server = app.listen(env.PORT, () => { const port = 52345;
logger.info({ message: `Server started on port:${env.PORT}` }); const server = app.listen(port, () => {
logger.info({ message: `Server started on port:${port}` });
}); });
initShutdownListener(server, services); initShutdownListener(server, services);
+6 -9
View File
@@ -1,18 +1,15 @@
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
import { logger } from "@/utils/logger.js"; import { logger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js";
const handleErrors = (error: unknown, req: Request, res: Response, _next: NextFunction) => { const handleErrors = (error: any, req: Request, res: Response, next: NextFunction) => {
const status = error instanceof AppError ? error.status || 500 : 500; const status = error.status || 500;
const message = error instanceof AppError ? error.message : "Server error"; const message = error.message || "Server error";
const service = error instanceof AppError ? error.service : "unknownService"; const service = error.service || "unknownService";
const method = error instanceof AppError ? error.method : "unknownMethod";
logger.error({ logger.error({
message: message, message: message,
service: service, service: service,
method: method, method: error.method,
stack: error instanceof AppError ? error.stack : undefined, stack: error.stack,
details: error instanceof AppError ? error.details : undefined,
}); });
res.status(status).json({ res.status(status).json({
status, status,
+5 -12
View File
@@ -1,9 +1,7 @@
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from "express";
import DOMPurify from "isomorphic-dompurify"; import DOMPurify from "isomorphic-dompurify";
export function sanitizeInput(input: string, options?: object): string; export const sanitizeInput = (input: any, options = {}) => {
export function sanitizeInput(input: unknown, options?: object): unknown;
export function sanitizeInput(input: unknown, options = {}): unknown {
if (typeof input !== "string") { if (typeof input !== "string") {
return input; return input;
} }
@@ -17,9 +15,9 @@ export function sanitizeInput(input: unknown, options = {}): unknown {
}; };
return DOMPurify.sanitize(input, defaultConfig); return DOMPurify.sanitize(input, defaultConfig);
} };
export const sanitizeObject = (obj: unknown, options = {}): unknown => { export const sanitizeObject = (obj: Record<string, any>, options = {}): Record<string, any> => {
if (typeof obj !== "object" || obj === null) { if (typeof obj !== "object" || obj === null) {
return obj; return obj;
} }
@@ -28,7 +26,7 @@ export const sanitizeObject = (obj: unknown, options = {}): unknown => {
return obj.map((item) => sanitizeObject(item, options)); return obj.map((item) => sanitizeObject(item, options));
} }
const sanitized: Record<string, unknown> = {}; const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
if (typeof value === "string") { if (typeof value === "string") {
sanitized[key] = sanitizeInput(value, options); sanitized[key] = sanitizeInput(value, options);
@@ -54,12 +52,7 @@ export const sanitizeBody = (options = {}): ((req: Request, res: Response, next:
export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => { export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
if (req.query && typeof req.query === "object") { if (req.query && typeof req.query === "object") {
for (const key of Object.keys(req.query)) { req.query = sanitizeObject(req.query, options);
const value = req.query[key];
if (typeof value === "string") {
req.query[key] = sanitizeInput(value, options);
}
}
} }
next(); next();
}; };
+7 -13
View File
@@ -2,17 +2,12 @@ import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import type { ISettingsService } from "@/service/system/settingsService.js"; import { Settings } from "@/types/settings.js";
import type { User } from "@/types/user.js";
const SERVICE_NAME = "verifyJWT"; const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer "; const TOKEN_PREFIX = "Bearer ";
const isUser = (payload: unknown): payload is User => { export const createVerifyJWT = (settingsService: { getSettings: () => Settings }) => {
return typeof payload === "object" && payload !== null && "id" in payload && "teamId" in payload && "role" in payload;
};
export const createVerifyJWT = (settingsService: ISettingsService) => {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
const token = req.headers["authorization"]; const token = req.headers["authorization"];
// Make sure a token is provided // Make sure a token is provided
@@ -36,22 +31,21 @@ export const createVerifyJWT = (settingsService: ISettingsService) => {
next(error); next(error);
return; return;
} }
jwt.verify(parsedToken, jwtSecret, (err: jwt.VerifyErrors | null, decoded: jwt.JwtPayload | string | undefined) => { jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => {
if (err) { if (err) {
const error = new AppError({ const error = new AppError({
message: err instanceof Error ? err.message : "Failed to authenticate token", message: "Failed to authenticate token",
details: err,
status: 401, status: 401,
service: SERVICE_NAME, service: SERVICE_NAME,
details: err instanceof Error ? { error: err } : undefined,
method: "verifyJWT", method: "verifyJWT",
}); });
next(error); next(error);
return; return;
} else if (isUser(decoded)) { } else {
// Token is valid, carry on
req.user = decoded; req.user = decoded;
next(); next();
} else {
next(new AppError({ message: "Invalid token payload", status: 401, service: SERVICE_NAME, method: "verifyJWT" }));
} }
}); });
}; };
@@ -20,19 +20,12 @@ export interface IChecksRepository {
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: string, dateRange: string,
filter: string | undefined, filter: string,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined status: boolean | undefined
): Promise<ChecksQueryResult>; ): Promise<ChecksQueryResult>;
findByTeamId( findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise<ChecksQueryResult>;
sortOrder: string,
dateRange: string,
filter: string | undefined,
page: number,
rowsPerPage: number,
teamId: string
): Promise<ChecksQueryResult>;
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>; findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findByDateRangeAndMonitorId( findByDateRangeAndMonitorId(
monitorId: string, monitorId: string,
@@ -47,5 +40,4 @@ export interface IChecksRepository {
deleteByMonitorId(monitorId: string): Promise<number>; deleteByMonitorId(monitorId: string): Promise<number>;
deleteByTeamId(teamId: string): Promise<number>; deleteByTeamId(teamId: string): Promise<number>;
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>; deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
deleteOlderThan(date: Date): Promise<number>;
} }
@@ -13,10 +13,9 @@ import type {
GotTimings, GotTimings,
MonitorType, MonitorType,
} from "@/types/index.js"; } from "@/types/index.js";
import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose"; import mongoose from "mongoose";
import { getDateForRange } from "@/utils/dataUtils.js"; import { getDateForRange } from "@/utils/dataUtils.js";
import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "StatusService"; const SERVICE_NAME = "StatusService";
@@ -27,8 +26,8 @@ type HardwareUpChecks = { totalChecks: number };
class MongoChecksRepository implements IChecksRepository { class MongoChecksRepository implements IChecksRepository {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
@@ -47,6 +46,13 @@ class MongoChecksRepository implements IChecksRepository {
return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}; };
const toOptionalDateString = (value?: Date | string | null): string | undefined => {
if (!value) {
return undefined;
}
return toDateString(value);
};
const mapTimings = (timings?: GotTimings): GotTimings => { const mapTimings = (timings?: GotTimings): GotTimings => {
const phases = timings?.phases ?? { const phases = timings?.phases ?? {
wait: 0, wait: 0,
@@ -165,6 +171,9 @@ class MongoChecksRepository implements IChecksRepository {
timings: mapTimings(doc.timings), timings: mapTimings(doc.timings),
statusCode: doc.statusCode ?? 0, statusCode: doc.statusCode ?? 0,
message: doc.message ?? "", message: doc.message ?? "",
ack: doc.ack ?? false,
ackAt: toOptionalDateString(doc.ackAt),
expiry: toDateString(doc.expiry),
cpu: mapCpu(doc.cpu), cpu: mapCpu(doc.cpu),
memory: mapMemory(doc.memory), memory: mapMemory(doc.memory),
disk: mapDisks(doc.disk), disk: mapDisks(doc.disk),
@@ -177,6 +186,7 @@ class MongoChecksRepository implements IChecksRepository {
seo: doc.seo, seo: doc.seo,
performance: doc.performance, performance: doc.performance,
audits: mapAudits(doc.audits), audits: mapAudits(doc.audits),
__v: doc.__v ?? 0,
createdAt: toDateString(doc.createdAt), createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt), updatedAt: toDateString(doc.updatedAt),
}; };
@@ -221,13 +231,13 @@ class MongoChecksRepository implements IChecksRepository {
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: string, dateRange: string,
filter: string | undefined, filter: string,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined status: boolean | undefined
) => { ) => {
// Match // Match
const matchStage: Record<string, unknown> = { const matchStage: Record<string, any> = {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
...(typeof status !== "undefined" && { status }), ...(typeof status !== "undefined" && { status }),
...(getDateForRange(dateRange) && { ...(getDateForRange(dateRange) && {
@@ -279,7 +289,7 @@ class MongoChecksRepository implements IChecksRepository {
}; };
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => { findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
const matchStage: Record<string, unknown> = { const matchStage: Record<string, any> = {
"metadata.teamId": new mongoose.Types.ObjectId(teamId), "metadata.teamId": new mongoose.Types.ObjectId(teamId),
...(getDateForRange(dateRange) && { ...(getDateForRange(dateRange) && {
createdAt: { createdAt: {
@@ -348,7 +358,7 @@ class MongoChecksRepository implements IChecksRepository {
); );
const mapped = results.reduce<LatestChecksMap>((acc, { monitorId, docs }) => { const mapped = results.reduce<LatestChecksMap>((acc, { monitorId, docs }) => {
acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc)); acc[monitorId] = docs.map((doc: any) => this.toEntity(doc as CheckDocument));
return acc; return acc;
}, {}); }, {});
return mapped; return mapped;
@@ -402,31 +412,6 @@ class MongoChecksRepository implements IChecksRepository {
return result.deletedCount ?? 0; return result.deletedCount ?? 0;
}; };
deleteOlderThan = async (cutoffDate: Date, batchDays: number = 30): Promise<number> => {
// Find the oldest check that is older than the cutoff
const oldest = await CheckModel.findOne({ createdAt: { $lt: cutoffDate } })
.sort({ createdAt: 1 })
.select({ createdAt: 1 });
if (!oldest?.createdAt) return 0;
let totalDeleted = 0;
let batchStart = new Date(oldest.createdAt);
const batchMs = batchDays * 24 * 60 * 60 * 1000;
// Delete in 30 day chunks until we reach the cutoff
while (batchStart < cutoffDate) {
// Advance startTime by batchMs to get to the end of last batch
const nextStart = batchStart.getTime() + batchMs;
const batchEnd = new Date(Math.min(nextStart, cutoffDate.getTime()));
const result = await CheckModel.deleteMany({
createdAt: { $gte: batchStart, $lt: batchEnd },
});
totalDeleted += result.deletedCount ?? 0;
batchStart = batchEnd;
}
return totalDeleted;
};
private findUptimeDateRangeChecks = async ( private findUptimeDateRangeChecks = async (
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">, monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
monitorObjectId: mongoose.Types.ObjectId, monitorObjectId: mongoose.Types.ObjectId,
@@ -1,18 +1,17 @@
import { IGeoChecksRepository } from "./IGeoChecksRepository.js"; import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.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 { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js"; import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
import mongoose, { PipelineStage } from "mongoose"; import mongoose from "mongoose";
import { getDateForRange } from "@/utils/dataUtils.js"; import { getDateForRange } from "@/utils/dataUtils.js";
import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "GeoChecksRepository"; const SERVICE_NAME = "GeoChecksRepository";
class MongoGeoChecksRepository implements IGeoChecksRepository { class MongoGeoChecksRepository implements IGeoChecksRepository {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
@@ -31,13 +30,13 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}; };
const mapMetadata = (metadata: GeoCheckMetadataDocument): GeoCheckMetadata => ({ const mapMetadata = (metadata: any): GeoCheckMetadata => ({
monitorId: toStringId(metadata.monitorId), monitorId: toStringId(metadata.monitorId),
teamId: toStringId(metadata.teamId), teamId: toStringId(metadata.teamId),
type: metadata.type, type: metadata.type,
}); });
const mapResults = (results: GeoCheckResult[]): GeoCheckResult[] => { const mapResults = (results: any[]): GeoCheckResult[] => {
if (!results || !Array.isArray(results)) { if (!results || !Array.isArray(results)) {
return []; return [];
} }
@@ -89,12 +88,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
})) }))
); );
return docs.map((doc) => this.toEntity(doc)); return docs.map((doc) => this.toEntity(doc));
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Failed to createGeoChecks: ${error.message}` : "Failed to createGeoChecks", message: `Failed to createGeoChecks: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "createGeoChecks", method: "createGeoChecks",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
@@ -109,7 +108,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
continents?: GeoContinent[] continents?: GeoContinent[]
): Promise<FlatGeoChecksQueryResult> => { ): Promise<FlatGeoChecksQueryResult> => {
try { try {
const matchStage: Record<string, unknown> = { const matchStage: Record<string, any> = {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
...(getDateForRange(dateRange) && { ...(getDateForRange(dateRange) && {
createdAt: { createdAt: {
@@ -125,8 +124,22 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
skip = page * rowsPerPage; skip = page * rowsPerPage;
} }
if (continents && continents.length > 0) {
const pipeline: any[] = [
{ $match: matchStage },
{ $unwind: "$results" },
{
$match: {
"results.location.continent": { $in: continents },
},
},
];
} else {
const pipeline: any[] = [{ $match: matchStage }, { $unwind: "$results" }];
}
// Common pipeline stages for both paths // Common pipeline stages for both paths
const pipeline: PipelineStage[] = [ const pipeline: any[] = [
{ $match: matchStage }, { $match: matchStage },
{ $unwind: "$results" }, { $unwind: "$results" },
// Filter by continent if specified // Filter by continent if specified
@@ -160,7 +173,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
]; ];
// Count pipeline // Count pipeline
const countPipeline: PipelineStage[] = [ const countPipeline: any[] = [
{ $match: matchStage }, { $match: matchStage },
{ $unwind: "$results" }, { $unwind: "$results" },
...(continents && continents.length > 0 ...(continents && continents.length > 0
@@ -192,12 +205,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
})); }));
return { geoChecksCount, geoChecks }; return { geoChecksCount, geoChecks };
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error finding geo checks by monitor ID: ${error.message}` : "Error finding geo checks by monitor ID", message: `Error finding geo checks by monitor ID: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "findByMonitorId", method: "findByMonitorId",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
@@ -213,15 +226,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
}, },
}).sort({ createdAt: -1 }); }).sort({ createdAt: -1 });
return docs.map(this.toEntity); return docs.map(this.toEntity);
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: message: `Error finding geo checks by monitor ID and date range: ${error.message}`,
error instanceof Error
? `Error finding geo checks by monitor ID and date range: ${error.message}`
: "Error finding geo checks by monitor ID and date range",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "findByMonitorIdAndDateRange", method: "findByMonitorIdAndDateRange",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -235,7 +244,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
continents?: GeoContinent[] continents?: GeoContinent[]
): Promise<GroupedGeoCheck[]> => { ): Promise<GroupedGeoCheck[]> => {
try { try {
const pipeline: PipelineStage[] = [ const pipeline: any[] = [
// Match geo checks for this monitor in date range // Match geo checks for this monitor in date range
{ {
$match: { $match: {
@@ -315,12 +324,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
const results = await GeoCheckModel.aggregate(pipeline); const results = await GeoCheckModel.aggregate(pipeline);
return results as GroupedGeoCheck[]; return results as GroupedGeoCheck[];
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error finding grouped geo checks: ${error.message}` : "Error finding grouped geo checks", message: `Error finding grouped geo checks: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "findGroupedByMonitorIdAndDateRange", method: "findGroupedByMonitorIdAndDateRange",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -332,12 +340,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId), "metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
}); });
return result.deletedCount || 0; return result.deletedCount || 0;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error deleting geo checks by monitor ID: ${error.message}` : "Error deleting geo checks by monitor ID", message: `Error deleting geo checks by monitor ID: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "deleteByMonitorId", method: "deleteByMonitorId",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -349,12 +356,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
"metadata.teamId": new mongoose.Types.ObjectId(teamId), "metadata.teamId": new mongoose.Types.ObjectId(teamId),
}); });
return result.deletedCount || 0; return result.deletedCount || 0;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error deleting geo checks by team ID: ${error.message}` : "Error deleting geo checks by team ID", message: `Error deleting geo checks by team ID: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "deleteByTeamId", method: "deleteByTeamId",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -365,12 +371,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository {
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } }); const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
return result.deletedCount || 0; return result.deletedCount || 0;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? `Error deleting orphaned geo checks: ${error.message}` : "Error deleting orphaned geo checks", message: `Error deleting orphaned geo checks: ${error.message}`,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "deleteByMonitorIdsNotIn", method: "deleteByMonitorIdsNotIn",
stack: error instanceof Error ? error.stack : undefined,
}); });
throw error; throw error;
} }
@@ -32,8 +32,8 @@ class MongoIncidentRepository implements IIncidentsRepository {
status?: boolean; status?: boolean;
monitorId?: string; monitorId?: string;
resolutionType?: string; resolutionType?: string;
}): Record<string, unknown> { }): Record<string, any> {
const matchStage: Record<string, unknown> = { const matchStage: Record<string, any> = {
teamId: new mongoose.Types.ObjectId(teamId), teamId: new mongoose.Types.ObjectId(teamId),
...(status !== undefined && { status }), ...(status !== undefined && { status }),
...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }),
@@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository {
// fetch // fetch
findById(id: string, teamId: string): Promise<MaintenanceWindow>; findById(id: string, teamId: string): Promise<MaintenanceWindow>;
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>; findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise<MaintenanceWindow[]>; findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
// update // update
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>; updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
// delete // delete
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>; deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
// other // other
countByTeamId(teamId: string, active?: boolean): Promise<number>; countByTeamId(teamId: string, active: boolean): Promise<number>;
} }
@@ -1,7 +1,7 @@
import type { MaintenanceWindow } from "@/types/index.js"; import type { MaintenanceWindow } from "@/types/index.js";
import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js"; import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js";
import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js"; import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js";
import mongoose, { SortOrder } from "mongoose"; import mongoose from "mongoose";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository { class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository {
@@ -75,13 +75,13 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
findByTeamId = async ( findByTeamId = async (
teamId: string, teamId: string,
active: boolean,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
field?: string, field: string,
order?: string, order: string
active?: boolean
): Promise<MaintenanceWindow[]> => { ): Promise<MaintenanceWindow[]> => {
const maintenanceQuery: Record<string, unknown> = { teamId }; const maintenanceQuery: Record<string, any> = { teamId };
if (active !== undefined) maintenanceQuery.active = active; if (active !== undefined) maintenanceQuery.active = active;
@@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
} }
// Sorting // Sorting
const sort: Record<string, SortOrder> = {}; const sort: Record<string, any> = {};
if (field !== undefined && order !== undefined) { if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1; sort[field] = order === "asc" ? 1 : -1;
} }
@@ -126,8 +126,8 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
} }
return this.toEntity(deleted); return this.toEntity(deleted);
}; };
countByTeamId = async (teamId: string, active?: boolean) => { countByTeamId = async (teamId: string, active: boolean) => {
const maintenanceQuery: Record<string, unknown> = { teamId }; const maintenanceQuery: Record<string, any> = { teamId };
if (active !== undefined) maintenanceQuery.active = active; if (active !== undefined) maintenanceQuery.active = active;
@@ -41,7 +41,6 @@ export interface IMonitorsRepository {
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>; findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
findGroupsByTeamId(teamId: string): Promise<string[]>; findGroupsByTeamId(teamId: string): Promise<string[]>;
removeNotificationFromMonitors(notificationId: string): Promise<void>; removeNotificationFromMonitors(notificationId: string): Promise<void>;
updateNotifications(teamId: string, monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set"): Promise<number>;
deleteByTeamIdsNotIn(teamIds: string[]): Promise<number>; deleteByTeamIdsNotIn(teamIds: string[]): Promise<number>;
findAllMonitorIds(): Promise<string[]>; findAllMonitorIds(): Promise<string[]>;
} }
@@ -1,9 +1,8 @@
import { MonitorModel } from "@/db/models/index.js"; import { MonitorModel } from "@/db/models/index.js";
import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js"; import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js";
import type { Monitor, MonitorsSummary, CheckSnapshot } from "@/types/index.js"; import type { Monitor, MonitorsSummary, CheckSnapshot, Check } from "@/types/index.js";
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { MongoBulkWriteError } from "mongodb";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
class MongoMonitorsRepository implements IMonitorsRepository { class MongoMonitorsRepository implements IMonitorsRepository {
@@ -21,9 +20,12 @@ class MongoMonitorsRepository implements IMonitorsRepository {
try { try {
const inserted = await MonitorModel.insertMany(payload, { ordered: false }); const inserted = await MonitorModel.insertMany(payload, { ordered: false });
return this.mapDocuments(inserted); return this.mapDocuments(inserted);
} catch (error: unknown) { } catch (error: any) {
if (error instanceof MongoBulkWriteError && "insertedDocs" in error && Array.isArray(error.insertedDocs) && error.insertedDocs.length > 0) { if (error.name === "MongoBulkWriteError" || error.name === "BulkWriteError") {
return this.mapDocuments(error.insertedDocs); const insertedDocs = error.insertedDocs || [];
if (insertedDocs.length > 0) {
return this.mapDocuments(insertedDocs);
}
} }
throw error; throw error;
} }
@@ -44,7 +46,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
}; };
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => { findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type } = config ?? {}; const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {};
const query: Record<string, unknown> = { const query: Record<string, unknown> = {
teamId: new mongoose.Types.ObjectId(teamId), teamId: new mongoose.Types.ObjectId(teamId),
@@ -296,41 +298,6 @@ class MongoMonitorsRepository implements IMonitorsRepository {
await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } });
}; };
updateNotifications = async (
teamId: string,
monitorIds: string[],
notificationIds: string[],
action: "add" | "remove" | "set"
): Promise<number> => {
let objectIds;
let notificationObjectIds;
try {
objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
notificationObjectIds = notificationIds.map((id) => new mongoose.Types.ObjectId(id));
} catch {
throw new AppError({ message: "One or more monitor or notification IDs are invalid", status: 400 });
}
const filter = { _id: { $in: objectIds }, teamId: new mongoose.Types.ObjectId(teamId) };
let update;
switch (action) {
case "add":
update = { $addToSet: { notifications: { $each: notificationObjectIds } } };
break;
case "remove":
update = { $pull: { notifications: { $in: notificationObjectIds } } };
break;
case "set":
update = { $set: { notifications: notificationObjectIds } };
break;
default:
throw new AppError({ message: `Invalid action: ${action}`, status: 400 });
}
const result = await MonitorModel.updateMany(filter, update);
return result.modifiedCount;
};
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
if (!documents?.length) { if (!documents?.length) {
return []; return [];
@@ -387,7 +354,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000, geoCheckInterval: doc.geoCheckInterval ?? 300000,
@@ -396,7 +363,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
}; };
}; };
private toEntityWithChecks = (doc: MonitorDocument): Monitor => { private toEntityWithChecks = (doc: any): Monitor => {
const toStringId = (value: unknown): string => { const toStringId = (value: unknown): string => {
if (value instanceof mongoose.Types.ObjectId) { if (value instanceof mongoose.Types.ObjectId) {
return value.toString(); return value.toString();
@@ -446,7 +413,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000, geoCheckInterval: doc.geoCheckInterval ?? 300000,
@@ -1,11 +1,11 @@
import type { Settings, SettingsUpdate } from "@/types/index.js"; import type { Settings } from "@/types/index.js";
export interface ISettingsRepository { export interface ISettingsRepository {
// create // create
create(settings: Partial<Settings>): Promise<Settings>; create(settings: Partial<Settings>): Promise<Settings>;
// fetch // fetch
findSingleton(): Promise<Settings | null>; findSingleton(): Promise<Settings | null>;
// update // update
update(settings: SettingsUpdate): Promise<Settings>; update(settings: Partial<Settings>): Promise<Settings>;
// delete // delete
deleteLegacy: () => Promise<boolean>; deleteLegacy: () => Promise<boolean>;
} }
@@ -1,6 +1,6 @@
import mongoose, { type UpdateQuery } from "mongoose"; import mongoose from "mongoose";
import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js";
import type { Settings, SettingsUpdate } from "@/types/index.js"; import type { Settings } from "@/types/index.js";
import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js"; import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js";
class MongoSettingsRepository implements ISettingsRepository { class MongoSettingsRepository implements ISettingsRepository {
@@ -59,23 +59,27 @@ class MongoSettingsRepository implements ISettingsRepository {
return this.toEntity(settings); return this.toEntity(settings);
}; };
update = async (settings: SettingsUpdate) => { update = async (settings: Partial<Settings>) => {
const $set: Record<string, unknown> = {}; const update: Record<string, any> = { $set: {}, $unset: {} };
const $unset: Record<string, string> = {};
// Iterate through settings and separate into $set and $unset // Iterate through settings and separate into $set and $unset
Object.entries(settings).forEach(([key, value]) => { Object.entries(settings).forEach(([key, value]) => {
if (value === undefined || value === null) { if (value === undefined || value === "") {
$unset[key] = ""; // Unset undefined or empty string values
update.$unset[key] = "";
} else { } else {
$set[key] = value; // Set defined values
update.$set[key] = value;
} }
}); });
const update: UpdateQuery<AppSettingsDocument> = { // Clean up empty operators
...(Object.keys($set).length > 0 && { $set }), if (Object.keys(update.$set).length === 0) {
...(Object.keys($unset).length > 0 && { $unset }), delete update.$set;
}; }
if (Object.keys(update.$unset).length === 0) {
delete update.$unset;
}
await AppSettingsModel.findOneAndUpdate({}, update, { await AppSettingsModel.findOneAndUpdate({}, update, {
upsert: true, upsert: true,
@@ -57,7 +57,6 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
showCharts: doc.showCharts, showCharts: doc.showCharts,
showUptimePercentage: doc.showUptimePercentage, showUptimePercentage: doc.showUptimePercentage,
showAdminLoginLink: doc.showAdminLoginLink, showAdminLoginLink: doc.showAdminLoginLink,
showInfrastructure: doc.showInfrastructure,
customCSS: doc.customCSS, customCSS: doc.customCSS,
createdAt: this.toDateString(doc.createdAt), createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt), updatedAt: this.toDateString(doc.updatedAt),
@@ -72,8 +71,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
}; };
create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => { create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
const { logo, ...restData } = data; const { logo: _logo, ...restData } = data;
void logo;
const statusPage = new StatusPageModel({ const statusPage = new StatusPageModel({
...restData, ...restData,
userId, userId,
@@ -111,8 +109,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
image: Express.Multer.File | undefined, image: Express.Multer.File | undefined,
patch: Partial<StatusPage> & { removeLogo?: string } patch: Partial<StatusPage> & { removeLogo?: string }
): Promise<StatusPage> => { ): Promise<StatusPage> => {
const { logo, removeLogo, ...restPatch } = patch; const { logo: _logo, removeLogo, ...restPatch } = patch;
void logo;
const updateData: StatusPageUpdateData = { ...restPatch }; const updateData: StatusPageUpdateData = { ...restPatch };
if (image) { if (image) {
updateData.logo = { updateData.logo = {
@@ -117,7 +117,7 @@ class MongoUsersRepository implements IUsersRepository {
delete candidateUser.deleteProfileImage; delete candidateUser.deleteProfileImage;
const updateQuery: Record<string, unknown> = { $set: candidateUser }; const updateQuery: Record<string, any> = { $set: candidateUser };
if (unsetFields) { if (unsetFields) {
updateQuery.$unset = unsetFields; updateQuery.$unset = unsetFields;
} }
+3 -5
View File
@@ -1,15 +1,14 @@
import { Router, RequestHandler } from "express"; import { Router, RequestHandler } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import multer from "multer"; import multer from "multer";
import { IAuthController } from "@/controllers/authController.js";
const upload = multer(); const upload = multer();
class AuthRoutes { class AuthRoutes {
private router: Router; private router: Router;
private authController: IAuthController; private authController: any;
constructor(authController: IAuthController, verifyJWT: RequestHandler) { constructor(authController: any, verifyJWT: RequestHandler) {
this.router = Router(); this.router = Router();
this.authController = authController; this.authController = authController;
this.initRoutes(verifyJWT); this.initRoutes(verifyJWT);
@@ -27,10 +26,9 @@ class AuthRoutes {
this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers); 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.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", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById);
this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById); 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.patch("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
this.router.delete("/user", verifyJWT, this.authController.deleteUser); this.router.delete("/user", verifyJWT, this.authController.deleteUser);
+3 -3
View File
@@ -1,13 +1,12 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { ICheckController } from "@/controllers/checkController.js";
class CheckRoutes { class CheckRoutes {
private router: Router; private router: Router;
private checkController: ICheckController; private checkController: any;
constructor(checkController: ICheckController) { constructor(checkController: any) {
this.router = Router(); this.router = Router();
this.checkController = checkController; this.checkController = checkController;
this.initRoutes(); this.initRoutes();
@@ -16,6 +15,7 @@ class CheckRoutes {
initRoutes() { initRoutes() {
this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId); this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId);
this.router.get("/team", this.checkController.getChecksByTeam); this.router.get("/team", this.checkController.getChecksByTeam);
this.router.put("/team/ttl", isAllowed(["admin", "superadmin"]), this.checkController.updateChecksTTL);
this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId); this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId);
this.router.get("/:monitorId", this.checkController.getChecksByMonitor); this.router.get("/:monitorId", this.checkController.getChecksByMonitor);
this.router.delete("/:monitorId", this.checkController.deleteChecks); this.router.delete("/:monitorId", this.checkController.deleteChecks);
+2 -3
View File
@@ -1,12 +1,11 @@
import { RequestHandler, Router } from "express"; import { RequestHandler, Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { IDiagnosticController } from "@/controllers/diagnosticController.js";
class DiagnosticRoutes { class DiagnosticRoutes {
private router: Router; private router: Router;
private diagnosticController: IDiagnosticController; private diagnosticController: any;
constructor(diagnosticController: IDiagnosticController, verifyJWT: RequestHandler) { constructor(diagnosticController: any, verifyJWT: RequestHandler) {
this.router = Router(); this.router = Router();
this.diagnosticController = diagnosticController; this.diagnosticController = diagnosticController;
this.initRoutes(verifyJWT); this.initRoutes(verifyJWT);
+2 -3
View File
@@ -1,11 +1,10 @@
import { IGeoCheckController } from "@/controllers/geoCheckController.js";
import { Router } from "express"; import { Router } from "express";
class GeoCheckRoutes { class GeoCheckRoutes {
private router: Router; private router: Router;
private geoCheckController: IGeoCheckController; private geoCheckController: any;
constructor(geoCheckController: IGeoCheckController) { constructor(geoCheckController: any) {
this.router = Router(); this.router = Router();
this.geoCheckController = geoCheckController; this.geoCheckController = geoCheckController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,12 +1,11 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { IIncidentController } from "@/controllers/incidentController.js";
class IncidentRoutes { class IncidentRoutes {
private router: Router; private router: Router;
private incidentController: IIncidentController; private incidentController: any;
constructor(incidentController: IIncidentController) { constructor(incidentController: any) {
this.router = Router(); this.router = Router();
this.incidentController = incidentController; this.incidentController = incidentController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,12 +1,11 @@
import { RequestHandler, Router } from "express"; import { RequestHandler, Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { IInviteController } from "@/controllers/inviteController.js";
class InviteRoutes { class InviteRoutes {
private router: Router; private router: Router;
private inviteController: IInviteController; private inviteController: any;
constructor(inviteController: IInviteController, verifyJWT: RequestHandler) { constructor(inviteController: any, verifyJWT: RequestHandler) {
this.router = Router(); this.router = Router();
this.inviteController = inviteController; this.inviteController = inviteController;
this.initRoutes(verifyJWT); this.initRoutes(verifyJWT);
+3 -4
View File
@@ -1,11 +1,10 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "@/middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { ILogController } from "@/controllers/logController.js";
class LogRoutes { class LogRoutes {
private router: Router; private router: Router;
private logController: ILogController; private logController: any;
constructor(logController: ILogController) { constructor(logController: any) {
this.router = Router(); this.router = Router();
this.logController = logController; this.logController = logController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,11 +1,10 @@
import { IMaintenanceWindowController } from "@/controllers/maintenanceWindowController.js";
import { Router } from "express"; import { Router } from "express";
class MaintenanceWindowRoutes { class MaintenanceWindowRoutes {
private router: Router; private router: Router;
private mwController: IMaintenanceWindowController; private mwController: any;
constructor(maintenanceWindowController: IMaintenanceWindowController) { constructor(maintenanceWindowController: any) {
this.router = Router(); this.router = Router();
this.mwController = maintenanceWindowController; this.mwController = maintenanceWindowController;
this.initRoutes(); this.initRoutes();
+9 -5
View File
@@ -1,11 +1,15 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "@/middleware/isAllowed.js"; import { isAllowed } from "@/middleware/isAllowed.js";
import { IMonitorController } from "@/controllers/monitorController.js"; import multer from "multer";
import { fetchMonitorCertificate } from "@/controllers/controllerUtils.js";
const upload = multer({
storage: multer.memoryStorage(), // Store file in memory as Buffer
});
class MonitorRoutes { class MonitorRoutes {
private router: Router; private router: Router;
private monitorController: IMonitorController; private monitorController: any;
constructor(monitorController: IMonitorController) { constructor(monitorController: any) {
this.router = Router(); this.router = Router();
this.monitorController = monitorController; this.monitorController = monitorController;
this.initRoutes(); this.initRoutes();
@@ -34,11 +38,10 @@ class MonitorRoutes {
// Util routes // Util routes
this.router.get("/certificate/:monitorId", (req, res, next) => { this.router.get("/certificate/:monitorId", (req, res, next) => {
this.monitorController.getMonitorCertificate(req, res, next); this.monitorController.getMonitorCertificate(req, res, next, fetchMonitorCertificate);
}); });
// General monitor CRUD routes // General monitor CRUD routes
this.router.patch("/notifications", isAllowed(["admin", "superadmin"]), this.monitorController.updateNotifications);
this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor);
this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors);
@@ -47,6 +50,7 @@ class MonitorRoutes {
this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON);
this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON); this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON);
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
this.router.get("/games", this.monitorController.getAllGames); this.router.get("/games", this.monitorController.getAllGames);
// Individual monitor CRUD routes // Individual monitor CRUD routes
+2 -3
View File
@@ -1,10 +1,9 @@
import { INotificationController } from "@/controllers/notificationController.js";
import { Router } from "express"; import { Router } from "express";
class NotificationRoutes { class NotificationRoutes {
private router: Router; private router: Router;
private notificationController: INotificationController; private notificationController: any;
constructor(notificationController: INotificationController) { constructor(notificationController: any) {
this.router = Router(); this.router = Router();
this.notificationController = notificationController; this.notificationController = notificationController;
this.initializeRoutes(); this.initializeRoutes();
+5 -4
View File
@@ -1,19 +1,20 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "@/middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { IJobQueueController } from "@/controllers/queueController.js";
class QueueRoutes { class QueueRoutes {
private router: Router; private router: Router;
private queueController: IJobQueueController; private queueController: any;
constructor(queueController: IJobQueueController) { constructor(queueController: any) {
this.router = Router(); this.router = Router();
this.queueController = queueController; this.queueController = queueController;
this.initRoutes(); this.initRoutes();
} }
initRoutes() { initRoutes() {
this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs); this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs);
this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob);
this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics); this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics);
this.router.get("/health", isAllowed(["admin", "superadmin"]), this.queueController.checkQueueHealth);
this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics); this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics);
this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue); this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue);
} }
+2 -3
View File
@@ -1,12 +1,11 @@
import { Router } from "express"; import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js"; import { isAllowed } from "../middleware/isAllowed.js";
import { ISettingsController } from "@/controllers/settingsController.js";
class SettingsRoutes { class SettingsRoutes {
private router: Router; private router: Router;
private settingsController: ISettingsController; private settingsController: any;
constructor(settingsController: ISettingsController) { constructor(settingsController: any) {
this.router = Router(); this.router = Router();
this.settingsController = settingsController; this.settingsController = settingsController;
this.initRoutes(); this.initRoutes();
+2 -3
View File
@@ -1,13 +1,12 @@
import { IStatusPageController } from "@/controllers/statusPageController.js";
import { RequestHandler, Router } from "express"; import { RequestHandler, Router } from "express";
import multer from "multer"; import multer from "multer";
const upload = multer(); const upload = multer();
class StatusPageRoutes { class StatusPageRoutes {
private router: Router; private router: Router;
private statusPageController: IStatusPageController; private statusPageController: any;
constructor(statusPageController: IStatusPageController, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) { constructor(statusPageController: any, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) {
this.router = Router(); this.router = Router();
this.statusPageController = statusPageController; this.statusPageController = statusPageController;
this.initRoutes(verifyJWT, verifyStatusPageAccess); this.initRoutes(verifyJWT, verifyStatusPageAccess);
+51 -77
View File
@@ -1,48 +1,25 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js";
MonitorStatusResponse,
CheckErrorInfo,
Check,
ILighthouseAudit,
ChecksQueryResult,
ChecksSummary,
MonitorPayloadMap,
} from "@/types/index.js";
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { ParseBoolean } from "@/utils/utils.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "checkService"; const SERVICE_NAME = "checkService";
export interface ICheckService { export interface ICheckService {
createChecks(checks: Check[]): Promise<Check[]>; createChecks(checks: Check[]): Promise<Check[]>;
buildCheck(statusResponse: MonitorStatusResponse<MonitorPayloadMap[keyof MonitorPayloadMap]>): Check | undefined; buildCheck(statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>): Partial<Check> | undefined;
getChecksByMonitor(params: { getChecksByMonitor(params: { monitorId: string; query: Record<string, string>; teamId: string }): Promise<ChecksQueryResult>;
monitorId: string; getChecksByTeam(params: { teamId: string; query: Record<string, string> }): Promise<ChecksQueryResult>;
teamId: string;
sortOrder: string;
dateRange: string;
page: number;
rowsPerPage: number;
filter?: string;
status?: boolean;
}): Promise<ChecksQueryResult>;
getChecksByTeam(params: {
teamId: string;
sortOrder: string;
dateRange: string;
page: number;
rowsPerPage: number;
filter?: string;
}): Promise<ChecksQueryResult>;
getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise<ChecksSummary>; getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise<ChecksSummary>;
deleteChecks(params: { monitorId: string; teamId: string }): Promise<number>; deleteChecks(params: { monitorId: string; teamId: string }): Promise<number>;
deleteChecksByTeamId(params: { teamId: string }): Promise<number>; deleteChecksByTeamId(params: { teamId: string }): Promise<number>;
deleteOlderThan(date: Date): Promise<number>; updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
} }
export class CheckService implements ICheckService { class CheckService implements ICheckService {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
@@ -62,14 +39,12 @@ export class CheckService implements ICheckService {
return this.checksRepository.createChecks(checks); return this.checksRepository.createChecks(checks);
}; };
buildCheck = (statusResponse: MonitorStatusResponse<MonitorPayloadMap[keyof MonitorPayloadMap]>): Check | undefined => { buildCheck = (statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>) => {
const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse; const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse;
const now = new Date().toISOString(); const check: Partial<Check> = {
const check: Check = {
id: new Types.ObjectId().toString(), id: new Types.ObjectId().toString(),
createdAt: now, createdAt: new Date().toISOString(),
updatedAt: now,
metadata: { metadata: {
monitorId, monitorId,
teamId, teamId,
@@ -137,25 +112,7 @@ export class CheckService implements ICheckService {
return check; return check;
}; };
getChecksByMonitor = async ({ getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record<string, string>; teamId: string }) => {
monitorId,
teamId,
sortOrder,
dateRange,
filter,
page,
rowsPerPage,
status,
}: {
monitorId: string;
teamId: string;
sortOrder: string;
dateRange: string;
page: number;
rowsPerPage: number;
status?: boolean;
filter?: string;
}) => {
if (!monitorId) { if (!monitorId) {
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
} }
@@ -166,54 +123,71 @@ export class CheckService implements ICheckService {
// For verification, throws an error if monitor doesn't belong to team // For verification, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId); await this.monitorsRepository.findById(monitorId, teamId);
const parsedPage = page ?? 0; const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
const parsedRowsPerPage = rowsPerPage ?? 5; 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;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, status); const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus);
return result; return result;
}; };
getChecksByTeam = async ({ getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record<string, string> }) => {
teamId, const { sortOrder, dateRange, filter, page, rowsPerPage } = query;
sortOrder, if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
dateRange, throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
filter, }
page,
rowsPerPage, if (!teamId) {
}: { throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
teamId: string; }
sortOrder: string;
dateRange: string; const parsedPage = page ? parseInt(page) : 0;
page: number; const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
rowsPerPage: number;
filter?: string;
}) => {
const parsedPage = page ?? 0;
const parsedRowsPerPage = rowsPerPage ?? 5;
const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId); const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId);
return checkData; return checkData;
}; };
getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => { getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => {
if (!teamId) {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksSummaryByTeamId", status: 400 });
}
const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange); const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange);
return summary; return summary;
}; };
deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => {
if (!monitorId) {
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 });
}
if (!teamId) {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 });
}
// For verification, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId); await this.monitorsRepository.findById(monitorId, teamId);
const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId); const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId);
return deletedCount; return deletedCount;
}; };
deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => { deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => {
if (!teamId) {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecksByTeamId", status: 400 });
}
const deletedCount = await this.checksRepository.deleteByTeamId(teamId); const deletedCount = await this.checksRepository.deleteByTeamId(teamId);
return deletedCount; return deletedCount;
}; };
deleteOlderThan = async (date: Date) => { updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => {
const deletedCount = await this.checksRepository.deleteOlderThan(date); throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
return deletedCount;
}; };
} }
export default CheckService;
@@ -3,19 +3,15 @@ import os from "os";
const SERVICE_NAME = "diagnosticService"; const SERVICE_NAME = "diagnosticService";
export interface IDiagnosticService { interface MemoryUsageMetrics {
getCPUUsage(): Promise<{ userUsageMs: number; systemUsageMs: number; usagePercentage: number }>; rss: number;
getSystemStats(): Promise<{ heapTotal: number;
osStats: { freeMemoryBytes: number; totalMemoryBytes: number }; heapUsed: number;
memoryUsage: Record<keyof NodeJS.MemoryUsage, number>; external: number;
cpuUsage: { userUsageMs: number; systemUsageMs: number; usagePercentage: number }; arrayBuffers: number;
v8HeapStats: { totalHeapSizeBytes: number; usedHeapSizeBytes: number; heapSizeLimitBytes: number };
eventLoopDelayMs: number;
uptimeMs: number;
}>;
} }
export class DiagnosticService implements IDiagnosticService { class DiagnosticService {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
constructor() { constructor() {
@@ -24,7 +20,8 @@ export class DiagnosticService implements IDiagnosticService {
* Clears performance marks after each measurement to prevent memory leaks. * Clears performance marks after each measurement to prevent memory leaks.
*/ */
const obs = new PerformanceObserver((items) => { const obs = new PerformanceObserver((items) => {
items.getEntries(); // Get the first entry but we don't need to store it
items.getEntries()[0];
performance.clearMarks(); performance.clearMarks();
}); });
obs.observe({ entryTypes: ["measure"] }); obs.observe({ entryTypes: ["measure"] });
@@ -106,3 +103,5 @@ export class DiagnosticService implements IDiagnosticService {
return diagnostics; return diagnostics;
}; };
} }
export default DiagnosticService;
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff. Show more